source-kb 0.2.2__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. cli/__init__.py +50 -0
  2. cli/__main__.py +5 -0
  3. cli/commands/__init__.py +1 -0
  4. cli/commands/anchor_fix.py +47 -0
  5. cli/commands/diff_doc.py +52 -0
  6. cli/commands/dispatch.py +77 -0
  7. cli/commands/extract.py +72 -0
  8. cli/commands/file_list.py +74 -0
  9. cli/commands/index.py +84 -0
  10. cli/commands/lock.py +89 -0
  11. cli/commands/merge.py +60 -0
  12. cli/commands/merge_delta.py +19 -0
  13. cli/commands/metadata.py +24 -0
  14. cli/commands/pipeline.py +45 -0
  15. cli/commands/post_merge.py +43 -0
  16. cli/commands/query.py +52 -0
  17. cli/commands/render.py +101 -0
  18. cli/commands/scan_repos.py +46 -0
  19. cli/commands/setup.py +94 -0
  20. cli/commands/split.py +196 -0
  21. cli/commands/stale_files.py +98 -0
  22. cli/commands/validate.py +191 -0
  23. core/__init__.py +32 -0
  24. core/config.py +261 -0
  25. core/docs/__init__.py +7 -0
  26. core/docs/section_updater.py +286 -0
  27. core/docs/shared.py +149 -0
  28. core/git.py +294 -0
  29. core/interfaces.py +249 -0
  30. core/monitor/__init__.py +5 -0
  31. core/monitor/progress.py +83 -0
  32. core/monitor/prompt_store.py +49 -0
  33. core/paths.py +141 -0
  34. core/preset.py +237 -0
  35. core/preset_accessors.py +202 -0
  36. core/preset_classify.py +132 -0
  37. core/preset_hooks.py +129 -0
  38. core/preset_profile.py +89 -0
  39. core/prompt/__init__.py +7 -0
  40. core/prompt/__main__.py +147 -0
  41. core/prompt/content.py +320 -0
  42. core/prompt/context_manager.py +164 -0
  43. core/prompt/renderer.py +236 -0
  44. core/prompt/response_parser.py +274 -0
  45. core/prompt/templates.py +357 -0
  46. core/prompt/validate_parity.py +162 -0
  47. core/prompt/variables.py +339 -0
  48. core/rag/__init__.py +22 -0
  49. core/rag/__main__.py +136 -0
  50. core/rag/bm25_index.py +268 -0
  51. core/rag/chunker.py +273 -0
  52. core/rag/embedder.py +151 -0
  53. core/rag/indexer.py +292 -0
  54. core/rag/loader.py +89 -0
  55. core/rag/retriever.py +82 -0
  56. core/skeleton/__init__.py +11 -0
  57. core/skeleton/__main__.py +934 -0
  58. core/skeleton/anchor_fix.py +250 -0
  59. core/skeleton/classify.py +331 -0
  60. core/skeleton/cmd_anchor_fix.py +43 -0
  61. core/skeleton/cmd_diff_doc.py +44 -0
  62. core/skeleton/cmd_lock.py +87 -0
  63. core/skeleton/cmd_merge_delta.py +41 -0
  64. core/skeleton/community.py +233 -0
  65. core/skeleton/dependency_graph.py +306 -0
  66. core/skeleton/diff_doc.py +248 -0
  67. core/skeleton/dispatch.py +273 -0
  68. core/skeleton/dispatch_render.py +319 -0
  69. core/skeleton/dispatch_source.py +111 -0
  70. core/skeleton/extract.py +218 -0
  71. core/skeleton/extract_methods.py +298 -0
  72. core/skeleton/file_list.py +239 -0
  73. core/skeleton/impact.py +278 -0
  74. core/skeleton/jar_download.py +177 -0
  75. core/skeleton/jar_resolver.py +186 -0
  76. core/skeleton/loader.py +162 -0
  77. core/skeleton/merge.py +278 -0
  78. core/skeleton/merge_delta.py +229 -0
  79. core/skeleton/metadata.py +96 -0
  80. core/skeleton/metadata_builders.py +264 -0
  81. core/skeleton/module_dag.py +330 -0
  82. core/skeleton/parsers/__init__.py +71 -0
  83. core/skeleton/parsers/jqassistant.py +300 -0
  84. core/skeleton/parsers/jqassistant_cypher.py +225 -0
  85. core/skeleton/parsers/regex.py +171 -0
  86. core/skeleton/parsers/treesitter.py +324 -0
  87. core/skeleton/parsers/treesitter_java.py +284 -0
  88. core/skeleton/parsers/treesitter_multi.py +289 -0
  89. core/skeleton/pom_parser.py +299 -0
  90. core/skeleton/post_merge.py +295 -0
  91. core/skeleton/post_merge_llm.py +82 -0
  92. core/skeleton/query.py +195 -0
  93. core/skeleton/shard_context.py +177 -0
  94. core/skeleton/split.py +180 -0
  95. core/skeleton/split_cache.py +107 -0
  96. core/skeleton/split_feedback.py +174 -0
  97. core/skeleton/split_plan.py +219 -0
  98. core/skeleton/split_plan_helpers.py +305 -0
  99. core/skeleton/split_plan_llm.py +274 -0
  100. core/utils.py +135 -0
  101. core/validators/__init__.py +65 -0
  102. core/validators/__main__.py +215 -0
  103. core/validators/consistency.py +203 -0
  104. core/validators/coverage.py +171 -0
  105. core/validators/duplicates.py +76 -0
  106. core/validators/engine.py +224 -0
  107. core/validators/links.py +76 -0
  108. core/validators/sampling.py +169 -0
  109. core/validators/structure.py +144 -0
  110. engine/__init__.py +7 -0
  111. engine/assembler.py +231 -0
  112. engine/confirm.py +65 -0
  113. engine/dedup.py +106 -0
  114. engine/main.py +211 -0
  115. engine/pipeline/__init__.py +163 -0
  116. engine/pipeline/recovery.py +250 -0
  117. engine/pipeline/steps/__init__.py +23 -0
  118. engine/pipeline/steps/audit.py +220 -0
  119. engine/pipeline/steps/audit_apply.py +195 -0
  120. engine/pipeline/steps/audit_helpers.py +155 -0
  121. engine/pipeline/steps/classify_llm.py +236 -0
  122. engine/pipeline/steps/classify_prompt.py +223 -0
  123. engine/pipeline/steps/finalize.py +160 -0
  124. engine/pipeline/steps/generate.py +169 -0
  125. engine/pipeline/steps/generate_batch.py +197 -0
  126. engine/pipeline/steps/generate_recovery.py +170 -0
  127. engine/pipeline/steps/llm_plan_split.py +253 -0
  128. engine/pipeline/steps/lock.py +64 -0
  129. engine/pipeline/steps/preflight.py +237 -0
  130. engine/pipeline/steps/preflight_adjust.py +147 -0
  131. engine/pipeline/steps/pregenerate.py +130 -0
  132. engine/pipeline/steps/quality.py +81 -0
  133. engine/pipeline/steps/skeleton.py +149 -0
  134. engine/pipeline/steps/source.py +163 -0
  135. engine/pipeline/steps/sync.py +117 -0
  136. engine/pipeline/steps/sync_finalize.py +237 -0
  137. engine/pipeline/steps/sync_update.py +341 -0
  138. engine/pipelines.py +91 -0
  139. engine/runner.py +335 -0
  140. engine/strategies/__init__.py +86 -0
  141. engine/strategies/api.py +128 -0
  142. engine/strategies/delegated.py +50 -0
  143. engine/strategies/dryrun.py +25 -0
  144. engine/two_phase.py +143 -0
  145. mcp_server/__init__.py +73 -0
  146. mcp_server/__main__.py +5 -0
  147. mcp_server/tools/__init__.py +1 -0
  148. mcp_server/tools/config.py +63 -0
  149. mcp_server/tools/discovery.py +276 -0
  150. mcp_server/tools/generation.py +184 -0
  151. mcp_server/tools/planning.py +144 -0
  152. mcp_server/tools/source.py +175 -0
  153. mcp_server/tools/validation.py +140 -0
  154. mcp_server/tools/workflow.py +166 -0
  155. mcp_server/workflow_loader.py +204 -0
  156. presets/generic/audit_dimensions.md +132 -0
  157. presets/generic/doc_types.yaml +152 -0
  158. presets/generic/preset.yaml +115 -0
  159. presets/java-spring/audit_dimensions.md +228 -0
  160. presets/java-spring/audit_dimensions.yaml +203 -0
  161. presets/java-spring/doc_types.yaml +269 -0
  162. presets/java-spring/hooks.py +122 -0
  163. presets/java-spring/preset.yaml +341 -0
  164. presets/java-spring/templates/README.md +34 -0
  165. presets/java-spring/templates/audit-system.md +15 -0
  166. presets/java-spring/templates/subagent-aop.md +105 -0
  167. presets/java-spring/templates/subagent-api.md +63 -0
  168. presets/java-spring/templates/subagent-architecture.md +111 -0
  169. presets/java-spring/templates/subagent-async-events.md +107 -0
  170. presets/java-spring/templates/subagent-audit-api-contracts.md +40 -0
  171. presets/java-spring/templates/subagent-audit-architecture.md +38 -0
  172. presets/java-spring/templates/subagent-audit-business.md +40 -0
  173. presets/java-spring/templates/subagent-audit-data-models.md +40 -0
  174. presets/java-spring/templates/subagent-business.md +129 -0
  175. presets/java-spring/templates/subagent-caching.md +75 -0
  176. presets/java-spring/templates/subagent-database-access.md +114 -0
  177. presets/java-spring/templates/subagent-enum.md +75 -0
  178. presets/java-spring/templates/subagent-error-handling.md +91 -0
  179. presets/java-spring/templates/subagent-external-integrations.md +80 -0
  180. presets/java-spring/templates/subagent-index.md +122 -0
  181. presets/java-spring/templates/subagent-messaging.md +97 -0
  182. presets/java-spring/templates/subagent-model.md +88 -0
  183. presets/java-spring/templates/subagent-observability.md +91 -0
  184. presets/java-spring/templates/subagent-scheduled.md +81 -0
  185. presets/java-spring/templates/subagent-security.md +102 -0
  186. presets/java-spring/templates/subagent-structure.md +101 -0
  187. presets/java-spring/templates/subagent-sync-section.md +34 -0
  188. presets/java-spring/templates/subagent-utils.md +73 -0
  189. presets/java-spring/templates/sync-system.md +8 -0
  190. presets/java-spring/workflow-extensions.md +112 -0
  191. skills/__init__.py +1 -0
  192. skills/_shared/README.md +30 -0
  193. skills/_shared/doc-coverage-shared.md +134 -0
  194. skills/_shared/doc-quality-standard.md +1058 -0
  195. skills/_shared/doc-subagent-rules.md +762 -0
  196. skills/_shared/windows-compat.md +89 -0
  197. skills/kb-audit/SKILL.md +52 -0
  198. skills/kb-audit/rules.md +88 -0
  199. skills/kb-audit/steps/step-01-prepare.md +75 -0
  200. skills/kb-audit/steps/step-02-audit.md +96 -0
  201. skills/kb-audit/steps/step-03-verify.md +65 -0
  202. skills/kb-audit/steps/step-04-report.md +64 -0
  203. skills/kb-init/SKILL.md +146 -0
  204. skills/kb-init/rules.md +187 -0
  205. skills/kb-init/steps/step-01-scope.md +62 -0
  206. skills/kb-init/steps/step-02-source.md +410 -0
  207. skills/kb-init/steps/step-03-generate.md +307 -0
  208. skills/kb-init/steps/step-04-quality.md +92 -0
  209. skills/kb-init/steps/step-05-finalize.md +132 -0
  210. skills/kb-init/templates/core/execution-modes.md +29 -0
  211. skills/kb-init/templates/core/output-only.md +4 -0
  212. skills/kb-init/templates/core/readwrite.md +33 -0
  213. skills/kb-search/SKILL.md +138 -0
  214. skills/kb-search/rules.md +64 -0
  215. skills/kb-sync/SKILL.md +43 -0
  216. skills/kb-sync/rules.md +70 -0
  217. skills/kb-sync/scripts/rebuild_module.py +91 -0
  218. skills/kb-sync/scripts/scan_repos.py +687 -0
  219. skills/kb-sync/steps/step-01-detect.md +72 -0
  220. skills/kb-sync/steps/step-02-update.md +71 -0
  221. skills/kb-sync/steps/step-03-verify.md +47 -0
  222. skills/kb-sync/steps/step-04-finalize.md +52 -0
  223. source_kb-0.2.2.dist-info/METADATA +194 -0
  224. source_kb-0.2.2.dist-info/RECORD +228 -0
  225. source_kb-0.2.2.dist-info/WHEEL +5 -0
  226. source_kb-0.2.2.dist-info/entry_points.txt +3 -0
  227. source_kb-0.2.2.dist-info/licenses/LICENSE +21 -0
  228. source_kb-0.2.2.dist-info/top_level.txt +6 -0
@@ -0,0 +1,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()
@@ -0,0 +1,7 @@
1
+ """core.prompt — prompt rendering and content assembly.
2
+
3
+ Sub-modules:
4
+ renderer — Template loading, variable computation, rendering
5
+ content — Source reading, skeleton reading, context building
6
+ templates — PromptTemplate dataclass + registry
7
+ """
@@ -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()