feat(ack): shard task records for merge-safe workspaces

This commit is contained in:
2026-09-20 19:15:43 +08:00
parent fd13dad2b0
commit 130dcc36c5
29 changed files with 985 additions and 218 deletions
+249
View File
@@ -0,0 +1,249 @@
#!/usr/bin/env python3
"""Merge-friendly ACK task records.
``tasks.yaml`` remains the small project manifest. When it contains a
``taskStore`` mapping, task state is materialized from immutable record files
under the configured directory. Legacy boards without that mapping keep
their original single-file behavior.
Records are complete task snapshots rather than patches. A later record
names its parent record(s), so a concurrent edit produces multiple heads and
is reported as a semantic merge conflict instead of being silently resolved
by file order or timestamps.
"""
from __future__ import annotations
import json
import re
from pathlib import Path
from typing import Any
from yaml_subset import (
DuplicateKeyError,
YamlSubsetError,
load_json_unique,
load_yaml_subset,
make_unique_pyyaml_loader,
)
STORE_VERSION = 1
GLOBAL_TASK_ID = "__project__"
TASK_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
RECORD_ID_RE = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,255}$")
RECORD_SUFFIXES = {".yaml", ".yml", ".json"}
class TaskStoreError(ValueError):
"""A malformed task store or record."""
def parse_document(path: Path) -> dict[str, Any]:
"""Parse one YAML/JSON mapping without importing the validator."""
try:
content = path.read_text(encoding="utf-8")
except OSError as exc:
raise TaskStoreError(f"读取失败: {path}: {exc}") from exc
try:
if path.suffix.lower() == ".json":
value = load_json_unique(content)
else:
try:
import yaml # type: ignore
except ImportError:
value = load_yaml_subset(content)
else:
value = yaml.load(
content,
Loader=make_unique_pyyaml_loader(yaml),
)
except (json.JSONDecodeError, DuplicateKeyError, YamlSubsetError) as exc:
raise TaskStoreError(f"解析失败: {path}: {exc}") from exc
except Exception as exc: # PyYAML errors are intentionally bounded.
raise TaskStoreError(f"解析失败: {path}") from exc
if not isinstance(value, dict):
raise TaskStoreError(f"{path}: 顶层必须是对象(mapping)")
return value
def _record_directory(manifest: dict[str, Any], manifest_path: Path) -> Path:
config = manifest.get("taskStore")
if not isinstance(config, dict):
raise TaskStoreError("taskStore 必须是对象")
version = config.get("version", STORE_VERSION)
if version != STORE_VERSION:
raise TaskStoreError(f"不支持 taskStore.version={version!r}")
directory = config.get("directory", "task-records")
if not isinstance(directory, str) or not directory.strip():
raise TaskStoreError("taskStore.directory 必须是非空相对路径")
relative = Path(directory)
if relative.is_absolute() or ".." in relative.parts:
raise TaskStoreError("taskStore.directory 必须位于任务板目录下")
return manifest_path.parent / relative
def _record_paths(directory: Path) -> list[Path]:
if not directory.exists():
return []
if directory.is_symlink() or not directory.is_dir():
raise TaskStoreError(f"taskStore.directory 不是普通目录: {directory}")
paths: list[Path] = []
for path in sorted(directory.iterdir(), key=lambda item: item.name):
if path.name.startswith("."):
continue
if path.is_symlink() or not path.is_file():
raise TaskStoreError(f"任务记录必须是普通文件: {path}")
if path.suffix.lower() in RECORD_SUFFIXES:
paths.append(path)
return paths
def _unique_collection(
records: list[dict[str, Any]],
field: str,
errors: list[str],
) -> list[dict[str, Any]]:
result: list[dict[str, Any]] = []
by_id: dict[str, dict[str, Any]] = {}
for record in records:
values = record.get(field)
if values is None:
continue
if not isinstance(values, list):
errors.append(f"{field} 必须是列表")
continue
for index, item in enumerate(values):
if not isinstance(item, dict):
errors.append(f"{field}[{index}] 必须是对象")
continue
item_id = item.get("id")
if not isinstance(item_id, str) or not item_id:
errors.append(f"{field}[{index}].id 必须是非空字符串")
continue
previous = by_id.get(item_id)
if previous is not None and previous != item:
errors.append(f"{field} 出现相同 id 的不同记录: {item_id}")
continue
if previous is None:
by_id[item_id] = item
result.append(item)
return result
def materialize_board(manifest: dict[str, Any], manifest_path: Path) -> dict[str, Any]:
"""Return a board-shaped view from a manifest and immutable records."""
directory = _record_directory(manifest, manifest_path)
paths = _record_paths(directory)
# This permits a staged rollout: a freshly initialized board has an empty
# store and the legacy empty ``tasks: []`` field until its first record.
if not paths:
return manifest
errors: list[str] = []
records: list[dict[str, Any]] = []
by_record_id: dict[str, dict[str, Any]] = {}
for path in paths:
record = parse_document(path)
where = str(path.relative_to(manifest_path.parent))
version = record.get("recordVersion", STORE_VERSION)
record_id = record.get("recordId")
task_id = record.get("taskId")
parents = record.get("parents", [])
if version != STORE_VERSION:
errors.append(f"{where}: 不支持 recordVersion={version!r}")
if not isinstance(record_id, str) or RECORD_ID_RE.fullmatch(record_id) is None:
errors.append(f"{where}: recordId 格式无效")
elif record_id in by_record_id:
errors.append(f"{where}: recordId 重复: {record_id}")
if not isinstance(task_id, str) or (
task_id != GLOBAL_TASK_ID and TASK_ID_RE.fullmatch(task_id) is None
):
errors.append(f"{where}: taskId 格式无效")
if not isinstance(parents, list) or any(
not isinstance(parent, str) or RECORD_ID_RE.fullmatch(parent) is None
for parent in parents
):
errors.append(f"{where}: parents 必须是 recordId 字符串列表")
task = record.get("task")
if task_id != GLOBAL_TASK_ID and not isinstance(task, dict):
errors.append(f"{where}: 非全局记录必须包含 task 对象")
if isinstance(record_id, str) and record_id not in by_record_id:
by_record_id[record_id] = record
records.append(record)
for record in records:
record_id = record.get("recordId")
task_id = record.get("taskId")
if not isinstance(record_id, str) or not isinstance(task_id, str):
continue
for parent in record.get("parents", []):
if parent not in by_record_id:
errors.append(f"{record_id}: 引用了不存在的 parent: {parent}")
elif by_record_id[parent].get("taskId") != task_id:
errors.append(f"{record_id}: parent 必须属于同一个 taskId")
task_records: dict[str, list[dict[str, Any]]] = {}
for record in records:
task_id = record.get("taskId")
if isinstance(task_id, str) and task_id != GLOBAL_TASK_ID:
task_records.setdefault(task_id, []).append(record)
tasks: list[dict[str, Any]] = []
for task_id in sorted(task_records):
items = task_records[task_id]
referenced = {
parent
for item in items
for parent in item.get("parents", [])
if isinstance(parent, str)
}
heads = [
item for item in items
if isinstance(item.get("recordId"), str)
and item["recordId"] not in referenced
]
if len(heads) != 1:
errors.append(
f"task {task_id}: 发现 {len(heads)} 个未合并 head,必须创建同时引用全部 head 的合并记录"
)
continue
task = heads[0].get("task")
if isinstance(task, dict):
tasks.append(task)
result = dict(manifest)
result["tasks"] = tasks
result["workerReceipts"] = _unique_collection(records, "workerReceipts", errors)
for field in ("deliveryRuns", "regressionRuns"):
if field in manifest or any(field in record for record in records):
result[field] = _unique_collection(records, field, errors)
result["summary"] = {
"open": [task["id"] for task in tasks if task.get("status") == "open"],
"verified": [task["id"] for task in tasks if task.get("status") == "verified"],
"failedRetest": [task["id"] for task in tasks if task.get("status") == "failed_retest"],
"leftovers": [task["id"] for task in tasks if task.get("status") == "leftover"],
}
if errors:
result["taskStoreErrors"] = errors
else:
result.pop("taskStoreErrors", None)
return result
def is_task_store_manifest(value: object) -> bool:
return isinstance(value, dict) and "taskStore" in value
__all__ = [
"GLOBAL_TASK_ID",
"STORE_VERSION",
"TaskStoreError",
"is_task_store_manifest",
"materialize_board",
"parse_document",
]