Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a023c0-a0a3-7c03-b395-5a0d2757214d
158 lines
5.7 KiB
Python
158 lines
5.7 KiB
Python
"""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
|