chore: push all design docs, V2 plan specs, and current working state
Includes AirPlan design documents, AircOding-alpha1-plan, AirPlanV2, AirPlan-ParaV2, AirPlan-Para V1 reference docs, and all working code changes across packages. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
350
AirPlan/docs/spec/AirPlan-ParaV2/lib/air_runtime/doc_sync.py
Executable file
350
AirPlan/docs/spec/AirPlan-ParaV2/lib/air_runtime/doc_sync.py
Executable file
@@ -0,0 +1,350 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Dict, Iterable, List
|
||||
|
||||
from .contracts import DocumentUpdate, ValidationRecord, WorkerResult, now_iso
|
||||
from .paths import (
|
||||
agents_path,
|
||||
architecture_c4_module_path,
|
||||
plan_path as workflow_plan_path,
|
||||
todo_path as workflow_todo_path,
|
||||
)
|
||||
from .todo_parser import parse_tasks
|
||||
|
||||
|
||||
def _project_relative(project_root: Path, raw_path: str) -> Path:
|
||||
candidate = Path(raw_path)
|
||||
if candidate.is_absolute():
|
||||
resolved = candidate.resolve()
|
||||
else:
|
||||
resolved = (project_root / candidate).resolve()
|
||||
|
||||
project_root_resolved = project_root.resolve()
|
||||
try:
|
||||
resolved.relative_to(project_root_resolved)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"document update path escapes project root: {raw_path}") from exc
|
||||
return resolved
|
||||
|
||||
|
||||
def _replace_marker_block(text: str, marker: str, content: str) -> str:
|
||||
begin = f"<!-- AIR-ENGINE:{marker}:BEGIN -->"
|
||||
end = f"<!-- AIR-ENGINE:{marker}:END -->"
|
||||
block = f"{begin}\n{content.rstrip()}\n{end}\n"
|
||||
start_index = text.find(begin)
|
||||
end_index = text.find(end)
|
||||
if start_index >= 0 and end_index > start_index:
|
||||
end_index += len(end)
|
||||
return text[:start_index].rstrip() + "\n\n" + block + text[end_index:].lstrip()
|
||||
return text.rstrip() + "\n\n" + block
|
||||
|
||||
|
||||
def _extract_marker_block(text: str, marker: str) -> str:
|
||||
begin = f"<!-- AIR-ENGINE:{marker}:BEGIN -->"
|
||||
end = f"<!-- AIR-ENGINE:{marker}:END -->"
|
||||
start_index = text.find(begin)
|
||||
end_index = text.find(end)
|
||||
if start_index < 0 or end_index <= start_index:
|
||||
return ""
|
||||
content_start = start_index + len(begin)
|
||||
return text[content_start:end_index].strip()
|
||||
|
||||
|
||||
def _apply_document_update(project_root: Path, update: DocumentUpdate) -> str:
|
||||
target_path = _project_relative(project_root, update.path)
|
||||
target_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
existing = target_path.read_text(encoding="utf-8") if target_path.exists() else ""
|
||||
|
||||
if update.action == "create_file":
|
||||
target_path.write_text(update.content.rstrip() + "\n", encoding="utf-8")
|
||||
elif update.action == "append_lines":
|
||||
joined = "\n".join(update.append_lines).rstrip()
|
||||
if existing.strip():
|
||||
target_path.write_text(existing.rstrip() + "\n" + joined + "\n", encoding="utf-8")
|
||||
else:
|
||||
target_path.write_text(joined + "\n", encoding="utf-8")
|
||||
elif update.action == "replace_block":
|
||||
updated = _replace_marker_block(existing, update.marker, update.content)
|
||||
target_path.write_text(updated, encoding="utf-8")
|
||||
else:
|
||||
raise ValueError(f"unsupported document update action: {update.action}")
|
||||
|
||||
return str(target_path)
|
||||
|
||||
|
||||
def apply_document_updates(project_root: Path, result: WorkerResult) -> List[str]:
|
||||
applied: List[str] = []
|
||||
for update in result.document_updates:
|
||||
applied.append(_apply_document_update(project_root, update))
|
||||
return applied
|
||||
|
||||
|
||||
def _validation_summary(validations: List[ValidationRecord]) -> str:
|
||||
if not validations:
|
||||
return "No explicit validation recorded"
|
||||
return "; ".join(
|
||||
f"{item.kind}:{item.status}" + (f" ({item.command})" if item.command else "")
|
||||
for item in validations
|
||||
)
|
||||
|
||||
|
||||
def _debug_summary(result: WorkerResult) -> str:
|
||||
if not result.debug_sessions:
|
||||
return "none"
|
||||
return ", ".join(
|
||||
f"{item.session_id} [{item.trigger}]"
|
||||
for item in result.debug_sessions
|
||||
)
|
||||
|
||||
|
||||
def _xdb_summary(result: WorkerResult) -> str:
|
||||
if not result.xdb_sessions:
|
||||
return "none"
|
||||
return ", ".join(
|
||||
f"{item.session_id} [{item.mode}:{item.status}]"
|
||||
for item in result.xdb_sessions
|
||||
)
|
||||
|
||||
|
||||
def _repair_summary(result: WorkerResult) -> str:
|
||||
if not result.repair_attempts:
|
||||
return "none"
|
||||
return ", ".join(
|
||||
f"{item.repair_id} [{item.status}]"
|
||||
for item in result.repair_attempts
|
||||
)
|
||||
|
||||
|
||||
def _todo_status_from_worker_status(worker_status: str) -> str:
|
||||
if worker_status == "done":
|
||||
return "DONE"
|
||||
return "BLOCKED"
|
||||
|
||||
|
||||
def _todo_status_from_result(result: WorkerResult) -> str:
|
||||
if result.status == "done":
|
||||
return "DONE"
|
||||
if result.repair_attempts:
|
||||
return "DOING"
|
||||
return "BLOCKED"
|
||||
|
||||
|
||||
def _rebuild_table_row(cells: List[str]) -> str:
|
||||
return "| " + " | ".join(cells) + " |"
|
||||
|
||||
|
||||
def update_todo_after_merge(
|
||||
project_root: Path,
|
||||
result: WorkerResult,
|
||||
applied_doc_paths: List[str],
|
||||
sync_paths: List[str],
|
||||
) -> Path:
|
||||
todo_path = workflow_todo_path(project_root)
|
||||
original_text = todo_path.read_text(encoding="utf-8")
|
||||
lines = original_text.splitlines()
|
||||
updated_lines: List[str] = []
|
||||
task_found = False
|
||||
|
||||
unique_applied_doc_paths = list(dict.fromkeys(applied_doc_paths))
|
||||
unique_sync_paths = list(dict.fromkeys(sync_paths))
|
||||
for raw_line in lines:
|
||||
if raw_line.startswith(f"| {result.task_id} "):
|
||||
cells = [cell.strip() for cell in raw_line.strip().strip("|").split("|")]
|
||||
if len(cells) > 8:
|
||||
cells = cells[:7] + ["; ".join(cell for cell in cells[7:] if cell)]
|
||||
if len(cells) >= 8:
|
||||
cells[1] = _todo_status_from_result(result)
|
||||
validation_cell = _validation_summary(result.validations)
|
||||
if result.evidence_paths:
|
||||
validation_cell += f"; evidence={len(result.evidence_paths)}"
|
||||
if result.blockers:
|
||||
validation_cell += "; blockers=" + ", ".join(result.blockers)
|
||||
if result.xdb_sessions:
|
||||
validation_cell += f"; xdb={len(result.xdb_sessions)}"
|
||||
if result.debug_sessions:
|
||||
validation_cell += f"; debug={len(result.debug_sessions)}"
|
||||
if result.repair_attempts:
|
||||
validation_cell += f"; repair={len(result.repair_attempts)}"
|
||||
cells[6] = validation_cell
|
||||
if unique_applied_doc_paths:
|
||||
cells[7] = "Merged by engine: " + ", ".join(
|
||||
Path(path).name for path in unique_applied_doc_paths
|
||||
)
|
||||
updated_lines.append(_rebuild_table_row(cells))
|
||||
task_found = True
|
||||
continue
|
||||
updated_lines.append(raw_line)
|
||||
|
||||
if not task_found:
|
||||
raise ValueError(f"task row not found in todo.md for {result.task_id}")
|
||||
|
||||
log_lines = [
|
||||
f"- `{now_iso()}` task `{result.task_id}` merged with status `{result.status}`",
|
||||
f" Summary: {result.summary}",
|
||||
f" Files Changed: {', '.join(f'`{item}`' for item in result.files_changed) or '`none`'}",
|
||||
f" Validations: {_validation_summary(result.validations)}",
|
||||
f" Evidence: {', '.join(f'`{item}`' for item in result.evidence_paths) or '`none`'}",
|
||||
f" XDB Sessions: {_xdb_summary(result)}",
|
||||
f" Debug Sessions: {_debug_summary(result)}",
|
||||
f" Repair Attempts: {_repair_summary(result)}",
|
||||
f" Risks: {', '.join(result.risks) or 'none'}",
|
||||
f" Blockers: {', '.join(result.blockers) or 'none'}",
|
||||
f" Applied Doc Paths: {', '.join(f'`{item}`' for item in unique_applied_doc_paths) or '`none`'}",
|
||||
f" Engine Sync Paths: {', '.join(f'`{item}`' for item in unique_sync_paths) or '`none`'}",
|
||||
]
|
||||
existing_log = _extract_marker_block(original_text, "TODO-RUN-LOG")
|
||||
existing_body = ""
|
||||
if existing_log:
|
||||
existing_lines = existing_log.splitlines()
|
||||
if existing_lines and existing_lines[0].strip() in {"# Air Engine Merge Log", "# AirEng Merge Log"}:
|
||||
existing_body = "\n".join(existing_lines[1:]).strip()
|
||||
else:
|
||||
existing_body = existing_log.strip()
|
||||
|
||||
merged_log_content = "# AirEng Merge Log\n\n" + "\n".join(log_lines)
|
||||
if existing_body:
|
||||
merged_log_content += "\n\n" + existing_body
|
||||
|
||||
text = "\n".join(updated_lines).rstrip() + "\n"
|
||||
text = _replace_marker_block(
|
||||
text,
|
||||
"TODO-RUN-LOG",
|
||||
merged_log_content,
|
||||
)
|
||||
todo_path.write_text(text, encoding="utf-8")
|
||||
return todo_path
|
||||
|
||||
|
||||
def mark_tasks_dispatched(
|
||||
project_root: Path,
|
||||
group_name: str,
|
||||
dispatched_tasks: List[Dict[str, object]],
|
||||
recommended_concurrency: int,
|
||||
) -> List[str]:
|
||||
updated_paths: List[str] = []
|
||||
current_todo_path = workflow_todo_path(project_root)
|
||||
current_plan_path = workflow_plan_path(project_root)
|
||||
task_ids = {str(item.get("taskId", "")).strip() for item in dispatched_tasks if str(item.get("taskId", "")).strip()}
|
||||
|
||||
if current_todo_path.exists() and task_ids:
|
||||
lines = current_todo_path.read_text(encoding="utf-8").splitlines()
|
||||
rewritten: List[str] = []
|
||||
for raw_line in lines:
|
||||
if raw_line.startswith("| "):
|
||||
cells = [cell.strip() for cell in raw_line.strip().strip("|").split("|")]
|
||||
if cells and cells[0] in task_ids and len(cells) >= 2:
|
||||
cells[1] = "DOING"
|
||||
raw_line = _rebuild_table_row(cells)
|
||||
rewritten.append(raw_line)
|
||||
current_todo_path.write_text("\n".join(rewritten).rstrip() + "\n", encoding="utf-8")
|
||||
updated_paths.append(str(current_todo_path))
|
||||
|
||||
if current_plan_path.exists():
|
||||
plan_text = current_plan_path.read_text(encoding="utf-8")
|
||||
lines = [
|
||||
"## AirEng Active Dispatch",
|
||||
"",
|
||||
f"- Updated At: `{now_iso()}`",
|
||||
f"- Group: `{group_name or 'default'}`",
|
||||
f"- Recommended Concurrency: `{recommended_concurrency}`",
|
||||
"- Tasks:",
|
||||
]
|
||||
if dispatched_tasks:
|
||||
for item in dispatched_tasks:
|
||||
lines.append(
|
||||
f" - `{item.get('taskId', '')}` handoff=`{item.get('handoffPath', '')}` brief=`{item.get('briefPath', '')}`"
|
||||
)
|
||||
else:
|
||||
lines.append(" - none")
|
||||
plan_text = _replace_marker_block(plan_text, "DISPATCH-STATUS", "\n".join(lines))
|
||||
current_plan_path.write_text(plan_text, encoding="utf-8")
|
||||
updated_paths.append(str(current_plan_path))
|
||||
|
||||
return updated_paths
|
||||
|
||||
|
||||
def _sync_block_content(result: WorkerResult, applied_doc_paths: List[str], title: str) -> str:
|
||||
unique_applied_doc_paths = list(dict.fromkeys(applied_doc_paths))
|
||||
lines = [
|
||||
title,
|
||||
"",
|
||||
f"- Last Synced At: `{now_iso()}`",
|
||||
f"- Task: `{result.task_id}`",
|
||||
f"- Status: `{result.status}`",
|
||||
f"- Summary: {result.summary}",
|
||||
f"- Files Changed: {', '.join(f'`{item}`' for item in result.files_changed) or '`none`'}",
|
||||
f"- Validations: {_validation_summary(result.validations)}",
|
||||
f"- XDB Sessions: {_xdb_summary(result)}",
|
||||
f"- Debug Sessions: {_debug_summary(result)}",
|
||||
f"- Repair Attempts: {_repair_summary(result)}",
|
||||
f"- Applied Doc Paths: {', '.join(f'`{item}`' for item in unique_applied_doc_paths) or '`none`'}",
|
||||
f"- Risks: {', '.join(result.risks) or 'none'}",
|
||||
f"- Blockers: {', '.join(result.blockers) or 'none'}",
|
||||
]
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
def sync_engine_managed_docs(project_root: Path, result: WorkerResult, applied_doc_paths: List[str]) -> List[str]:
|
||||
updated_paths: List[str] = []
|
||||
current_agents_path = agents_path(project_root)
|
||||
if current_agents_path.exists():
|
||||
agents_text = current_agents_path.read_text(encoding="utf-8")
|
||||
agents_text = _replace_marker_block(
|
||||
agents_text,
|
||||
"AGENTS-SYNC",
|
||||
_sync_block_content(result, applied_doc_paths, "## AirEng Sync"),
|
||||
)
|
||||
current_agents_path.write_text(agents_text, encoding="utf-8")
|
||||
updated_paths.append(str(current_agents_path))
|
||||
|
||||
c4_path = architecture_c4_module_path(project_root)
|
||||
if c4_path.exists():
|
||||
c4_text = c4_path.read_text(encoding="utf-8")
|
||||
c4_text = _replace_marker_block(
|
||||
c4_text,
|
||||
"C4-SYNC",
|
||||
_sync_block_content(result, applied_doc_paths, "## AirEng Sync"),
|
||||
)
|
||||
c4_path.write_text(c4_text, encoding="utf-8")
|
||||
updated_paths.append(str(c4_path))
|
||||
|
||||
return updated_paths
|
||||
|
||||
|
||||
def enforce_doc_sync_requirements(project_root: Path, result: WorkerResult) -> None:
|
||||
task = None
|
||||
todo_path = workflow_todo_path(project_root)
|
||||
if todo_path.exists():
|
||||
task = next((item for item in parse_tasks(todo_path) if item.task_id == result.task_id), None)
|
||||
|
||||
required_doc_paths = set(result.global_doc_paths)
|
||||
if task is not None:
|
||||
required_doc_paths.update(task.global_doc_paths)
|
||||
|
||||
if result.status != "done":
|
||||
return
|
||||
|
||||
if not required_doc_paths:
|
||||
return
|
||||
|
||||
covered_paths = set()
|
||||
for update in result.document_updates:
|
||||
covered_paths.add(update.path)
|
||||
|
||||
missing_paths = []
|
||||
for required in sorted(required_doc_paths):
|
||||
if required == "AirPlan/docs/architecture/adr/":
|
||||
if not any("AirPlan/docs/architecture/adr/" in path.replace("\\", "/") for path in covered_paths):
|
||||
missing_paths.append(required)
|
||||
continue
|
||||
if required not in covered_paths:
|
||||
missing_paths.append(required)
|
||||
|
||||
if missing_paths:
|
||||
raise ValueError(
|
||||
"document sync is incomplete for task "
|
||||
+ result.task_id
|
||||
+ ": missing updates for "
|
||||
+ ", ".join(missing_paths)
|
||||
)
|
||||
Reference in New Issue
Block a user