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,237 @@
|
|
|
1
|
+
"""Sync finalize steps — post-update validation, baseline, and repair.
|
|
2
|
+
|
|
3
|
+
Steps:
|
|
4
|
+
- SharedDocsCascadeStep: update _shared/ docs when cross-module info changes
|
|
5
|
+
- UpdateBaselineStep: update baseline ONLY after validation passes
|
|
6
|
+
- AnchorFixStep: repair broken cross-document links
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
from core.interfaces import Step, StepResult, PipelineContext
|
|
16
|
+
from core.git import get_head_commit
|
|
17
|
+
from engine.pipeline import register_step
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@register_step
|
|
23
|
+
class SharedDocsCascadeStep(Step):
|
|
24
|
+
"""Update _shared/ documents when module changes affect cross-module info.
|
|
25
|
+
|
|
26
|
+
Triggers:
|
|
27
|
+
- New/modified @FeignClient -> _shared/cross-module-calls.md
|
|
28
|
+
- New/modified error codes -> _shared/error-codes.md
|
|
29
|
+
- New/modified @TableName -> _shared/database-ddl.md
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
default_name = "shared-docs-cascade"
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
super().__init__("shared-docs-cascade")
|
|
36
|
+
|
|
37
|
+
def run(self, ctx: PipelineContext) -> StepResult:
|
|
38
|
+
affected_docs = ctx.state.get("affected_docs", {})
|
|
39
|
+
if not affected_docs:
|
|
40
|
+
return StepResult(status="skipped", message="No affected docs")
|
|
41
|
+
|
|
42
|
+
shared_dir = ctx.knowledge_dir / "_shared"
|
|
43
|
+
if not shared_dir.is_dir():
|
|
44
|
+
return StepResult(status="skipped", message="No _shared/ directory")
|
|
45
|
+
|
|
46
|
+
triggers = _detect_shared_doc_triggers(affected_docs)
|
|
47
|
+
if not triggers:
|
|
48
|
+
return StepResult(status="skipped", message="No shared doc impact")
|
|
49
|
+
|
|
50
|
+
logger.info("[shared-docs-cascade] Detected triggers: %s", triggers)
|
|
51
|
+
return StepResult(
|
|
52
|
+
status="ok",
|
|
53
|
+
message=f"Detected {len(triggers)} shared doc triggers (update deferred)",
|
|
54
|
+
details={"triggers": triggers},
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@register_step
|
|
59
|
+
class UpdateBaselineStep(Step):
|
|
60
|
+
"""Update sync baseline ONLY after all validations pass.
|
|
61
|
+
|
|
62
|
+
Preconditions:
|
|
63
|
+
- Not too many failures (fail ratio < 50%)
|
|
64
|
+
- No fatal errors in prior steps
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
default_name = "update-baseline"
|
|
68
|
+
|
|
69
|
+
def __init__(self):
|
|
70
|
+
super().__init__("update-baseline")
|
|
71
|
+
|
|
72
|
+
def run(self, ctx: PipelineContext) -> StepResult:
|
|
73
|
+
failed_sections = ctx.state.get("failed_sections", [])
|
|
74
|
+
updated_sections = ctx.state.get("updated_sections", [])
|
|
75
|
+
total = len(updated_sections) + len(failed_sections)
|
|
76
|
+
|
|
77
|
+
if total > 0 and len(failed_sections) > 0:
|
|
78
|
+
fail_ratio = len(failed_sections) / total
|
|
79
|
+
if fail_ratio > 0.5:
|
|
80
|
+
return StepResult(
|
|
81
|
+
status="skipped",
|
|
82
|
+
message=f"Too many failures ({len(failed_sections)}/{total}), baseline not updated",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if ctx.state.get("dry_run", False):
|
|
86
|
+
_write_dry_run_report(ctx)
|
|
87
|
+
return StepResult(status="skipped", message="Dry-run: baseline not updated")
|
|
88
|
+
|
|
89
|
+
_update_baseline(ctx)
|
|
90
|
+
return StepResult(status="ok", message="Baseline updated")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@register_step
|
|
94
|
+
class AnchorFixStep(Step):
|
|
95
|
+
"""Repair broken cross-document anchor links after document updates."""
|
|
96
|
+
|
|
97
|
+
default_name = "anchor-fix"
|
|
98
|
+
|
|
99
|
+
def __init__(self):
|
|
100
|
+
super().__init__("anchor-fix")
|
|
101
|
+
|
|
102
|
+
def run(self, ctx: PipelineContext) -> StepResult:
|
|
103
|
+
from core.skeleton.anchor_fix import fix_anchors
|
|
104
|
+
|
|
105
|
+
updated_sections = ctx.state.get("updated_sections", [])
|
|
106
|
+
if not updated_sections:
|
|
107
|
+
return StepResult(status="skipped", message="No updates to check")
|
|
108
|
+
|
|
109
|
+
module_names = {s["module"] for s in updated_sections}
|
|
110
|
+
total_fixed = 0
|
|
111
|
+
total_degraded = 0
|
|
112
|
+
|
|
113
|
+
for module_name in module_names:
|
|
114
|
+
module_dir = ctx.knowledge_dir / module_name
|
|
115
|
+
if not module_dir.is_dir():
|
|
116
|
+
continue
|
|
117
|
+
|
|
118
|
+
result = fix_anchors(module_dir)
|
|
119
|
+
total_fixed += result.links_fixed
|
|
120
|
+
total_degraded += result.links_degraded
|
|
121
|
+
|
|
122
|
+
if total_fixed or total_degraded:
|
|
123
|
+
return StepResult(
|
|
124
|
+
status="ok",
|
|
125
|
+
message=f"Anchor fix: {total_fixed} fixed, {total_degraded} degraded",
|
|
126
|
+
)
|
|
127
|
+
return StepResult(status="ok", message="All links valid")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ---------------------------------------------------------------------------
|
|
131
|
+
# Helpers
|
|
132
|
+
# ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def _load_baseline(ctx: PipelineContext) -> dict[str, str]:
|
|
136
|
+
"""Load sync baseline (module -> commit hash)."""
|
|
137
|
+
state_dir = ctx.knowledge_dir.parent / ".source-kb"
|
|
138
|
+
state_file = state_dir / "sync-state.json"
|
|
139
|
+
if not state_file.exists():
|
|
140
|
+
return {}
|
|
141
|
+
try:
|
|
142
|
+
all_state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
143
|
+
return all_state.get(ctx.kb_name, {})
|
|
144
|
+
except (json.JSONDecodeError, OSError):
|
|
145
|
+
return {}
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _update_baseline(ctx: PipelineContext) -> None:
|
|
149
|
+
"""Update sync baseline with current HEAD commits."""
|
|
150
|
+
module_repos: dict[str, Path] = ctx.state.get("module_repos", {})
|
|
151
|
+
if not module_repos:
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
state_dir = ctx.knowledge_dir.parent / ".source-kb"
|
|
155
|
+
state_dir.mkdir(parents=True, exist_ok=True)
|
|
156
|
+
state_file = state_dir / "sync-state.json"
|
|
157
|
+
|
|
158
|
+
all_state: dict = {}
|
|
159
|
+
if state_file.exists():
|
|
160
|
+
try:
|
|
161
|
+
all_state = json.loads(state_file.read_text(encoding="utf-8"))
|
|
162
|
+
except (json.JSONDecodeError, OSError):
|
|
163
|
+
pass
|
|
164
|
+
|
|
165
|
+
kb_state = all_state.setdefault(ctx.kb_name, {})
|
|
166
|
+
for name, repo_path in module_repos.items():
|
|
167
|
+
branch = ctx.state.get("module_branches", {}).get(name, "main")
|
|
168
|
+
commit = get_head_commit(repo_path, ref=f"origin/{branch}")
|
|
169
|
+
if commit:
|
|
170
|
+
kb_state[name] = commit
|
|
171
|
+
|
|
172
|
+
state_file.write_text(
|
|
173
|
+
json.dumps(all_state, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _classify_changes(changed_files: list[str], preset: dict) -> dict[str, list[str]]:
|
|
178
|
+
"""Classify changed files into affected doc types."""
|
|
179
|
+
from core.preset_classify import classify_file
|
|
180
|
+
|
|
181
|
+
classification: dict[str, list[str]] = {}
|
|
182
|
+
for fpath in changed_files:
|
|
183
|
+
categories = classify_file(preset, fpath)
|
|
184
|
+
if categories:
|
|
185
|
+
for cat in categories:
|
|
186
|
+
classification.setdefault(cat, []).append(fpath)
|
|
187
|
+
else:
|
|
188
|
+
fallback_cat = "source"
|
|
189
|
+
doc_types = preset.get("doc_types", {})
|
|
190
|
+
for dt_key, dt_config in doc_types.items():
|
|
191
|
+
if isinstance(dt_config, dict) and not dt_config.get("conditional", True):
|
|
192
|
+
fallback_cat = dt_key
|
|
193
|
+
break
|
|
194
|
+
classification.setdefault(fallback_cat, []).append(fpath)
|
|
195
|
+
|
|
196
|
+
return classification
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _write_dry_run_report(ctx: PipelineContext) -> None:
|
|
200
|
+
"""Write dry-run report with proposed changes."""
|
|
201
|
+
changes = ctx.state.get("dry_run_changes", [])
|
|
202
|
+
updated = ctx.state.get("updated_sections", [])
|
|
203
|
+
|
|
204
|
+
report = {
|
|
205
|
+
"operation": "kb-sync",
|
|
206
|
+
"mode": "dry-run",
|
|
207
|
+
"proposed_changes": changes + [
|
|
208
|
+
{"doc_path": s["doc"], "section": s["heading"],
|
|
209
|
+
"action": s["action"], "module": s["module"]}
|
|
210
|
+
for s in updated
|
|
211
|
+
],
|
|
212
|
+
"summary": {
|
|
213
|
+
"total_changes": len(changes) + len(updated),
|
|
214
|
+
},
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
report_path = ctx.knowledge_dir / ".dry-run-report.json"
|
|
218
|
+
report_path.write_text(
|
|
219
|
+
json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
220
|
+
)
|
|
221
|
+
logger.info("[dry-run] Report written: %s", report_path)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
def _detect_shared_doc_triggers(affected_docs: dict[str, list[str]]) -> list[str]:
|
|
225
|
+
"""Detect if changes affect shared documents."""
|
|
226
|
+
triggers: list[str] = []
|
|
227
|
+
|
|
228
|
+
if any(cat in affected_docs for cat in {"feign", "feign-external"}):
|
|
229
|
+
triggers.append("cross-module-calls")
|
|
230
|
+
|
|
231
|
+
if "error-handling" in affected_docs:
|
|
232
|
+
triggers.append("error-codes")
|
|
233
|
+
|
|
234
|
+
if "model" in affected_docs:
|
|
235
|
+
triggers.append("database-ddl")
|
|
236
|
+
|
|
237
|
+
return triggers
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""Sync update step — LLM-powered incremental document section updates.
|
|
2
|
+
|
|
3
|
+
Steps:
|
|
4
|
+
- IncrementalUpdateStep: LLM call per affected section, or delegated manifest
|
|
5
|
+
|
|
6
|
+
Requirements: Req 11, 15, 19, 24
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, TYPE_CHECKING
|
|
15
|
+
|
|
16
|
+
from core.interfaces import Step, StepResult, PipelineContext, LlmRequest
|
|
17
|
+
from core.utils import validate_path_within_bounds
|
|
18
|
+
from engine.pipeline import register_step
|
|
19
|
+
|
|
20
|
+
if TYPE_CHECKING:
|
|
21
|
+
from core.skeleton.impact import SectionImpact
|
|
22
|
+
|
|
23
|
+
logger = logging.getLogger(__name__)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@register_step
|
|
27
|
+
class IncrementalUpdateStep(Step):
|
|
28
|
+
"""LLM-powered incremental document section updates.
|
|
29
|
+
|
|
30
|
+
Modes:
|
|
31
|
+
- CLI (api): Impact analysis -> LLM call -> validate -> write
|
|
32
|
+
- Delegated: Generate .sync-manifest.json for Agent dispatch
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
default_name = "incremental-update"
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
super().__init__("incremental-update", checkpoint="incremental-update")
|
|
39
|
+
|
|
40
|
+
def run(self, ctx: PipelineContext) -> StepResult:
|
|
41
|
+
from core.preset import load_preset
|
|
42
|
+
from core.skeleton.impact import analyze_impact
|
|
43
|
+
from core.docs.section_updater import replace_section, append_section
|
|
44
|
+
from core.prompt.response_parser import validate_sync_response
|
|
45
|
+
from engine.strategies import create_strategy
|
|
46
|
+
from engine.pipeline.steps.generate import _make_config_obj
|
|
47
|
+
from engine.pipeline.steps.sync_finalize import _classify_changes
|
|
48
|
+
|
|
49
|
+
changed_files = ctx.state.get("changed_files", [])
|
|
50
|
+
if not changed_files:
|
|
51
|
+
return StepResult(status="skipped", message="No changes to process")
|
|
52
|
+
|
|
53
|
+
preset_name = ctx.kb_config.get("preset", "generic")
|
|
54
|
+
preset = load_preset(preset_name)
|
|
55
|
+
config_obj = _make_config_obj(ctx)
|
|
56
|
+
|
|
57
|
+
change_details: dict[str, list[dict]] = ctx.state.get("change_details", {})
|
|
58
|
+
|
|
59
|
+
# Create strategy — delegated mode is handled via response.status, not upfront check
|
|
60
|
+
strategy = create_strategy(config_obj)
|
|
61
|
+
dry_run = ctx.state.get("dry_run", False)
|
|
62
|
+
|
|
63
|
+
total_updated = 0
|
|
64
|
+
total_failed = 0
|
|
65
|
+
all_updated_sections: list[dict] = []
|
|
66
|
+
all_failed_sections: list[dict] = []
|
|
67
|
+
|
|
68
|
+
for module_name, changes in change_details.items():
|
|
69
|
+
module_dir = ctx.knowledge_dir / module_name
|
|
70
|
+
source_cache = ctx.cache_dir / module_name
|
|
71
|
+
|
|
72
|
+
if not module_dir.is_dir():
|
|
73
|
+
continue
|
|
74
|
+
|
|
75
|
+
module_files = [c["file"] for c in changes]
|
|
76
|
+
|
|
77
|
+
plan = analyze_impact(
|
|
78
|
+
changed_files=module_files,
|
|
79
|
+
module_dir=module_dir,
|
|
80
|
+
preset=preset,
|
|
81
|
+
source_cache=source_cache,
|
|
82
|
+
change_details=changes,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
if not plan.sections:
|
|
86
|
+
logger.info("[sync] %s: no sections affected", module_name)
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
for impact in plan.sections:
|
|
90
|
+
doc_path = module_dir / impact.doc_path
|
|
91
|
+
if not validate_path_within_bounds(doc_path, module_dir):
|
|
92
|
+
logger.warning("[sync] Path traversal blocked: %s", doc_path)
|
|
93
|
+
total_failed += 1
|
|
94
|
+
continue
|
|
95
|
+
if not doc_path.exists() and impact.change_type != "create":
|
|
96
|
+
all_failed_sections.append({
|
|
97
|
+
"module": module_name, "doc": impact.doc_path,
|
|
98
|
+
"heading": impact.section_heading, "reason": "doc not found",
|
|
99
|
+
})
|
|
100
|
+
total_failed += 1
|
|
101
|
+
continue
|
|
102
|
+
|
|
103
|
+
old_content = ""
|
|
104
|
+
if doc_path.exists() and impact.change_type == "update":
|
|
105
|
+
old_content = _read_section_content(doc_path, impact.section_heading)
|
|
106
|
+
|
|
107
|
+
system_prompt = _build_sync_system_prompt(impact.doc_path)
|
|
108
|
+
user_prompt = _build_sync_user_prompt(
|
|
109
|
+
impact, old_content, module_files, source_cache
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
if dry_run:
|
|
113
|
+
ctx.state.setdefault("dry_run_changes", []).append({
|
|
114
|
+
"module": module_name,
|
|
115
|
+
"doc_path": impact.doc_path,
|
|
116
|
+
"section": impact.section_heading,
|
|
117
|
+
"action": impact.change_type,
|
|
118
|
+
"affected_files": impact.affected_files,
|
|
119
|
+
})
|
|
120
|
+
total_updated += 1
|
|
121
|
+
continue
|
|
122
|
+
|
|
123
|
+
request = LlmRequest(system=system_prompt, user=user_prompt)
|
|
124
|
+
try:
|
|
125
|
+
response = strategy.call(request)
|
|
126
|
+
except Exception as e:
|
|
127
|
+
logger.error("[sync] LLM call failed for %s/%s: %s",
|
|
128
|
+
module_name, impact.section_heading, e)
|
|
129
|
+
all_failed_sections.append({
|
|
130
|
+
"module": module_name, "doc": impact.doc_path,
|
|
131
|
+
"heading": impact.section_heading, "reason": str(e),
|
|
132
|
+
})
|
|
133
|
+
total_failed += 1
|
|
134
|
+
continue
|
|
135
|
+
|
|
136
|
+
if response.status == "delegated":
|
|
137
|
+
# Accumulate for manifest generation
|
|
138
|
+
ctx.state.setdefault("_delegated_sections", []).append({
|
|
139
|
+
"module": module_name,
|
|
140
|
+
"doc_path": impact.doc_path,
|
|
141
|
+
"section_heading": impact.section_heading,
|
|
142
|
+
"action": impact.change_type,
|
|
143
|
+
"affected_files": impact.affected_files,
|
|
144
|
+
"confidence": impact.confidence,
|
|
145
|
+
})
|
|
146
|
+
total_updated += 1
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
if response.status != "done":
|
|
150
|
+
all_failed_sections.append({
|
|
151
|
+
"module": module_name, "doc": impact.doc_path,
|
|
152
|
+
"heading": impact.section_heading,
|
|
153
|
+
"reason": f"LLM status: {response.status}",
|
|
154
|
+
})
|
|
155
|
+
total_failed += 1
|
|
156
|
+
continue
|
|
157
|
+
|
|
158
|
+
is_valid, content = validate_sync_response(response.content, old_content)
|
|
159
|
+
if not is_valid:
|
|
160
|
+
logger.warning("[sync] Invalid response for %s/%s: %s",
|
|
161
|
+
module_name, impact.section_heading, content)
|
|
162
|
+
_mark_unverified(doc_path, impact.section_heading)
|
|
163
|
+
all_failed_sections.append({
|
|
164
|
+
"module": module_name, "doc": impact.doc_path,
|
|
165
|
+
"heading": impact.section_heading, "reason": content,
|
|
166
|
+
})
|
|
167
|
+
total_failed += 1
|
|
168
|
+
continue
|
|
169
|
+
|
|
170
|
+
if impact.change_type == "update":
|
|
171
|
+
replace_section(doc_path, impact.section_heading, content)
|
|
172
|
+
elif impact.change_type == "create":
|
|
173
|
+
append_section(doc_path, impact.section_heading, content)
|
|
174
|
+
|
|
175
|
+
all_updated_sections.append({
|
|
176
|
+
"module": module_name, "doc": impact.doc_path,
|
|
177
|
+
"heading": impact.section_heading, "action": impact.change_type,
|
|
178
|
+
})
|
|
179
|
+
total_updated += 1
|
|
180
|
+
|
|
181
|
+
ctx.state["updated_sections"] = all_updated_sections
|
|
182
|
+
ctx.state["failed_sections"] = all_failed_sections
|
|
183
|
+
|
|
184
|
+
affected_docs = _classify_changes(changed_files, preset)
|
|
185
|
+
ctx.state["affected_docs"] = affected_docs
|
|
186
|
+
|
|
187
|
+
# If delegated mode accumulated sections, write manifest and return delegated
|
|
188
|
+
delegated_sections = ctx.state.pop("_delegated_sections", [])
|
|
189
|
+
if delegated_sections:
|
|
190
|
+
return self._write_manifest(ctx, delegated_sections, change_details)
|
|
191
|
+
|
|
192
|
+
if total_updated == 0 and total_failed == 0:
|
|
193
|
+
return StepResult(status="skipped", message="No sections affected")
|
|
194
|
+
|
|
195
|
+
return StepResult(
|
|
196
|
+
status="ok",
|
|
197
|
+
message=f"Updated {total_updated} sections, {total_failed} failed",
|
|
198
|
+
details={"updated": total_updated, "failed": total_failed},
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
def _write_manifest(
|
|
202
|
+
self, ctx: PipelineContext, delegated_sections: list[dict],
|
|
203
|
+
change_details: dict[str, list[dict]],
|
|
204
|
+
) -> StepResult:
|
|
205
|
+
"""Write sync manifest from accumulated delegated responses."""
|
|
206
|
+
manifest = {"operation": "kb-sync", "modules": []}
|
|
207
|
+
|
|
208
|
+
# Group sections by module
|
|
209
|
+
by_module: dict[str, list[dict]] = {}
|
|
210
|
+
for s in delegated_sections:
|
|
211
|
+
by_module.setdefault(s["module"], []).append(s)
|
|
212
|
+
|
|
213
|
+
for module_name, sections in by_module.items():
|
|
214
|
+
module_files = [c["file"] for c in change_details.get(module_name, [])]
|
|
215
|
+
module_entry = {
|
|
216
|
+
"name": module_name,
|
|
217
|
+
"changed_files": module_files,
|
|
218
|
+
"affected_sections": [
|
|
219
|
+
{
|
|
220
|
+
"doc_path": s["doc_path"],
|
|
221
|
+
"section_heading": s["section_heading"],
|
|
222
|
+
"action": s["action"],
|
|
223
|
+
"affected_files": s["affected_files"],
|
|
224
|
+
"confidence": s.get("confidence", 1.0),
|
|
225
|
+
}
|
|
226
|
+
for s in sections
|
|
227
|
+
],
|
|
228
|
+
}
|
|
229
|
+
manifest["modules"].append(module_entry)
|
|
230
|
+
|
|
231
|
+
manifest_path = ctx.knowledge_dir / ".sync-manifest.json"
|
|
232
|
+
manifest_path.write_text(
|
|
233
|
+
json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8"
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
total_sections = sum(len(m["affected_sections"]) for m in manifest["modules"])
|
|
237
|
+
return StepResult(
|
|
238
|
+
status="delegated",
|
|
239
|
+
message=f"Manifest: {total_sections} sections in {len(manifest['modules'])} modules",
|
|
240
|
+
details={"manifest_path": str(manifest_path), "sections": total_sections},
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Helpers
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _read_section_content(doc_path: Path, heading: str) -> str:
|
|
250
|
+
"""Read the content of a specific section from a document."""
|
|
251
|
+
from core.docs.section_updater import find_section_boundaries
|
|
252
|
+
|
|
253
|
+
try:
|
|
254
|
+
content = doc_path.read_text(encoding="utf-8", errors="replace")
|
|
255
|
+
except OSError:
|
|
256
|
+
return ""
|
|
257
|
+
|
|
258
|
+
lines = content.split("\n")
|
|
259
|
+
if lines and lines[-1] == "":
|
|
260
|
+
lines = lines[:-1]
|
|
261
|
+
|
|
262
|
+
bounds = find_section_boundaries(lines, heading)
|
|
263
|
+
if bounds is None:
|
|
264
|
+
return ""
|
|
265
|
+
|
|
266
|
+
start, end = bounds
|
|
267
|
+
section_lines = lines[start + 1:end]
|
|
268
|
+
return "\n".join(section_lines).strip()
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _mark_unverified(doc_path: Path, heading: str) -> None:
|
|
272
|
+
"""Mark a section as unverified by prepending a warning marker."""
|
|
273
|
+
from core.docs.section_updater import find_section_boundaries
|
|
274
|
+
|
|
275
|
+
try:
|
|
276
|
+
content = doc_path.read_text(encoding="utf-8")
|
|
277
|
+
except OSError:
|
|
278
|
+
return
|
|
279
|
+
|
|
280
|
+
lines = content.split("\n")
|
|
281
|
+
if lines and lines[-1] == "":
|
|
282
|
+
lines = lines[:-1]
|
|
283
|
+
|
|
284
|
+
bounds = find_section_boundaries(lines, heading)
|
|
285
|
+
if bounds is None:
|
|
286
|
+
return
|
|
287
|
+
|
|
288
|
+
start, end = bounds
|
|
289
|
+
for i in range(start + 1, end):
|
|
290
|
+
if lines[i].strip():
|
|
291
|
+
if not lines[i].startswith("[UNVERIFIED]"):
|
|
292
|
+
lines[i] = f"[UNVERIFIED] {lines[i]}"
|
|
293
|
+
break
|
|
294
|
+
|
|
295
|
+
doc_path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _build_sync_system_prompt(doc_type: str) -> str:
|
|
299
|
+
"""Build system prompt for sync section update."""
|
|
300
|
+
from core.preset import find_preset_template
|
|
301
|
+
|
|
302
|
+
template_path = find_preset_template("java-spring", "sync-system.md")
|
|
303
|
+
if template_path and template_path.exists():
|
|
304
|
+
try:
|
|
305
|
+
return template_path.read_text(encoding="utf-8").strip()
|
|
306
|
+
except OSError:
|
|
307
|
+
pass
|
|
308
|
+
return (
|
|
309
|
+
"You are a knowledge base maintainer. Update the specific document section "
|
|
310
|
+
"based on source code changes.\n\n"
|
|
311
|
+
"Rules:\n"
|
|
312
|
+
"- Only rewrite the affected section content, maintain the style of the rest\n"
|
|
313
|
+
"- Describe from a business perspective, not code translation\n"
|
|
314
|
+
"- Output section body only (without the heading line)\n"
|
|
315
|
+
"- Do not fabricate; mark uncertain content with [UNVERIFIED]"
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
|
|
319
|
+
def _build_sync_user_prompt(
|
|
320
|
+
impact: SectionImpact, old_content: str, changed_files: list[str], source_cache: Path
|
|
321
|
+
) -> str:
|
|
322
|
+
"""Build user prompt for sync section update."""
|
|
323
|
+
parts = [
|
|
324
|
+
f"Document: {impact.doc_path}",
|
|
325
|
+
f"Section: {impact.section_heading}",
|
|
326
|
+
f"Change type: {impact.change_type}",
|
|
327
|
+
"",
|
|
328
|
+
"Current section content:",
|
|
329
|
+
old_content if old_content else "(empty)",
|
|
330
|
+
"",
|
|
331
|
+
"Changed files:",
|
|
332
|
+
]
|
|
333
|
+
for f in impact.affected_files[:10]:
|
|
334
|
+
parts.append(f" - {f}")
|
|
335
|
+
if len(impact.affected_files) > 10:
|
|
336
|
+
parts.append(f" ... and {len(impact.affected_files) - 10} more files")
|
|
337
|
+
|
|
338
|
+
parts.append("")
|
|
339
|
+
parts.append("Rewrite this section based on the source code changes.")
|
|
340
|
+
|
|
341
|
+
return "\n".join(parts)
|
engine/pipelines.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Pipeline builders — compose steps into named pipelines.
|
|
2
|
+
|
|
3
|
+
Usage:
|
|
4
|
+
from engine.pipelines import build_init_pipeline, build_sync_pipeline, build_audit_pipeline
|
|
5
|
+
|
|
6
|
+
pipeline = build_init_pipeline()
|
|
7
|
+
result = pipeline.execute(ctx)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from engine.pipeline import Pipeline
|
|
13
|
+
from engine.pipeline.steps.lock import AcquireLockStep, ReleaseLockStep
|
|
14
|
+
from engine.pipeline.steps.source import FetchSourceStep, BuildSourceStep, ResolveDepsStep
|
|
15
|
+
from engine.pipeline.steps.skeleton import ExtractSkeletonStep, ExtractFileListStep, ClassifyStep
|
|
16
|
+
from engine.pipeline.steps.pregenerate import PregenerateMetadataStep, DispatchPlanStep
|
|
17
|
+
from engine.pipeline.steps.preflight import PreflightCheckStep
|
|
18
|
+
from engine.pipeline.steps.generate import GenerateDocsStep
|
|
19
|
+
from engine.pipeline.steps.quality import ValidateStep, CoveragePreCheckStep
|
|
20
|
+
from engine.pipeline.steps.finalize import (
|
|
21
|
+
MergeShardsStep, DeduplicateStep, RebuildIndexStep,
|
|
22
|
+
CleanProgressStep, SharedDocsStep,
|
|
23
|
+
)
|
|
24
|
+
from engine.pipeline.steps.sync import (
|
|
25
|
+
UpdateCacheStep, DetectChangesStep,
|
|
26
|
+
)
|
|
27
|
+
from engine.pipeline.steps.sync_update import IncrementalUpdateStep
|
|
28
|
+
from engine.pipeline.steps.sync_finalize import (
|
|
29
|
+
SharedDocsCascadeStep, UpdateBaselineStep, AnchorFixStep,
|
|
30
|
+
)
|
|
31
|
+
from engine.pipeline.steps.audit import FetchAndScaleStep, AuditDocsStep
|
|
32
|
+
from engine.pipeline.steps.audit_apply import ApplyFixesStep, GenerateReportStep
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def build_init_pipeline() -> Pipeline:
|
|
36
|
+
"""Full kb-init pipeline."""
|
|
37
|
+
return Pipeline("kb-init", [
|
|
38
|
+
AcquireLockStep(timeout_minutes=120),
|
|
39
|
+
FetchSourceStep(),
|
|
40
|
+
BuildSourceStep(),
|
|
41
|
+
ResolveDepsStep(),
|
|
42
|
+
ExtractSkeletonStep(),
|
|
43
|
+
ExtractFileListStep(),
|
|
44
|
+
ClassifyStep(),
|
|
45
|
+
CoveragePreCheckStep(),
|
|
46
|
+
PregenerateMetadataStep(),
|
|
47
|
+
DispatchPlanStep(),
|
|
48
|
+
PreflightCheckStep(),
|
|
49
|
+
GenerateDocsStep(),
|
|
50
|
+
MergeShardsStep(),
|
|
51
|
+
DeduplicateStep(),
|
|
52
|
+
ValidateStep(),
|
|
53
|
+
SharedDocsStep(),
|
|
54
|
+
CleanProgressStep(),
|
|
55
|
+
RebuildIndexStep(),
|
|
56
|
+
ReleaseLockStep(),
|
|
57
|
+
])
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def build_sync_pipeline() -> Pipeline:
|
|
61
|
+
"""Incremental kb-sync pipeline."""
|
|
62
|
+
return Pipeline("kb-sync", [
|
|
63
|
+
AcquireLockStep(timeout_minutes=30),
|
|
64
|
+
UpdateCacheStep(),
|
|
65
|
+
BuildSourceStep(),
|
|
66
|
+
DetectChangesStep(),
|
|
67
|
+
ExtractSkeletonStep(),
|
|
68
|
+
ExtractFileListStep(),
|
|
69
|
+
IncrementalUpdateStep(),
|
|
70
|
+
SharedDocsCascadeStep(),
|
|
71
|
+
ValidateStep(),
|
|
72
|
+
UpdateBaselineStep(),
|
|
73
|
+
AnchorFixStep(),
|
|
74
|
+
RebuildIndexStep(),
|
|
75
|
+
ReleaseLockStep(),
|
|
76
|
+
])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def build_audit_pipeline() -> Pipeline:
|
|
80
|
+
"""kb-audit pipeline."""
|
|
81
|
+
return Pipeline("kb-audit", [
|
|
82
|
+
AcquireLockStep(timeout_minutes=30),
|
|
83
|
+
FetchAndScaleStep(),
|
|
84
|
+
AuditDocsStep(),
|
|
85
|
+
ApplyFixesStep(),
|
|
86
|
+
AnchorFixStep(),
|
|
87
|
+
ValidateStep(),
|
|
88
|
+
RebuildIndexStep(),
|
|
89
|
+
GenerateReportStep(),
|
|
90
|
+
ReleaseLockStep(),
|
|
91
|
+
])
|