feat: add fast forge work-record reconciliation
Assistant: codex Assistant-Model: gpt-5.6-sol Assistant-Session: 01a053ff-1d6f-7fe2-ac1c-a6eb40a42a0c
This commit is contained in:
parent
a65cef02cf
commit
34f5cb3fc3
22 changed files with 799 additions and 162 deletions
|
|
@ -125,6 +125,10 @@ class DerivedTask:
|
|||
title: str | None
|
||||
status: str | None
|
||||
priority: str | None
|
||||
description: str | None = None
|
||||
needs_human: bool = False
|
||||
intervention_note: str | None = None
|
||||
blocking_reason: str | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
@ -135,6 +139,8 @@ class DerivedWorkplan:
|
|||
status: str | None
|
||||
relative_path: str
|
||||
archived: bool
|
||||
owner: str | None = None
|
||||
description: str | None = None
|
||||
tasks: list[DerivedTask] = field(default_factory=list)
|
||||
|
||||
|
||||
|
|
@ -180,6 +186,8 @@ class DerivedProjection:
|
|||
"uuid": w.uuid,
|
||||
"title": w.title,
|
||||
"status": w.status,
|
||||
"owner": w.owner,
|
||||
"description": w.description,
|
||||
"relative_path": w.relative_path,
|
||||
"archived": w.archived,
|
||||
"tasks": [
|
||||
|
|
@ -189,6 +197,10 @@ class DerivedProjection:
|
|||
"title": t.title,
|
||||
"status": t.status,
|
||||
"priority": t.priority,
|
||||
"description": t.description,
|
||||
"needs_human": t.needs_human,
|
||||
"intervention_note": t.intervention_note,
|
||||
"blocking_reason": t.blocking_reason,
|
||||
}
|
||||
for t in w.tasks
|
||||
],
|
||||
|
|
@ -279,6 +291,11 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
|
|||
if not title:
|
||||
prev = [t for pos, t in headings if pos < m.start()]
|
||||
title = prev[-1] if prev else None
|
||||
following_headings = [pos for pos, _title in headings if pos > m.end()]
|
||||
description_end = min(following_headings) if following_headings else len(body)
|
||||
description = str(block.get("description") or "").strip()
|
||||
if not description:
|
||||
description = body[m.end() : description_end].strip()
|
||||
out.append(
|
||||
DerivedTask(
|
||||
# A bare `T01` is not an identifier: it is unique only within
|
||||
|
|
@ -298,11 +315,36 @@ def _parse_tasks(body: str, workplan_id: str) -> list[DerivedTask]:
|
|||
title=title,
|
||||
status=(str(block["status"]).strip() if block.get("status") else None),
|
||||
priority=(str(block["priority"]).strip() if block.get("priority") else None),
|
||||
description=description or None,
|
||||
needs_human=bool(block.get("needs_human", False)),
|
||||
intervention_note=(
|
||||
str(block["intervention_note"]).strip()
|
||||
if block.get("intervention_note")
|
||||
else None
|
||||
),
|
||||
blocking_reason=(
|
||||
str(block["blocking_reason"]).strip()
|
||||
if block.get("blocking_reason")
|
||||
else None
|
||||
),
|
||||
)
|
||||
)
|
||||
return out
|
||||
|
||||
|
||||
def _workplan_description(body: str) -> str | None:
|
||||
"""Return bounded prose under ``## Goal`` when one is present."""
|
||||
match = re.search(r"^##\s+Goal\s*$", body, re.MULTILINE | re.IGNORECASE)
|
||||
if match is None:
|
||||
return None
|
||||
remainder = body[match.end() :]
|
||||
next_heading = re.search(r"^##\s+", remainder, re.MULTILINE)
|
||||
if next_heading is not None:
|
||||
remainder = remainder[: next_heading.start()]
|
||||
value = remainder.strip()
|
||||
return value[:4000] or None
|
||||
|
||||
|
||||
def derive_from_checkout(repo_root: Path, repo_slug: str, commit: str) -> DerivedProjection:
|
||||
"""Derive a projection from an already-materialised checkout."""
|
||||
proj = DerivedProjection(repo_slug=repo_slug, commit=commit)
|
||||
|
|
@ -331,6 +373,12 @@ def derive_from_checkout(repo_root: Path, repo_slug: str, commit: str) -> Derive
|
|||
status=(str(meta["status"]).strip() if meta.get("status") else None),
|
||||
relative_path=str(path.relative_to(repo_root).as_posix()),
|
||||
archived=path.parent.name == "archived",
|
||||
owner=(str(meta["owner"]).strip() if meta.get("owner") else None),
|
||||
description=(
|
||||
str(meta["description"]).strip()
|
||||
if meta.get("description")
|
||||
else _workplan_description(body)
|
||||
),
|
||||
tasks=_parse_tasks(body, rid),
|
||||
)
|
||||
)
|
||||
|
|
@ -696,6 +744,10 @@ def _sync_existing_workplan_tasks(
|
|||
"workplan_id": row.id,
|
||||
"record_id": dt.record_id,
|
||||
"title": (dt.title or dt.record_id),
|
||||
"description": dt.description,
|
||||
"needs_human": dt.needs_human,
|
||||
"intervention_note": dt.intervention_note,
|
||||
"blocking_reason": dt.blocking_reason,
|
||||
}
|
||||
st = _coerce_task_status(dt.status)
|
||||
if st is not None:
|
||||
|
|
@ -715,10 +767,30 @@ def _sync_existing_workplan_tasks(
|
|||
if dt.title and dt.title.strip() and ht.title != dt.title.strip():
|
||||
ht.title = dt.title.strip()
|
||||
changed = True
|
||||
if getattr(ht, "description", None) != dt.description:
|
||||
ht.description = dt.description
|
||||
changed = True
|
||||
st = _coerce_task_status(dt.status)
|
||||
if st is not None and ht.status != st:
|
||||
ht.status = st
|
||||
changed = True
|
||||
if getattr(ht, "needs_human", False) != dt.needs_human:
|
||||
ht.needs_human = dt.needs_human
|
||||
changed = True
|
||||
if getattr(ht, "intervention_note", None) != dt.intervention_note:
|
||||
ht.intervention_note = dt.intervention_note
|
||||
changed = True
|
||||
if getattr(ht, "blocking_reason", None) != dt.blocking_reason:
|
||||
ht.blocking_reason = dt.blocking_reason
|
||||
changed = True
|
||||
if dt.priority:
|
||||
try:
|
||||
task_priority = TaskPriority(dt.priority.strip().lower())
|
||||
except ValueError:
|
||||
task_priority = None
|
||||
if task_priority is not None and getattr(ht, "priority", None) != task_priority:
|
||||
ht.priority = task_priority
|
||||
changed = True
|
||||
if changed:
|
||||
outcome.updated_tasks.append(dt.record_id)
|
||||
|
||||
|
|
@ -1054,7 +1126,9 @@ async def reset_repository_projection(
|
|||
topic_id=repo.topic_id,
|
||||
slug=w.record_id.lower(),
|
||||
title=w.title or w.record_id,
|
||||
description=w.description,
|
||||
status=w.status or "proposed",
|
||||
owner=w.owner,
|
||||
backing_filename=w.relative_path.rsplit("/", 1)[-1],
|
||||
backing_relative_path=w.relative_path,
|
||||
backing_archived=w.archived,
|
||||
|
|
@ -1071,8 +1145,12 @@ async def reset_repository_projection(
|
|||
workplan_id=row.id,
|
||||
record_id=t.record_id,
|
||||
title=t.title or t.record_id,
|
||||
description=t.description,
|
||||
status=t.status or "todo",
|
||||
priority=t.priority or "medium",
|
||||
needs_human=t.needs_human,
|
||||
intervention_note=t.intervention_note,
|
||||
blocking_reason=t.blocking_reason,
|
||||
)
|
||||
)
|
||||
outcome.created.append(w.record_id)
|
||||
|
|
@ -1090,6 +1168,12 @@ async def reset_repository_projection(
|
|||
if w.title and w.title.strip() and row.title != w.title.strip():
|
||||
row.title = w.title.strip()
|
||||
changed = True
|
||||
if w.description and getattr(row, "description", None) != w.description:
|
||||
row.description = w.description
|
||||
changed = True
|
||||
if getattr(row, "owner", None) != w.owner:
|
||||
row.owner = w.owner
|
||||
changed = True
|
||||
if w.status and row.status != w.status:
|
||||
row.status = w.status
|
||||
changed = True
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue