Private
Public Access
0
0
Files
manual_slop/scripts/aggregate_directives.py
T

238 lines
8.6 KiB
Python

#!/usr/bin/env python3
"""Aggregate directive bodies from a preset markdown file.
Supports three preset sections:
## Inherits — name of parent preset (resolved recursively, 3-level max)
## Directives to warm — list of <name>: <path to v1.md>
## Non-directive context — list of <description>: <path to full markdown file>
Child directives + non-directive files override parent on name/path conflict.
"""
from __future__ import annotations
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
if hasattr(sys.stdout, "reconfigure"):
try:
sys.stdout.reconfigure(encoding="utf-8")
except Exception:
pass
if hasattr(sys.stderr, "reconfigure"):
try:
sys.stderr.reconfigure(encoding="utf-8")
except Exception:
pass
REPO_ROOT = Path(__file__).resolve().parent.parent
ENTRY_RE = re.compile(r"^\s*-\s+(?P<name>[^:]+):\s+(?P<path>\S+)\s*$")
EXPECTED_PATTERN_TEMPLATE = r"^conductor/directives/{name}/v\d+\.md$"
MAX_INHERIT_DEPTH = 3
@dataclass(frozen=True, slots=True)
class PresetSections:
inherits: str
directives: list[tuple[str, Path]]
non_directive_context: list[tuple[str, Path]]
def parse_preset_sections(preset_path: Path) -> PresetSections:
"""Parse a preset markdown file into Inherits, Directives, Non-directive context."""
if not preset_path.exists():
raise FileNotFoundError("preset file not found: " + str(preset_path))
text = preset_path.read_text(encoding="utf-8")
inherits: str = ""
directives: list[tuple[str, Path]] = []
non_directive: list[tuple[str, Path]] = []
current_section: str = ""
for line in text.splitlines():
if line.startswith("## Inherits"):
current_section = "inherits"
continue
if line.startswith("## Directives to warm"):
current_section = "directives"
continue
if line.startswith("## Non-directive context"):
current_section = "non_directive"
continue
if line.startswith("## ") or line.startswith("# "):
current_section = ""
continue
if current_section == "inherits" and line.strip():
inherits = line.strip()
elif current_section in ("directives", "non_directive"):
m = ENTRY_RE.match(line)
if m:
name = m.group("name")
rel_path = Path(m.group("path"))
if current_section == "directives":
directives.append((name, rel_path))
else:
non_directive.append((name, rel_path))
return PresetSections(inherits=inherits, directives=directives, non_directive_context=non_directive)
def resolve_inheritance(
preset_path: Path,
root: Path | None = None,
_visited: frozenset[str] | None = None,
_depth: int = 0,
) -> PresetSections:
"""Resolve a preset, recursively flattening parent directives + non-directive files."""
if root is None:
root = REPO_ROOT
p = preset_path if preset_path.is_absolute() else (root / preset_path).resolve()
if _visited is None:
_visited = frozenset()
key = str(p)
if key in _visited:
raise ValueError("inheritance loop detected at " + key)
if _depth > MAX_INHERIT_DEPTH:
raise ValueError("inheritance depth exceeds " + str(MAX_INHERIT_DEPTH) + " at " + key)
sections = parse_preset_sections(p)
if not sections.inherits:
return sections
parent_name = sections.inherits
parent_path = (root / "conductor" / "directives" / "presets" / (parent_name + ".md")).resolve()
if not parent_path.exists():
raise FileNotFoundError("parent preset not found: " + str(parent_path))
parent = resolve_inheritance(parent_path, root=root, _visited=_visited | {key}, _depth=_depth + 1)
merged_directives: list[tuple[str, Path]] = []
child_names: set[str] = {n for n, _ in sections.directives}
for name, path in parent.directives:
if name not in child_names:
merged_directives.append((name, path))
for name, path in sections.directives:
merged_directives.append((name, path))
merged_non_directive: list[tuple[str, Path]] = []
child_paths: set[Path] = {p for _, p in sections.non_directive_context}
for desc, path in parent.non_directive_context:
if path not in child_paths:
merged_non_directive.append((desc, path))
for desc, path in sections.non_directive_context:
merged_non_directive.append((desc, path))
return PresetSections(inherits="", directives=merged_directives, non_directive_context=merged_non_directive)
def aggregate_directives(
preset_path: str | Path,
*,
max_chars: int = 0,
project_root: str | Path | None = None,
) -> str:
"""
Read preset at preset_path, resolve inheritance, read each directive's v1.md
and each non-directive-context file, concatenate into a single text blob.
Preset format:
## Inherits <parent_name>
## Directives to warm (list of <name>: <relative path to v1.md>)
## Non-directive context (list of <description>: <relative path>)
Directives appear first (same ===== banner format as before), then a
NON-DIRECTIVE CONTEXT section appends each non-directive file's full body.
"""
root = Path(project_root).resolve() if project_root is not None else REPO_ROOT
p = Path(preset_path)
if not p.is_absolute():
p = (root / p).resolve()
if not p.exists():
raise FileNotFoundError("preset file not found: " + str(p))
sections = resolve_inheritance(p, root=root)
if not sections.directives and not sections.non_directive_context:
raise ValueError("no directive or non-directive entries found in " + str(p))
missing: list[tuple[str, Path]] = []
directive_bodies: list[tuple[str, str]] = []
for name, rel in sections.directives:
abs_path = (root / rel).resolve()
if not abs_path.exists():
missing.append((name, abs_path))
continue
try:
rel_str = abs_path.relative_to(root).as_posix()
except ValueError:
rel_str = str(abs_path)
pattern = EXPECTED_PATTERN_TEMPLATE.format(name=re.escape(name))
if not re.match(pattern, rel_str):
print("WARNING: " + name + ": path " + str(abs_path) + " does not match pattern conductor/directives/" + name + "/vN.md; including anyway", file=sys.stderr)
try:
body = abs_path.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
raise ValueError(name + ": UTF-8 decode error in " + str(abs_path) + ": " + str(e)) from e
directive_bodies.append((name, body))
if missing:
names = ", ".join(n for n, _ in missing)
raise FileNotFoundError("missing v1.md files for directives: " + names)
non_directive_bodies: list[tuple[str, str]] = []
for desc, rel in sections.non_directive_context:
abs_path = (root / rel).resolve()
if not abs_path.exists():
print("WARNING: non-directive context file not found: " + str(abs_path), file=sys.stderr)
continue
try:
body = abs_path.read_text(encoding="utf-8")
except UnicodeDecodeError as e:
raise ValueError(desc + ": UTF-8 decode error in " + str(abs_path) + ": " + str(e)) from e
non_directive_bodies.append((desc, body))
out_lines: list[str] = []
for name, body in directive_bodies:
out_lines.append("=" * 40)
out_lines.append(name)
out_lines.append("=" * 40)
out_lines.append("")
out_lines.append(body.rstrip("\n"))
out_lines.append("")
out_lines.append("")
if non_directive_bodies:
out_lines.append("=" * 40)
out_lines.append("NON-DIRECTIVE CONTEXT")
out_lines.append("=" * 40)
out_lines.append("")
for desc, body in non_directive_bodies:
out_lines.append("--- " + desc + " ---")
out_lines.append("")
out_lines.append(body.rstrip("\n"))
out_lines.append("")
out_lines.append("")
rendered = "\n".join(out_lines)
if max_chars > 0 and len(rendered) > max_chars:
truncated = rendered[:max_chars]
suffix = "\n\n[truncated: showing " + str(max_chars) + " of " + str(len(rendered)) + " chars; omit max_chars for full text]"
rendered = truncated + suffix
return rendered
def aggregate(preset_path: Path, output_path: Path | None) -> int:
try:
rendered = aggregate_directives(preset_path)
except (FileNotFoundError, ValueError) as e:
print("ERROR: " + str(e), file=sys.stderr)
return 1
if output_path:
output_path.write_text(rendered, encoding="utf-8")
print("Wrote " + str(rendered.count("=" * 40) // 2) + " sections to " + str(output_path), file=sys.stderr)
else:
sys.stdout.write(rendered)
if not rendered.endswith("\n"):
sys.stdout.write("\n")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description="Aggregate directive bodies from a preset markdown file.")
parser.add_argument("preset", type=Path, help="Path to the preset markdown file")
parser.add_argument("-o", "--output", type=Path, default=None, help="Write output to file instead of stdout")
args = parser.parse_args()
if not args.preset.exists():
print("ERROR: preset file not found: " + str(args.preset), file=sys.stderr)
return 1
output = args.output.resolve() if args.output else None
return aggregate(args.preset.resolve(), output)
if __name__ == "__main__":
sys.exit(main())