"""Build repository observation + work-record index from files.""" from __future__ import annotations import re from pathlib import Path import yaml from repo_manager.cache import source_fingerprint from repo_manager.classification import require_valid_classification from repo_manager.gitops import head_sha, is_git_repo from repo_manager.index_store import RepoIndex, WorkRecordEntry, _now from repo_manager.parse.record import iter_record_files, parse_record_file from repo_manager.parse.register import iter_register_files, parse_register_file from repo_manager.parse.workplan import iter_workplan_files, parse_workplan_file def _slug_from_path(repo_root: Path) -> str: return re.sub(r"[^a-z0-9]+", "-", repo_root.name.lower()).strip("-") or "repo" def load_classification(repo_root: Path) -> dict | None: path = repo_root / ".repo-classification.yaml" if not path.is_file(): return None data = yaml.safe_load(path.read_text(encoding="utf-8")) or {} if isinstance(data, dict) and "repo_classification" in data: classification = data["repo_classification"] else: classification = data if isinstance(data, dict) else None if not isinstance(classification, dict): return None return require_valid_classification(classification) def observe_repository(repo_root: Path, *, slug: str | None = None) -> tuple[dict, RepoIndex]: """Return (RepositorySnapshot-like dict, rebuilt RepoIndex).""" repo_root = repo_root.resolve() slug = slug or _slug_from_path(repo_root) classification = load_classification(repo_root) intent_path = None description = None for name in ("INTENT.md", "GOAL.md"): p = repo_root / name if p.is_file(): intent_path = name # first non-empty line of body as weak description text = p.read_text(encoding="utf-8") for line in text.splitlines(): line = line.strip() if line and not line.startswith("#") and not line.startswith("---") and not line.startswith(">"): description = line[:200] break break records: list[WorkRecordEntry] = [] for path in iter_workplan_files(repo_root): wp = parse_workplan_file(path, repo_root=repo_root) records.append( WorkRecordEntry( kind="workplan", id=wp.id, status=wp.status, title=wp.title, source_path=wp.path, uuid=wp.state_hub_workstream_id, extra={ key: wp.frontmatter[key] for key in ("depends_on", "related", "needs_human", "intervention_note") if key in wp.frontmatter }, ) ) for task in wp.tasks: records.append( WorkRecordEntry( kind="task", id=task.id, status=task.status, title=task.title, source_path=wp.path, uuid=task.state_hub_task_id, parent_id=wp.id, extra={ key: task.raw[key] for key in ("depends_on", "needs_human", "intervention_note", "blocking_reason") if key in task.raw }, ) ) for path in iter_record_files(repo_root): for record in parse_record_file(path, repo_root=repo_root): records.append( WorkRecordEntry( kind=record.kind, id=record.id, status=record.status, title=record.title, source_path=record.source_path, uuid=record.uuid, extra={"record": record.raw}, ) ) for path in iter_register_files(repo_root): for entry in parse_register_file(path, repo_root=repo_root): records.append( WorkRecordEntry( kind=f"register:{entry.kind}", id=entry.id, status=entry.status, title=entry.title, source_path=entry.source_path, extra={"register_kind": entry.kind, "entry": entry.raw}, ) ) sha = head_sha(repo_root) if is_git_repo(repo_root) else None fingerprint, source_files = source_fingerprint(repo_root) index = RepoIndex( slug=slug, repo_root=str(repo_root), head_sha=sha, observed_at=_now(), source_fingerprint=fingerprint, source_files=source_files, work_records=records, ) snapshot = { "api_version": "0.1", "slug": slug, "lifecycle": "active", "domain": (classification or {}).get("domain"), "classification": classification, "purpose": { "description": description, "intent_path": intent_path, }, "locations": { "repo_root": str(repo_root), }, "revision": { "head_sha": sha, "observed_at": index.observed_at, }, "index": { "workplan_count": sum(1 for r in records if r.kind == "workplan"), "task_count": sum(1 for r in records if r.kind == "task"), "register_entry_count": sum(1 for r in records if r.kind.startswith("register:")), "intake_count": sum(1 for r in records if r.kind == "intake"), "decision_count": sum(1 for r in records if r.kind == "decision"), "record_count": len(records), }, } return snapshot, index