130 lines
4.0 KiB
Python
130 lines
4.0 KiB
Python
# [DEF:MergeSpec:Module]
|
||
# @TIER: TRIVIAL
|
||
# @COMPLEXITY: 1
|
||
# @LAYER: Infra
|
||
|
||
# [DEF:merge_spec:Module]
|
||
# @TIER: TRIVIAL
|
||
# @COMPLEXITY: 1
|
||
# @LAYER: Infra
|
||
|
||
import sys
|
||
from datetime import datetime
|
||
from pathlib import Path
|
||
|
||
REVIEW_PROMPT = (
|
||
"Другая LLM создала этот feature-пакет. Твоя задача - провести независимое "
|
||
"ортогональное spec review, оценить готовность спецификации, найти противоречия, "
|
||
"пробелы, риски реализации и подготовить структурированный отчет с корректировками. "
|
||
"Сфокусируйся именно на review пакета спецификации, а не на переписывании реализации."
|
||
)
|
||
|
||
CANONICAL_MD_STAGES = (
|
||
("exact", "spec.md"),
|
||
("exact", "ux_reference.md"),
|
||
("prefix", "checklists/"),
|
||
("exact", "plan.md"),
|
||
("exact", "research.md"),
|
||
("exact", "data-model.md"),
|
||
("prefix", "contracts/"),
|
||
("exact", "quickstart.md"),
|
||
("exact", "tasks.md"),
|
||
)
|
||
|
||
|
||
def relative_key(path: Path, root: Path) -> str:
|
||
return path.relative_to(root).as_posix()
|
||
|
||
|
||
def ordered_markdown_files(target_dir: Path) -> list[Path]:
|
||
markdown_files = [path for path in target_dir.rglob("*.md") if path.is_file()]
|
||
remaining = {relative_key(path, target_dir): path for path in markdown_files}
|
||
|
||
ordered: list[Path] = []
|
||
|
||
for stage_type, stage_value in CANONICAL_MD_STAGES:
|
||
if stage_type == "exact":
|
||
path = remaining.pop(stage_value, None)
|
||
if path is not None:
|
||
ordered.append(path)
|
||
continue
|
||
|
||
stage_matches = sorted(
|
||
[
|
||
path
|
||
for relative_path, path in remaining.items()
|
||
if relative_path.startswith(stage_value)
|
||
],
|
||
key=lambda path: relative_key(path, target_dir),
|
||
)
|
||
ordered.extend(stage_matches)
|
||
for path in stage_matches:
|
||
remaining.pop(relative_key(path, target_dir), None)
|
||
|
||
ordered.extend(
|
||
sorted(remaining.values(), key=lambda path: relative_key(path, target_dir))
|
||
)
|
||
|
||
return ordered
|
||
|
||
|
||
def merge_specs(feature_number):
|
||
specs_dir = Path("specs")
|
||
if not specs_dir.exists():
|
||
print("Error: 'specs' directory not found.")
|
||
return
|
||
|
||
# Find the directory starting with the feature number
|
||
target_dir = None
|
||
for item in specs_dir.iterdir():
|
||
if item.is_dir() and item.name.startswith(f"{feature_number}-"):
|
||
target_dir = item
|
||
break
|
||
|
||
if not target_dir:
|
||
print(
|
||
f"Error: No directory found for feature number '{feature_number}' in 'specs/'."
|
||
)
|
||
return
|
||
|
||
feature_name = target_dir.name
|
||
now = datetime.now().strftime("%Y%m%d-%H%M%S")
|
||
output_filename = f"{feature_name}-{now}.md"
|
||
|
||
content_blocks = [
|
||
REVIEW_PROMPT,
|
||
"",
|
||
"Порядок артефактов: spec -> ux_reference -> checklist -> plan -> research -> data-model -> contracts -> quickstart -> tasks -> remaining markdown.",
|
||
"",
|
||
]
|
||
|
||
files_to_merge = ordered_markdown_files(target_dir)
|
||
|
||
for file_path in files_to_merge:
|
||
relative_path = file_path.relative_to(target_dir)
|
||
try:
|
||
with open(file_path, "r", encoding="utf-8") as f:
|
||
file_content = f.read()
|
||
|
||
content_blocks.append(f"--- FILE: {relative_path} ---\n")
|
||
content_blocks.append(file_content)
|
||
content_blocks.append("\n")
|
||
except Exception as e:
|
||
print(f"Skipping file {file_path} due to error: {e}")
|
||
|
||
with open(output_filename, "w", encoding="utf-8") as f:
|
||
f.write("\n".join(content_blocks))
|
||
|
||
print(f"Successfully created: {output_filename}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
if len(sys.argv) < 2:
|
||
print("Usage: python merge_spec.py <feature_number>")
|
||
sys.exit(1)
|
||
|
||
merge_specs(sys.argv[1])
|
||
# [/DEF:merge_spec:Module]
|
||
|
||
# [/DEF:MergeSpec:Module]
|