#!/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 : ## Non-directive context — list of : 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[^:]+):\s+(?P\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 ## Directives to warm (list of : ) ## Non-directive context (list of : ) 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())