22eda53d79
Affected files: .obsidian/graph.json 99 System/INDEX.md 99 System/Tools/agent-tools/AGENT_TOOLS.md 99 System/Tools/agent-tools/vault_audit.py 99 System/Tools/agent-tools/vault_common.py 99 System/Tools/agent-tools/vault_frontmatter.py 99 System/Tools/agent-tools/vault_index.py 99 System/Tools/agent-tools/vault_ingest.py AGENTS.md
136 lines
3.9 KiB
Python
136 lines
3.9 KiB
Python
#!/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())
|