#!/usr/bin/env python3 from __future__ import annotations import argparse from datetime import date from pathlib import Path from vault_common import ( REQUIRED_FRONTMATTER, ROOT, VALID_STATUS, VALID_TYPE, all_records, as_list, json_print, note_record, note_files, rel, split_frontmatter, ) def resolve_paths(paths: list[str]) -> list[Path]: if not paths: return note_files() result = [] for item in paths: path = Path(item) if not path.is_absolute(): path = ROOT / path result.append(path) return result def validate_record(record: dict[str, object]) -> list[str]: fm = record["frontmatter"] issues = [] if not record["has_frontmatter"]: issues.append("missing_frontmatter") for field in sorted(REQUIRED_FRONTMATTER): if field not in fm or fm[field] in (None, ""): issues.append(f"missing_{field}") if fm.get("status") and str(fm["status"]) not in VALID_STATUS: issues.append("invalid_status") if fm.get("type") and str(fm["type"]) not in VALID_TYPE: issues.append("invalid_type") if "tags" in fm and not isinstance(fm["tags"], list): issues.append("tags_not_list") if "aliases" in fm and fm["aliases"] not in (None, "") and not isinstance(fm["aliases"], list): issues.append("aliases_not_list") return issues def check(paths: list[Path]) -> list[dict[str, object]]: results = [] for path in paths: record = note_record(path) issues = validate_record(record) if issues: results.append({"path": rel(path), "issues": issues}) return results def yaml_list(values: list[str]) -> list[str]: if not values: return ["[]"] return [""] + [f" - {value}" for value in values] def normalize(path: Path) -> dict[str, object]: text = path.read_text(encoding="utf-8") fm, body, _ = split_frontmatter(text) title = fm.get("title") or path.stem status = fm.get("status") if fm.get("status") in VALID_STATUS else "seed" note_type = fm.get("type") if fm.get("type") in VALID_TYPE else "concept" tags = as_list(fm.get("tags")) aliases = as_list(fm.get("aliases")) created = fm.get("created") or date.today().isoformat() updated = date.today().isoformat() lines = [ "---", f"title: {title}", f"status: {status}", f"type: {note_type}", "tags:" + ("\n".join(yaml_list(tags)) if tags else " []"), f"created: {created}", f"updated: {updated}", "aliases:" + ("\n".join(yaml_list(aliases)) if aliases else " []"), ] for key, value in fm.items(): if key in {"title", "status", "type", "tags", "created", "updated", "aliases"}: continue if isinstance(value, list): lines.append(f"{key}:") lines.extend(f" - {item}" for item in value) elif value in (None, ""): lines.append(f"{key}:") else: lines.append(f"{key}: {value}") lines.extend(["---", "", body.lstrip("\n")]) path.write_text("\n".join(lines), encoding="utf-8") return {"path": rel(path), "written": True} def main() -> int: parser = argparse.ArgumentParser(description="Agent-facing frontmatter helper") sub = parser.add_subparsers(dest="command", required=True) check_cmd = sub.add_parser("check") check_cmd.add_argument("paths", nargs="*") normalize_cmd = sub.add_parser("normalize") normalize_cmd.add_argument("paths", nargs="+") args = parser.parse_args() if args.command == "check": issues = check(resolve_paths(args.paths)) json_print({"ok": True, "issues": issues, "total_with_issues": len(issues)}) elif args.command == "normalize": written = [normalize(path) for path in resolve_paths(args.paths)] json_print({"ok": True, "written": written}) return 0 if __name__ == "__main__": raise SystemExit(main())