easy-coding-harness 0.10.0-beta.4 → 0.10.0-beta.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -0
- package/README.md +20 -12
- package/package.json +1 -1
- package/templates/common/bundled-skills/ec-init/SKILL.md +3 -1
- package/templates/common/bundled-skills/ec-meta/references/local-architecture/README.md +15 -7
- package/templates/common/skills/ec-analysis/SKILL.md +37 -8
- package/templates/common/skills/ec-git/SKILL.md +7 -1
- package/templates/common/skills/ec-implementing/SKILL.md +16 -1
- package/templates/common/skills/ec-memory/SKILL.md +65 -3
- package/templates/common/skills/ec-reviewing/SKILL.md +6 -0
- package/templates/common/skills/ec-task-close/SKILL.md +4 -0
- package/templates/common/skills/ec-task-management/SKILL.md +7 -1
- package/templates/common/skills/ec-verification/SKILL.md +11 -1
- package/templates/common/skills/ec-workflow/SKILL.md +45 -15
- package/templates/main-constraint/AGENTS.md.tpl +12 -0
- package/templates/main-constraint/CLAUDE.md.tpl +12 -0
- package/templates/runtime/templates/dev-spec-skeleton.md +3 -1
- package/templates/shared-hooks/easy_coding_state.py +1997 -113
- package/templates/shared-hooks/easy_dev_spec.py +444 -30
- package/templates/shared-hooks/easy_dev_spec_execution.py +1014 -0
- package/templates/shared-hooks/easy_dev_spec_protocol.py +1426 -18
|
@@ -16,8 +16,11 @@ from typing import Any, Iterable
|
|
|
16
16
|
|
|
17
17
|
|
|
18
18
|
SCHEMA = "easy-dev-spec/v1"
|
|
19
|
+
EXECUTION_SCHEMA = "easy-dev-spec-execution/v1"
|
|
19
20
|
MANIFEST_BEGIN = "<!-- EDS:MANIFEST:BEGIN -->"
|
|
20
21
|
MANIFEST_END = "<!-- EDS:MANIFEST:END -->"
|
|
22
|
+
EXECUTION_BEGIN = "<!-- EDS:EXECUTION:BEGIN -->"
|
|
23
|
+
EXECUTION_END = "<!-- EDS:EXECUTION:END -->"
|
|
21
24
|
SECTION_BEGIN_RE = re.compile(r"^<!-- EDS:SECTION:BEGIN id=([a-z0-9][a-z0-9-]*) -->$")
|
|
22
25
|
SECTION_END_RE = re.compile(r"^<!-- EDS:SECTION:END id=([a-z0-9][a-z0-9-]*) -->$")
|
|
23
26
|
ID_PATTERNS = {
|
|
@@ -31,6 +34,32 @@ ID_PATTERNS = {
|
|
|
31
34
|
VALID_STATUSES = {"DRAFT", "BLOCKED", "READY"}
|
|
32
35
|
VALID_DEPENDENCY_TYPES = {"hard", "contract", "integration"}
|
|
33
36
|
VALID_ACTIONS = {"add", "modify", "delete"}
|
|
37
|
+
VALID_EXECUTION_TASK_STATUSES = {
|
|
38
|
+
"not_started",
|
|
39
|
+
"in_progress",
|
|
40
|
+
"blocked",
|
|
41
|
+
"implemented",
|
|
42
|
+
"verified",
|
|
43
|
+
"completed",
|
|
44
|
+
"cancelled",
|
|
45
|
+
}
|
|
46
|
+
VALID_EXECUTION_DEPENDENCY_STATUSES = {"pending", "satisfied"}
|
|
47
|
+
VALID_EXECUTION_EVIDENCE_STATUSES = {"passed", "failed", "recorded"}
|
|
48
|
+
VALID_EXECUTION_EVENT_TYPES = {
|
|
49
|
+
"task_status_changed",
|
|
50
|
+
"step_status_changed",
|
|
51
|
+
"dependency_status_changed",
|
|
52
|
+
"spec_revised",
|
|
53
|
+
}
|
|
54
|
+
VALID_EXECUTION_TASK_TRANSITIONS = {
|
|
55
|
+
"not_started": {"in_progress", "cancelled"},
|
|
56
|
+
"in_progress": {"in_progress", "blocked", "implemented", "cancelled"},
|
|
57
|
+
"blocked": {"blocked", "in_progress", "cancelled"},
|
|
58
|
+
"implemented": {"in_progress", "blocked", "verified"},
|
|
59
|
+
"verified": {"in_progress", "blocked", "completed"},
|
|
60
|
+
"completed": {"in_progress"},
|
|
61
|
+
"cancelled": {"in_progress"},
|
|
62
|
+
}
|
|
34
63
|
REQUIRED_TASK_HEADINGS = (
|
|
35
64
|
"目标、交付物与非目标",
|
|
36
65
|
"文件与符号级改动",
|
|
@@ -54,6 +83,13 @@ FORBIDDEN_READY_PATTERNS = {
|
|
|
54
83
|
r"实施时检查|复用现有机制|在合适位置|新增相关组件|以目标分支为准|接入所有\s*Ability"
|
|
55
84
|
),
|
|
56
85
|
}
|
|
86
|
+
BLOCKED_EVIDENCE_FIELDS = (
|
|
87
|
+
"已问问题",
|
|
88
|
+
"用户原始回答",
|
|
89
|
+
"落入 Spec 的结论与影响范围",
|
|
90
|
+
"剩余阻塞",
|
|
91
|
+
"解除条件与责任方",
|
|
92
|
+
)
|
|
57
93
|
|
|
58
94
|
|
|
59
95
|
class CanonicalSpecError(ValueError):
|
|
@@ -89,6 +125,9 @@ class ValidationReport:
|
|
|
89
125
|
warnings: list[ValidationIssue] = field(default_factory=list)
|
|
90
126
|
manifest: dict[str, Any] | None = None
|
|
91
127
|
sections: dict[str, Section] = field(default_factory=dict)
|
|
128
|
+
execution: dict[str, Any] | None = None
|
|
129
|
+
design_sha256: str | None = None
|
|
130
|
+
document_sha256: str | None = None
|
|
92
131
|
|
|
93
132
|
@property
|
|
94
133
|
def ok(self) -> bool:
|
|
@@ -99,6 +138,11 @@ class ValidationReport:
|
|
|
99
138
|
"protocol": self.protocol,
|
|
100
139
|
"status": self.status,
|
|
101
140
|
"ok": self.ok,
|
|
141
|
+
"design_sha256": self.design_sha256,
|
|
142
|
+
"document_sha256": self.document_sha256,
|
|
143
|
+
"execution_revision": (
|
|
144
|
+
self.execution.get("execution_revision") if self.execution else None
|
|
145
|
+
),
|
|
102
146
|
"issues": [issue.to_dict() for issue in self.issues],
|
|
103
147
|
"warnings": [warning.to_dict() for warning in self.warnings],
|
|
104
148
|
}
|
|
@@ -147,6 +191,51 @@ def parse_manifest(text: str) -> dict[str, Any] | None:
|
|
|
147
191
|
return manifest
|
|
148
192
|
|
|
149
193
|
|
|
194
|
+
def split_execution_region(text: str) -> tuple[str, dict[str, Any] | None]:
|
|
195
|
+
"""Return design-only text and the optional shared execution ledger.
|
|
196
|
+
|
|
197
|
+
The execution block is deliberately excluded from the design digest. Runtime
|
|
198
|
+
progress can therefore change without invalidating a previously selected
|
|
199
|
+
design scope.
|
|
200
|
+
"""
|
|
201
|
+
|
|
202
|
+
begin_count = text.count(EXECUTION_BEGIN)
|
|
203
|
+
end_count = text.count(EXECUTION_END)
|
|
204
|
+
if begin_count == 0 and end_count == 0:
|
|
205
|
+
design_text = text.rstrip() + "\n"
|
|
206
|
+
return design_text, None
|
|
207
|
+
if begin_count != 1 or end_count != 1:
|
|
208
|
+
raise CanonicalSpecError("文档必须包含零组或一组 execution 边界")
|
|
209
|
+
begin = text.find(EXECUTION_BEGIN)
|
|
210
|
+
end = text.find(EXECUTION_END)
|
|
211
|
+
if end < begin:
|
|
212
|
+
raise CanonicalSpecError("execution 结束标记位于开始标记之前")
|
|
213
|
+
trailing = text[end + len(EXECUTION_END) :]
|
|
214
|
+
if trailing.strip():
|
|
215
|
+
raise CanonicalSpecError("execution 区域必须是文档最后一个非空区域")
|
|
216
|
+
inner = text[begin + len(EXECUTION_BEGIN) : end]
|
|
217
|
+
fenced = re.fullmatch(r"\s*```json\s*\n(.*?)\n```\s*", inner, re.DOTALL)
|
|
218
|
+
if not fenced:
|
|
219
|
+
raise CanonicalSpecError("execution 必须是边界内唯一的 ```json 代码块")
|
|
220
|
+
try:
|
|
221
|
+
execution = json.loads(fenced.group(1))
|
|
222
|
+
except json.JSONDecodeError as exc:
|
|
223
|
+
raise CanonicalSpecError(
|
|
224
|
+
f"execution 不是合法 JSON:第 {exc.lineno} 行第 {exc.colno} 列 {exc.msg}"
|
|
225
|
+
) from exc
|
|
226
|
+
if not isinstance(execution, dict):
|
|
227
|
+
raise CanonicalSpecError("execution 顶层必须是 JSON object")
|
|
228
|
+
design_text = text[:begin].rstrip() + "\n"
|
|
229
|
+
return design_text, execution
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def design_sha256(text: str) -> str:
|
|
233
|
+
"""Hash only design-bearing content, excluding the execution ledger."""
|
|
234
|
+
|
|
235
|
+
design_text, _ = split_execution_region(text)
|
|
236
|
+
return hashlib.sha256(design_text.encode("utf-8")).hexdigest()
|
|
237
|
+
|
|
238
|
+
|
|
150
239
|
def _parse_section_objects(text: str) -> dict[str, Section]:
|
|
151
240
|
"""Parse section objects with line metadata."""
|
|
152
241
|
|
|
@@ -959,6 +1048,34 @@ def _validate_ready_manifest_values(
|
|
|
959
1048
|
)
|
|
960
1049
|
|
|
961
1050
|
|
|
1051
|
+
def _validate_blocked_evidence(
|
|
1052
|
+
section_objects: dict[str, Section],
|
|
1053
|
+
issues: list[ValidationIssue],
|
|
1054
|
+
) -> None:
|
|
1055
|
+
"""Require proof that BLOCKED was chosen only after a user answer."""
|
|
1056
|
+
|
|
1057
|
+
global_section = section_objects.get("global-context")
|
|
1058
|
+
if not global_section:
|
|
1059
|
+
return
|
|
1060
|
+
content = global_section.content
|
|
1061
|
+
if "用户判断与剩余阻塞" not in content:
|
|
1062
|
+
_issue(
|
|
1063
|
+
issues,
|
|
1064
|
+
"blocked.evidence",
|
|
1065
|
+
"BLOCKED 文档必须包含“用户判断与剩余阻塞”记录,证明已提问并收到回答",
|
|
1066
|
+
"global-context",
|
|
1067
|
+
)
|
|
1068
|
+
return
|
|
1069
|
+
for marker in BLOCKED_EVIDENCE_FIELDS:
|
|
1070
|
+
if not _has_labeled_value(content, marker):
|
|
1071
|
+
_issue(
|
|
1072
|
+
issues,
|
|
1073
|
+
"blocked.evidence",
|
|
1074
|
+
f"BLOCKED 文档缺少 {marker} 的具体值",
|
|
1075
|
+
"global-context",
|
|
1076
|
+
)
|
|
1077
|
+
|
|
1078
|
+
|
|
962
1079
|
def _validate_ready_non_task_sections(
|
|
963
1080
|
repo_by_id: dict[str, dict[str, Any]],
|
|
964
1081
|
contract_by_id: dict[str, dict[str, Any]],
|
|
@@ -1375,6 +1492,1176 @@ def _check_dag(
|
|
|
1375
1492
|
visit(node)
|
|
1376
1493
|
|
|
1377
1494
|
|
|
1495
|
+
def _validate_execution_evidence(
|
|
1496
|
+
values: Any,
|
|
1497
|
+
issues: list[ValidationIssue],
|
|
1498
|
+
item_id: str,
|
|
1499
|
+
event_ids: set[str] | None = None,
|
|
1500
|
+
valid_test_ids: set[str] | None = None,
|
|
1501
|
+
) -> None:
|
|
1502
|
+
if not isinstance(values, list):
|
|
1503
|
+
_issue(issues, "execution.evidence", f"{item_id}.evidence 必须是数组", item_id)
|
|
1504
|
+
return
|
|
1505
|
+
for index, value in enumerate(values):
|
|
1506
|
+
label = f"{item_id}.evidence[{index}]"
|
|
1507
|
+
if not isinstance(value, dict):
|
|
1508
|
+
_issue(issues, "execution.evidence", f"{label} 必须是 object", item_id)
|
|
1509
|
+
continue
|
|
1510
|
+
for field_name in ("kind", "ref"):
|
|
1511
|
+
if not isinstance(value.get(field_name), str) or not value[field_name].strip():
|
|
1512
|
+
_issue(
|
|
1513
|
+
issues,
|
|
1514
|
+
"execution.evidence",
|
|
1515
|
+
f"{label}.{field_name} 必须是非空字符串",
|
|
1516
|
+
item_id,
|
|
1517
|
+
)
|
|
1518
|
+
if value.get("status") not in VALID_EXECUTION_EVIDENCE_STATUSES:
|
|
1519
|
+
_issue(
|
|
1520
|
+
issues,
|
|
1521
|
+
"execution.evidence",
|
|
1522
|
+
f"{label}.status 必须是 passed、failed 或 recorded",
|
|
1523
|
+
item_id,
|
|
1524
|
+
)
|
|
1525
|
+
evidence_event_id = value.get("event_id")
|
|
1526
|
+
if evidence_event_id is not None and (
|
|
1527
|
+
not isinstance(evidence_event_id, str)
|
|
1528
|
+
or (event_ids is not None and evidence_event_id not in event_ids)
|
|
1529
|
+
):
|
|
1530
|
+
_issue(
|
|
1531
|
+
issues,
|
|
1532
|
+
"execution.evidence",
|
|
1533
|
+
f"{label}.event_id 必须引用存在的执行事件",
|
|
1534
|
+
item_id,
|
|
1535
|
+
)
|
|
1536
|
+
if value.get("kind") == "test" and (
|
|
1537
|
+
not isinstance(value.get("test_id"), str) or not value["test_id"].strip()
|
|
1538
|
+
):
|
|
1539
|
+
_issue(
|
|
1540
|
+
issues,
|
|
1541
|
+
"execution.evidence",
|
|
1542
|
+
f"{label} 的 test 证据必须声明 test_id",
|
|
1543
|
+
item_id,
|
|
1544
|
+
)
|
|
1545
|
+
elif (
|
|
1546
|
+
value.get("kind") == "test"
|
|
1547
|
+
and valid_test_ids is not None
|
|
1548
|
+
and value.get("test_id") not in valid_test_ids
|
|
1549
|
+
):
|
|
1550
|
+
_issue(
|
|
1551
|
+
issues,
|
|
1552
|
+
"execution.evidence",
|
|
1553
|
+
f"{label}.test_id 未引用当前 manifest Test",
|
|
1554
|
+
item_id,
|
|
1555
|
+
)
|
|
1556
|
+
evidence_sha256 = value.get("sha256")
|
|
1557
|
+
if evidence_sha256 is not None and (
|
|
1558
|
+
not isinstance(evidence_sha256, str)
|
|
1559
|
+
or not re.fullmatch(r"[0-9a-f]{64}", evidence_sha256)
|
|
1560
|
+
):
|
|
1561
|
+
_issue(
|
|
1562
|
+
issues,
|
|
1563
|
+
"execution.evidence",
|
|
1564
|
+
f"{label}.sha256 必须是 64 位小写 SHA-256",
|
|
1565
|
+
item_id,
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
|
|
1569
|
+
def _validate_execution_state(
|
|
1570
|
+
manifest: dict[str, Any],
|
|
1571
|
+
execution: dict[str, Any],
|
|
1572
|
+
current_design_sha256: str,
|
|
1573
|
+
issues: list[ValidationIssue],
|
|
1574
|
+
) -> None:
|
|
1575
|
+
"""Validate the additive shared execution ledger."""
|
|
1576
|
+
|
|
1577
|
+
if execution.get("schema") != EXECUTION_SCHEMA:
|
|
1578
|
+
_issue(
|
|
1579
|
+
issues,
|
|
1580
|
+
"execution.schema",
|
|
1581
|
+
f"execution.schema 必须是 {EXECUTION_SCHEMA}",
|
|
1582
|
+
"execution",
|
|
1583
|
+
)
|
|
1584
|
+
if execution.get("spec_id") != manifest.get("spec_id"):
|
|
1585
|
+
_issue(issues, "execution.spec_id", "execution.spec_id 必须与 manifest 一致", "execution")
|
|
1586
|
+
if execution.get("design_revision") != manifest.get("revision"):
|
|
1587
|
+
_issue(
|
|
1588
|
+
issues,
|
|
1589
|
+
"execution.design_revision",
|
|
1590
|
+
"execution.design_revision 必须与 manifest.revision 一致;设计改动后应执行 sync-design",
|
|
1591
|
+
"execution",
|
|
1592
|
+
)
|
|
1593
|
+
if execution.get("design_sha256") != current_design_sha256:
|
|
1594
|
+
_issue(
|
|
1595
|
+
issues,
|
|
1596
|
+
"execution.design_sha256",
|
|
1597
|
+
"execution.design_sha256 与当前静态设计不一致;设计改动后应执行 sync-design",
|
|
1598
|
+
"execution",
|
|
1599
|
+
)
|
|
1600
|
+
execution_revision = execution.get("execution_revision")
|
|
1601
|
+
if (
|
|
1602
|
+
not isinstance(execution_revision, int)
|
|
1603
|
+
or isinstance(execution_revision, bool)
|
|
1604
|
+
or execution_revision < 0
|
|
1605
|
+
):
|
|
1606
|
+
_issue(
|
|
1607
|
+
issues,
|
|
1608
|
+
"execution.revision",
|
|
1609
|
+
"execution.execution_revision 必须是大于等于 0 的整数",
|
|
1610
|
+
"execution",
|
|
1611
|
+
)
|
|
1612
|
+
updated_at = execution.get("updated_at")
|
|
1613
|
+
if updated_at is not None and (
|
|
1614
|
+
not isinstance(updated_at, str)
|
|
1615
|
+
or not re.fullmatch(r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})", updated_at)
|
|
1616
|
+
):
|
|
1617
|
+
_issue(issues, "execution.timestamp", "execution.updated_at 必须是 ISO-8601 时间或 null", "execution")
|
|
1618
|
+
|
|
1619
|
+
manifest_tasks = {
|
|
1620
|
+
str(task.get("task_id")): task
|
|
1621
|
+
for task in manifest.get("tasks", [])
|
|
1622
|
+
if isinstance(task, dict) and isinstance(task.get("task_id"), str)
|
|
1623
|
+
}
|
|
1624
|
+
manifest_steps = {
|
|
1625
|
+
str(step.get("step_id")): step
|
|
1626
|
+
for step in manifest.get("steps", [])
|
|
1627
|
+
if isinstance(step, dict) and isinstance(step.get("step_id"), str)
|
|
1628
|
+
}
|
|
1629
|
+
manifest_test_ids = {
|
|
1630
|
+
str(test.get("test_id"))
|
|
1631
|
+
for test in manifest.get("tests", [])
|
|
1632
|
+
if isinstance(test, dict) and isinstance(test.get("test_id"), str)
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
events = execution.get("events")
|
|
1636
|
+
if not isinstance(events, list):
|
|
1637
|
+
_issue(issues, "execution.events", "execution.events 必须是数组", "execution")
|
|
1638
|
+
events = []
|
|
1639
|
+
event_ids: set[str] = set()
|
|
1640
|
+
event_by_id: dict[str, dict[str, Any]] = {}
|
|
1641
|
+
idempotency_keys: set[str] = set()
|
|
1642
|
+
for index, event in enumerate(events):
|
|
1643
|
+
label = f"execution.events[{index}]"
|
|
1644
|
+
if not isinstance(event, dict):
|
|
1645
|
+
_issue(issues, "execution.event", f"{label} 必须是 object", "execution")
|
|
1646
|
+
continue
|
|
1647
|
+
event_id = event.get("event_id")
|
|
1648
|
+
if not isinstance(event_id, str) or not re.fullmatch(r"EV-[0-9a-fA-F-]{36}", event_id):
|
|
1649
|
+
_issue(issues, "execution.event", f"{label}.event_id 非法", "execution")
|
|
1650
|
+
elif event_id in event_ids:
|
|
1651
|
+
_issue(issues, "execution.event", f"执行事件 ID 重复:{event_id}", "execution")
|
|
1652
|
+
else:
|
|
1653
|
+
event_ids.add(event_id)
|
|
1654
|
+
event_by_id[event_id] = event
|
|
1655
|
+
idempotency_key = event.get("idempotency_key")
|
|
1656
|
+
if idempotency_key is not None:
|
|
1657
|
+
if not isinstance(idempotency_key, str) or not idempotency_key.strip():
|
|
1658
|
+
_issue(issues, "execution.event", f"{label}.idempotency_key 非法", "execution")
|
|
1659
|
+
elif idempotency_key in idempotency_keys:
|
|
1660
|
+
_issue(
|
|
1661
|
+
issues,
|
|
1662
|
+
"execution.event",
|
|
1663
|
+
f"执行事件幂等键重复:{idempotency_key}",
|
|
1664
|
+
"execution",
|
|
1665
|
+
)
|
|
1666
|
+
else:
|
|
1667
|
+
idempotency_keys.add(idempotency_key)
|
|
1668
|
+
if event.get("type") not in VALID_EXECUTION_EVENT_TYPES:
|
|
1669
|
+
_issue(issues, "execution.event", f"{label}.type 非法", "execution")
|
|
1670
|
+
for field_name in ("app", "agent", "timestamp", "summary"):
|
|
1671
|
+
if not isinstance(event.get(field_name), str) or not event[field_name].strip():
|
|
1672
|
+
_issue(
|
|
1673
|
+
issues,
|
|
1674
|
+
"execution.event",
|
|
1675
|
+
f"{label}.{field_name} 必须是非空字符串",
|
|
1676
|
+
"execution",
|
|
1677
|
+
)
|
|
1678
|
+
event_timestamp = event.get("timestamp")
|
|
1679
|
+
if isinstance(event_timestamp, str):
|
|
1680
|
+
if not re.fullmatch(
|
|
1681
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
|
|
1682
|
+
event_timestamp,
|
|
1683
|
+
):
|
|
1684
|
+
_issue(
|
|
1685
|
+
issues,
|
|
1686
|
+
"execution.timestamp",
|
|
1687
|
+
f"{label}.timestamp 必须是 ISO-8601 时间",
|
|
1688
|
+
"execution",
|
|
1689
|
+
)
|
|
1690
|
+
event_design_revision = event.get("design_revision")
|
|
1691
|
+
manifest_revision = manifest.get("revision")
|
|
1692
|
+
if (
|
|
1693
|
+
not isinstance(event_design_revision, int)
|
|
1694
|
+
or isinstance(event_design_revision, bool)
|
|
1695
|
+
or not isinstance(manifest_revision, int)
|
|
1696
|
+
or event_design_revision < 1
|
|
1697
|
+
or event_design_revision > manifest_revision
|
|
1698
|
+
):
|
|
1699
|
+
_issue(
|
|
1700
|
+
issues,
|
|
1701
|
+
"execution.event",
|
|
1702
|
+
f"{label}.design_revision 必须引用当前或历史静态 revision",
|
|
1703
|
+
"execution",
|
|
1704
|
+
)
|
|
1705
|
+
if event.get("type") == "spec_revised":
|
|
1706
|
+
task_ids = event.get("task_ids")
|
|
1707
|
+
if not isinstance(task_ids, list) or not task_ids or not all(
|
|
1708
|
+
isinstance(task_id, str) and task_id for task_id in task_ids
|
|
1709
|
+
):
|
|
1710
|
+
_issue(
|
|
1711
|
+
issues,
|
|
1712
|
+
"execution.event",
|
|
1713
|
+
f"{label}.task_ids 必须列出受设计修订影响的任务",
|
|
1714
|
+
"execution",
|
|
1715
|
+
)
|
|
1716
|
+
elif len(task_ids) != len(set(task_ids)):
|
|
1717
|
+
_issue(
|
|
1718
|
+
issues,
|
|
1719
|
+
"execution.event",
|
|
1720
|
+
f"{label}.task_ids 不得重复",
|
|
1721
|
+
"execution",
|
|
1722
|
+
)
|
|
1723
|
+
elif event_design_revision == manifest.get("revision") and not set(task_ids).issubset(
|
|
1724
|
+
manifest_tasks
|
|
1725
|
+
):
|
|
1726
|
+
_issue(
|
|
1727
|
+
issues,
|
|
1728
|
+
"execution.event",
|
|
1729
|
+
f"{label}.task_ids 含当前 manifest 不存在的任务",
|
|
1730
|
+
"execution",
|
|
1731
|
+
)
|
|
1732
|
+
requested_task_ids = event.get("requested_task_ids")
|
|
1733
|
+
if not isinstance(requested_task_ids, list) or not all(
|
|
1734
|
+
isinstance(task_id, str) and task_id for task_id in requested_task_ids
|
|
1735
|
+
):
|
|
1736
|
+
_issue(
|
|
1737
|
+
issues,
|
|
1738
|
+
"execution.event",
|
|
1739
|
+
f"{label}.requested_task_ids 必须是字符串数组",
|
|
1740
|
+
"execution",
|
|
1741
|
+
)
|
|
1742
|
+
elif len(requested_task_ids) != len(set(requested_task_ids)):
|
|
1743
|
+
_issue(
|
|
1744
|
+
issues,
|
|
1745
|
+
"execution.event",
|
|
1746
|
+
f"{label}.requested_task_ids 不得重复",
|
|
1747
|
+
"execution",
|
|
1748
|
+
)
|
|
1749
|
+
from_revision = event.get("from_design_revision")
|
|
1750
|
+
to_revision = event.get("to_design_revision")
|
|
1751
|
+
if (
|
|
1752
|
+
not isinstance(from_revision, int)
|
|
1753
|
+
or not isinstance(to_revision, int)
|
|
1754
|
+
or to_revision != from_revision + 1
|
|
1755
|
+
or event_design_revision != to_revision
|
|
1756
|
+
):
|
|
1757
|
+
_issue(
|
|
1758
|
+
issues,
|
|
1759
|
+
"execution.event",
|
|
1760
|
+
f"{label} 的设计 revision 迁移必须恰好递增 1",
|
|
1761
|
+
"execution",
|
|
1762
|
+
)
|
|
1763
|
+
elif not isinstance(event.get("task_id"), str) or not event["task_id"].strip():
|
|
1764
|
+
_issue(issues, "execution.event", f"{label}.task_id 必须是非空字符串", "execution")
|
|
1765
|
+
if event.get("type") == "task_status_changed":
|
|
1766
|
+
from_status = event.get("from_status")
|
|
1767
|
+
to_status = event.get("to_status")
|
|
1768
|
+
if (
|
|
1769
|
+
from_status not in VALID_EXECUTION_TASK_STATUSES
|
|
1770
|
+
or to_status not in VALID_EXECUTION_TASK_STATUSES
|
|
1771
|
+
or to_status not in VALID_EXECUTION_TASK_TRANSITIONS.get(str(from_status), set())
|
|
1772
|
+
):
|
|
1773
|
+
_issue(issues, "execution.event", f"{label} 的任务状态迁移字段非法", "execution")
|
|
1774
|
+
if (
|
|
1775
|
+
event_design_revision == manifest.get("revision")
|
|
1776
|
+
and event.get("task_id") not in manifest_tasks
|
|
1777
|
+
):
|
|
1778
|
+
_issue(
|
|
1779
|
+
issues,
|
|
1780
|
+
"execution.event",
|
|
1781
|
+
f"{label}.task_id 未引用当前 manifest task",
|
|
1782
|
+
"execution",
|
|
1783
|
+
)
|
|
1784
|
+
if event_design_revision == manifest.get("revision"):
|
|
1785
|
+
allowed_test_ids = set(
|
|
1786
|
+
_string_list(manifest_tasks.get(str(event.get("task_id")), {}), "test_ids")
|
|
1787
|
+
)
|
|
1788
|
+
event_test_ids = {
|
|
1789
|
+
str(value.get("test_id"))
|
|
1790
|
+
for value in event.get("evidence", [])
|
|
1791
|
+
if isinstance(value, dict) and value.get("kind") == "test"
|
|
1792
|
+
} if isinstance(event.get("evidence", []), list) else set()
|
|
1793
|
+
if not event_test_ids.issubset(allowed_test_ids):
|
|
1794
|
+
_issue(
|
|
1795
|
+
issues,
|
|
1796
|
+
"execution.event",
|
|
1797
|
+
f"{label} 引用了其他任务的 Test",
|
|
1798
|
+
"execution",
|
|
1799
|
+
)
|
|
1800
|
+
if event.get("type") == "step_status_changed":
|
|
1801
|
+
if event.get("step_status") not in {"completed", "failed"}:
|
|
1802
|
+
_issue(issues, "execution.event", f"{label}.step_status 非法", "execution")
|
|
1803
|
+
if event_design_revision == manifest.get("revision"):
|
|
1804
|
+
event_task_id = str(event.get("task_id"))
|
|
1805
|
+
event_step_id = str(event.get("step_id"))
|
|
1806
|
+
step = manifest_steps.get(event_step_id)
|
|
1807
|
+
if step is None or step.get("task_id") != event_task_id:
|
|
1808
|
+
_issue(
|
|
1809
|
+
issues,
|
|
1810
|
+
"execution.event",
|
|
1811
|
+
f"{label}.step_id 未引用当前 task 的 Step",
|
|
1812
|
+
"execution",
|
|
1813
|
+
)
|
|
1814
|
+
elif event.get("step_status") == "completed":
|
|
1815
|
+
event_evidence = (
|
|
1816
|
+
event.get("evidence", [])
|
|
1817
|
+
if isinstance(event.get("evidence", []), list)
|
|
1818
|
+
else []
|
|
1819
|
+
)
|
|
1820
|
+
passed_test_ids = {
|
|
1821
|
+
str(value.get("test_id"))
|
|
1822
|
+
for value in event_evidence
|
|
1823
|
+
if isinstance(value, dict)
|
|
1824
|
+
and value.get("kind") == "test"
|
|
1825
|
+
and value.get("status") == "passed"
|
|
1826
|
+
}
|
|
1827
|
+
missing_test_ids = set(_string_list(step, "test_ids")) - passed_test_ids
|
|
1828
|
+
if missing_test_ids:
|
|
1829
|
+
_issue(
|
|
1830
|
+
issues,
|
|
1831
|
+
"execution.event",
|
|
1832
|
+
f"{label} 缺少绑定 Test 的 passed 证据:{', '.join(sorted(missing_test_ids))}",
|
|
1833
|
+
"execution",
|
|
1834
|
+
)
|
|
1835
|
+
event_task_test_ids = set(
|
|
1836
|
+
_string_list(manifest_tasks.get(event_task_id, {}), "test_ids")
|
|
1837
|
+
)
|
|
1838
|
+
unknown_test_ids = passed_test_ids - event_task_test_ids
|
|
1839
|
+
if unknown_test_ids:
|
|
1840
|
+
_issue(
|
|
1841
|
+
issues,
|
|
1842
|
+
"execution.event",
|
|
1843
|
+
f"{label} 引用了其他任务或不存在的 Test:{', '.join(sorted(unknown_test_ids))}",
|
|
1844
|
+
"execution",
|
|
1845
|
+
)
|
|
1846
|
+
event_task_test_ids = set(
|
|
1847
|
+
_string_list(manifest_tasks.get(event_task_id, {}), "test_ids")
|
|
1848
|
+
)
|
|
1849
|
+
all_event_test_ids = {
|
|
1850
|
+
str(value.get("test_id"))
|
|
1851
|
+
for value in (
|
|
1852
|
+
event.get("evidence", [])
|
|
1853
|
+
if isinstance(event.get("evidence", []), list)
|
|
1854
|
+
else []
|
|
1855
|
+
)
|
|
1856
|
+
if isinstance(value, dict) and value.get("kind") == "test"
|
|
1857
|
+
}
|
|
1858
|
+
if not all_event_test_ids.issubset(event_task_test_ids):
|
|
1859
|
+
_issue(
|
|
1860
|
+
issues,
|
|
1861
|
+
"execution.event",
|
|
1862
|
+
f"{label} 引用了其他任务的 Test",
|
|
1863
|
+
"execution",
|
|
1864
|
+
)
|
|
1865
|
+
if event.get("type") == "dependency_status_changed" and (
|
|
1866
|
+
event.get("dependency_type") not in VALID_DEPENDENCY_TYPES
|
|
1867
|
+
or event.get("dependency_status")
|
|
1868
|
+
not in VALID_EXECUTION_DEPENDENCY_STATUSES
|
|
1869
|
+
):
|
|
1870
|
+
_issue(issues, "execution.event", f"{label} 的依赖状态字段非法", "execution")
|
|
1871
|
+
if (
|
|
1872
|
+
event.get("type") == "dependency_status_changed"
|
|
1873
|
+
and event_design_revision == manifest.get("revision")
|
|
1874
|
+
):
|
|
1875
|
+
source_task = manifest_tasks.get(str(event.get("task_id")))
|
|
1876
|
+
expected_edge = (
|
|
1877
|
+
source_task is not None
|
|
1878
|
+
and any(
|
|
1879
|
+
isinstance(dependency, dict)
|
|
1880
|
+
and dependency.get("task_id") == event.get("dependency_task_id")
|
|
1881
|
+
and dependency.get("type") == event.get("dependency_type")
|
|
1882
|
+
for dependency in source_task.get("depends_on", [])
|
|
1883
|
+
)
|
|
1884
|
+
)
|
|
1885
|
+
if not expected_edge:
|
|
1886
|
+
_issue(
|
|
1887
|
+
issues,
|
|
1888
|
+
"execution.event",
|
|
1889
|
+
f"{label} 未引用当前 manifest 的依赖边",
|
|
1890
|
+
"execution",
|
|
1891
|
+
)
|
|
1892
|
+
source_task_id = str(event.get("task_id"))
|
|
1893
|
+
dependency_task_id = str(event.get("dependency_task_id"))
|
|
1894
|
+
allowed_test_ids = set(
|
|
1895
|
+
_string_list(manifest_tasks.get(source_task_id, {}), "test_ids")
|
|
1896
|
+
) | set(_string_list(manifest_tasks.get(dependency_task_id, {}), "test_ids"))
|
|
1897
|
+
event_test_ids = {
|
|
1898
|
+
str(value.get("test_id"))
|
|
1899
|
+
for value in (
|
|
1900
|
+
event.get("evidence", [])
|
|
1901
|
+
if isinstance(event.get("evidence", []), list)
|
|
1902
|
+
else []
|
|
1903
|
+
)
|
|
1904
|
+
if isinstance(value, dict) and value.get("kind") == "test"
|
|
1905
|
+
}
|
|
1906
|
+
if not event_test_ids.issubset(allowed_test_ids):
|
|
1907
|
+
_issue(
|
|
1908
|
+
issues,
|
|
1909
|
+
"execution.event",
|
|
1910
|
+
f"{label} 引用了依赖边之外的 Test",
|
|
1911
|
+
"execution",
|
|
1912
|
+
)
|
|
1913
|
+
_validate_execution_evidence(
|
|
1914
|
+
event.get("evidence", []),
|
|
1915
|
+
issues,
|
|
1916
|
+
label,
|
|
1917
|
+
valid_test_ids=(
|
|
1918
|
+
manifest_test_ids
|
|
1919
|
+
if event_design_revision == manifest.get("revision")
|
|
1920
|
+
else None
|
|
1921
|
+
),
|
|
1922
|
+
)
|
|
1923
|
+
|
|
1924
|
+
active_design_revision: int | None = None
|
|
1925
|
+
for index, event in enumerate(events):
|
|
1926
|
+
if not isinstance(event, dict):
|
|
1927
|
+
continue
|
|
1928
|
+
event_revision = event.get("design_revision")
|
|
1929
|
+
if not isinstance(event_revision, int) or isinstance(event_revision, bool):
|
|
1930
|
+
continue
|
|
1931
|
+
if active_design_revision is None:
|
|
1932
|
+
active_design_revision = (
|
|
1933
|
+
event.get("from_design_revision")
|
|
1934
|
+
if event.get("type") == "spec_revised"
|
|
1935
|
+
and isinstance(event.get("from_design_revision"), int)
|
|
1936
|
+
else event_revision
|
|
1937
|
+
)
|
|
1938
|
+
if event.get("type") == "spec_revised":
|
|
1939
|
+
if event.get("from_design_revision") != active_design_revision:
|
|
1940
|
+
_issue(
|
|
1941
|
+
issues,
|
|
1942
|
+
"execution.revision_chain",
|
|
1943
|
+
f"execution.events[{index}] 未承接前一设计 revision",
|
|
1944
|
+
"execution",
|
|
1945
|
+
)
|
|
1946
|
+
if isinstance(event.get("to_design_revision"), int):
|
|
1947
|
+
active_design_revision = event["to_design_revision"]
|
|
1948
|
+
elif event_revision != active_design_revision:
|
|
1949
|
+
_issue(
|
|
1950
|
+
issues,
|
|
1951
|
+
"execution.revision_chain",
|
|
1952
|
+
f"execution.events[{index}].design_revision 与事件链当前 revision 不一致",
|
|
1953
|
+
"execution",
|
|
1954
|
+
)
|
|
1955
|
+
if events and active_design_revision != execution.get("design_revision"):
|
|
1956
|
+
_issue(
|
|
1957
|
+
issues,
|
|
1958
|
+
"execution.revision_chain",
|
|
1959
|
+
"执行事件的设计 revision 链未到达当前 execution.design_revision",
|
|
1960
|
+
"execution",
|
|
1961
|
+
)
|
|
1962
|
+
current_spec_revision_events = [
|
|
1963
|
+
event
|
|
1964
|
+
for event in events
|
|
1965
|
+
if isinstance(event, dict)
|
|
1966
|
+
and event.get("type") == "spec_revised"
|
|
1967
|
+
and event.get("design_revision") == manifest.get("revision")
|
|
1968
|
+
]
|
|
1969
|
+
for event in current_spec_revision_events:
|
|
1970
|
+
requested = event.get("requested_task_ids", [])
|
|
1971
|
+
if not isinstance(requested, list):
|
|
1972
|
+
continue
|
|
1973
|
+
requested_set = set(requested) if requested else set(manifest_tasks)
|
|
1974
|
+
if not requested_set.issubset(manifest_tasks):
|
|
1975
|
+
continue
|
|
1976
|
+
reverse_dependencies = {task_id: set() for task_id in manifest_tasks}
|
|
1977
|
+
for dependent_task_id, task in manifest_tasks.items():
|
|
1978
|
+
for dependency in task.get("depends_on", []):
|
|
1979
|
+
if isinstance(dependency, dict) and dependency.get("task_id") in manifest_tasks:
|
|
1980
|
+
reverse_dependencies[str(dependency.get("task_id"))].add(
|
|
1981
|
+
dependent_task_id
|
|
1982
|
+
)
|
|
1983
|
+
expected_reset = set(requested_set)
|
|
1984
|
+
pending_reset = list(requested_set)
|
|
1985
|
+
while pending_reset:
|
|
1986
|
+
changed_task_id = pending_reset.pop()
|
|
1987
|
+
for dependent_task_id in reverse_dependencies.get(changed_task_id, set()):
|
|
1988
|
+
if dependent_task_id not in expected_reset:
|
|
1989
|
+
expected_reset.add(dependent_task_id)
|
|
1990
|
+
pending_reset.append(dependent_task_id)
|
|
1991
|
+
if set(event.get("task_ids", [])) != expected_reset:
|
|
1992
|
+
_issue(
|
|
1993
|
+
issues,
|
|
1994
|
+
"execution.revision_chain",
|
|
1995
|
+
"当前 spec_revised.task_ids 必须等于显式受影响任务及其全部后继闭包",
|
|
1996
|
+
"execution",
|
|
1997
|
+
)
|
|
1998
|
+
|
|
1999
|
+
if isinstance(execution_revision, int) and execution_revision != len(events):
|
|
2000
|
+
_issue(
|
|
2001
|
+
issues,
|
|
2002
|
+
"execution.revision",
|
|
2003
|
+
"execution.execution_revision 必须等于追加事件数量",
|
|
2004
|
+
"execution",
|
|
2005
|
+
)
|
|
2006
|
+
if events:
|
|
2007
|
+
last_timestamp = events[-1].get("timestamp") if isinstance(events[-1], dict) else None
|
|
2008
|
+
if updated_at != last_timestamp:
|
|
2009
|
+
_issue(
|
|
2010
|
+
issues,
|
|
2011
|
+
"execution.timestamp",
|
|
2012
|
+
"execution.updated_at 必须等于最后一条事件时间",
|
|
2013
|
+
"execution",
|
|
2014
|
+
)
|
|
2015
|
+
elif updated_at is not None:
|
|
2016
|
+
_issue(
|
|
2017
|
+
issues,
|
|
2018
|
+
"execution.timestamp",
|
|
2019
|
+
"没有执行事件时 execution.updated_at 必须是 null",
|
|
2020
|
+
"execution",
|
|
2021
|
+
)
|
|
2022
|
+
|
|
2023
|
+
snapshots = execution.get("tasks")
|
|
2024
|
+
if not isinstance(snapshots, list):
|
|
2025
|
+
_issue(issues, "execution.tasks", "execution.tasks 必须是数组", "execution")
|
|
2026
|
+
snapshots = []
|
|
2027
|
+
snapshot_by_id: dict[str, dict[str, Any]] = {}
|
|
2028
|
+
for index, snapshot in enumerate(snapshots):
|
|
2029
|
+
label = f"execution.tasks[{index}]"
|
|
2030
|
+
if not isinstance(snapshot, dict):
|
|
2031
|
+
_issue(issues, "execution.task", f"{label} 必须是 object", "execution")
|
|
2032
|
+
continue
|
|
2033
|
+
task_id = snapshot.get("task_id")
|
|
2034
|
+
if not isinstance(task_id, str) or task_id not in manifest_tasks:
|
|
2035
|
+
_issue(issues, "execution.task", f"{label}.task_id 未引用当前 manifest task", "execution")
|
|
2036
|
+
continue
|
|
2037
|
+
if task_id in snapshot_by_id:
|
|
2038
|
+
_issue(issues, "execution.task", f"执行任务快照重复:{task_id}", task_id)
|
|
2039
|
+
continue
|
|
2040
|
+
snapshot_by_id[task_id] = snapshot
|
|
2041
|
+
if snapshot.get("status") not in VALID_EXECUTION_TASK_STATUSES:
|
|
2042
|
+
_issue(issues, "execution.task", f"{task_id}.status 非法", task_id)
|
|
2043
|
+
expected_steps = set(_string_list(manifest_tasks[task_id], "step_ids"))
|
|
2044
|
+
completed_steps = snapshot.get("completed_step_ids")
|
|
2045
|
+
failed_steps = snapshot.get("failed_step_ids")
|
|
2046
|
+
for field_name, values in (
|
|
2047
|
+
("completed_step_ids", completed_steps),
|
|
2048
|
+
("failed_step_ids", failed_steps),
|
|
2049
|
+
):
|
|
2050
|
+
if not isinstance(values, list) or not all(isinstance(value, str) for value in values):
|
|
2051
|
+
_issue(issues, "execution.task", f"{task_id}.{field_name} 必须是字符串数组", task_id)
|
|
2052
|
+
elif len(values) != len(set(values)) or not set(values).issubset(expected_steps):
|
|
2053
|
+
_issue(
|
|
2054
|
+
issues,
|
|
2055
|
+
"execution.task",
|
|
2056
|
+
f"{task_id}.{field_name} 必须是本任务 Step 的无重复子集",
|
|
2057
|
+
task_id,
|
|
2058
|
+
)
|
|
2059
|
+
if isinstance(completed_steps, list) and isinstance(failed_steps, list) and set(
|
|
2060
|
+
completed_steps
|
|
2061
|
+
).intersection(failed_steps):
|
|
2062
|
+
_issue(issues, "execution.task", f"{task_id} 的完成和失败 Step 不能重叠", task_id)
|
|
2063
|
+
blockers = snapshot.get("blockers")
|
|
2064
|
+
if not isinstance(blockers, list) or not all(
|
|
2065
|
+
isinstance(blocker, str) and blocker.strip() for blocker in blockers
|
|
2066
|
+
):
|
|
2067
|
+
_issue(issues, "execution.task", f"{task_id}.blockers 必须是非空字符串数组", task_id)
|
|
2068
|
+
last_event_id = snapshot.get("last_event_id")
|
|
2069
|
+
if last_event_id is not None and last_event_id not in event_ids:
|
|
2070
|
+
_issue(issues, "execution.task", f"{task_id}.last_event_id 未引用存在的事件", task_id)
|
|
2071
|
+
snapshot_updated_at = snapshot.get("updated_at")
|
|
2072
|
+
if snapshot_updated_at is not None and (
|
|
2073
|
+
not isinstance(snapshot_updated_at, str)
|
|
2074
|
+
or not re.fullmatch(
|
|
2075
|
+
r"\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})",
|
|
2076
|
+
snapshot_updated_at,
|
|
2077
|
+
)
|
|
2078
|
+
):
|
|
2079
|
+
_issue(
|
|
2080
|
+
issues,
|
|
2081
|
+
"execution.timestamp",
|
|
2082
|
+
f"{task_id}.updated_at 必须是 ISO-8601 时间或 null",
|
|
2083
|
+
task_id,
|
|
2084
|
+
)
|
|
2085
|
+
linked_last_event = event_by_id.get(str(last_event_id)) if last_event_id else None
|
|
2086
|
+
if linked_last_event is not None:
|
|
2087
|
+
owns_task = linked_last_event.get("task_id") == task_id or (
|
|
2088
|
+
linked_last_event.get("type") == "spec_revised"
|
|
2089
|
+
and task_id in linked_last_event.get("task_ids", [])
|
|
2090
|
+
)
|
|
2091
|
+
if not owns_task:
|
|
2092
|
+
_issue(
|
|
2093
|
+
issues,
|
|
2094
|
+
"execution.task",
|
|
2095
|
+
f"{task_id}.last_event_id 引用了其他任务的事件",
|
|
2096
|
+
task_id,
|
|
2097
|
+
)
|
|
2098
|
+
if snapshot_updated_at != linked_last_event.get("timestamp"):
|
|
2099
|
+
_issue(
|
|
2100
|
+
issues,
|
|
2101
|
+
"execution.timestamp",
|
|
2102
|
+
f"{task_id}.updated_at 必须等于 last_event_id 的事件时间",
|
|
2103
|
+
task_id,
|
|
2104
|
+
)
|
|
2105
|
+
elif last_event_id is None and snapshot_updated_at is not None:
|
|
2106
|
+
_issue(
|
|
2107
|
+
issues,
|
|
2108
|
+
"execution.timestamp",
|
|
2109
|
+
f"{task_id} 没有 last_event_id 时 updated_at 必须是 null",
|
|
2110
|
+
task_id,
|
|
2111
|
+
)
|
|
2112
|
+
snapshot_evidence = snapshot.get("evidence", [])
|
|
2113
|
+
_validate_execution_evidence(
|
|
2114
|
+
snapshot_evidence,
|
|
2115
|
+
issues,
|
|
2116
|
+
task_id,
|
|
2117
|
+
event_ids,
|
|
2118
|
+
manifest_test_ids,
|
|
2119
|
+
)
|
|
2120
|
+
if isinstance(snapshot_evidence, list):
|
|
2121
|
+
for evidence_index, evidence in enumerate(snapshot_evidence):
|
|
2122
|
+
if not isinstance(evidence, dict) or not isinstance(
|
|
2123
|
+
evidence.get("event_id"), str
|
|
2124
|
+
):
|
|
2125
|
+
continue
|
|
2126
|
+
source_event = event_by_id.get(evidence["event_id"])
|
|
2127
|
+
if source_event is None:
|
|
2128
|
+
continue
|
|
2129
|
+
owns_task = source_event.get("task_id") == task_id
|
|
2130
|
+
event_evidence = source_event.get("evidence", [])
|
|
2131
|
+
projected = {
|
|
2132
|
+
key: value for key, value in evidence.items() if key != "event_id"
|
|
2133
|
+
}
|
|
2134
|
+
if not owns_task or projected not in event_evidence:
|
|
2135
|
+
_issue(
|
|
2136
|
+
issues,
|
|
2137
|
+
"execution.evidence",
|
|
2138
|
+
f"{task_id}.evidence[{evidence_index}] 与来源事件不一致",
|
|
2139
|
+
task_id,
|
|
2140
|
+
)
|
|
2141
|
+
|
|
2142
|
+
dependencies = snapshot.get("dependencies")
|
|
2143
|
+
if not isinstance(dependencies, list):
|
|
2144
|
+
_issue(issues, "execution.dependency", f"{task_id}.dependencies 必须是数组", task_id)
|
|
2145
|
+
dependencies = []
|
|
2146
|
+
actual_dependencies: dict[tuple[str, str], dict[str, Any]] = {}
|
|
2147
|
+
for dependency in dependencies:
|
|
2148
|
+
if not isinstance(dependency, dict):
|
|
2149
|
+
_issue(issues, "execution.dependency", f"{task_id} 的依赖快照必须是 object", task_id)
|
|
2150
|
+
continue
|
|
2151
|
+
key = (str(dependency.get("task_id")), str(dependency.get("type")))
|
|
2152
|
+
if key in actual_dependencies:
|
|
2153
|
+
_issue(issues, "execution.dependency", f"{task_id} 的依赖快照重复:{key[0]}", task_id)
|
|
2154
|
+
actual_dependencies[key] = dependency
|
|
2155
|
+
if dependency.get("status") not in VALID_EXECUTION_DEPENDENCY_STATUSES:
|
|
2156
|
+
_issue(issues, "execution.dependency", f"{task_id}->{key[0]} 的执行依赖状态非法", task_id)
|
|
2157
|
+
evidence_event_id = dependency.get("evidence_event_id")
|
|
2158
|
+
if evidence_event_id is not None and evidence_event_id not in event_ids:
|
|
2159
|
+
_issue(
|
|
2160
|
+
issues,
|
|
2161
|
+
"execution.dependency",
|
|
2162
|
+
f"{task_id}->{key[0]} 的 evidence_event_id 未引用存在的事件",
|
|
2163
|
+
task_id,
|
|
2164
|
+
)
|
|
2165
|
+
if dependency.get("status") == "pending" and evidence_event_id is not None:
|
|
2166
|
+
_issue(
|
|
2167
|
+
issues,
|
|
2168
|
+
"execution.dependency",
|
|
2169
|
+
f"{task_id}->{key[0]} 为 pending 时不得保留 evidence_event_id",
|
|
2170
|
+
task_id,
|
|
2171
|
+
)
|
|
2172
|
+
if evidence_event_id is not None:
|
|
2173
|
+
evidence_event = event_by_id.get(str(evidence_event_id))
|
|
2174
|
+
if evidence_event is not None and not (
|
|
2175
|
+
evidence_event.get("type") == "dependency_status_changed"
|
|
2176
|
+
and evidence_event.get("task_id") == task_id
|
|
2177
|
+
and evidence_event.get("dependency_task_id") == key[0]
|
|
2178
|
+
and evidence_event.get("dependency_type") == key[1]
|
|
2179
|
+
and evidence_event.get("dependency_status") == "satisfied"
|
|
2180
|
+
):
|
|
2181
|
+
_issue(
|
|
2182
|
+
issues,
|
|
2183
|
+
"execution.dependency",
|
|
2184
|
+
f"{task_id}->{key[0]} 的 evidence_event_id 未引用本依赖的 satisfied 事件",
|
|
2185
|
+
task_id,
|
|
2186
|
+
)
|
|
2187
|
+
expected_dependencies = {
|
|
2188
|
+
(str(dependency.get("task_id")), str(dependency.get("type")))
|
|
2189
|
+
for dependency in manifest_tasks[task_id].get("depends_on", [])
|
|
2190
|
+
if isinstance(dependency, dict)
|
|
2191
|
+
}
|
|
2192
|
+
if set(actual_dependencies) != expected_dependencies:
|
|
2193
|
+
_issue(
|
|
2194
|
+
issues,
|
|
2195
|
+
"execution.dependency",
|
|
2196
|
+
f"{task_id}.dependencies 必须与 manifest.depends_on 完全一致",
|
|
2197
|
+
task_id,
|
|
2198
|
+
)
|
|
2199
|
+
if set(snapshot_by_id) != set(manifest_tasks):
|
|
2200
|
+
_issue(
|
|
2201
|
+
issues,
|
|
2202
|
+
"execution.tasks",
|
|
2203
|
+
"execution.tasks 必须为当前 manifest 中每个任务提供且只提供一个快照",
|
|
2204
|
+
"execution",
|
|
2205
|
+
)
|
|
2206
|
+
|
|
2207
|
+
replay_status = {task_id: "not_started" for task_id in manifest_tasks}
|
|
2208
|
+
replay_completed_steps = {task_id: set() for task_id in manifest_tasks}
|
|
2209
|
+
replay_failed_steps = {task_id: set() for task_id in manifest_tasks}
|
|
2210
|
+
replay_passed_test_ids = {task_id: set() for task_id in manifest_tasks}
|
|
2211
|
+
replay_dependencies: dict[str, dict[tuple[str, str], tuple[str, str | None]]] = {
|
|
2212
|
+
task_id: {
|
|
2213
|
+
(str(dependency.get("task_id")), str(dependency.get("type"))): (
|
|
2214
|
+
"satisfied"
|
|
2215
|
+
if dependency.get("type") == "contract" and manifest.get("status") == "READY"
|
|
2216
|
+
else "pending",
|
|
2217
|
+
None,
|
|
2218
|
+
)
|
|
2219
|
+
for dependency in task.get("depends_on", [])
|
|
2220
|
+
if isinstance(dependency, dict)
|
|
2221
|
+
}
|
|
2222
|
+
for task_id, task in manifest_tasks.items()
|
|
2223
|
+
}
|
|
2224
|
+
for event_index, event in enumerate(events):
|
|
2225
|
+
if not isinstance(event, dict):
|
|
2226
|
+
continue
|
|
2227
|
+
event_type = event.get("type")
|
|
2228
|
+
if event_type == "spec_revised":
|
|
2229
|
+
affected_task_ids = event.get("task_ids", [])
|
|
2230
|
+
if not isinstance(affected_task_ids, list):
|
|
2231
|
+
affected_task_ids = []
|
|
2232
|
+
for affected_task_id in affected_task_ids:
|
|
2233
|
+
if affected_task_id not in manifest_tasks:
|
|
2234
|
+
continue
|
|
2235
|
+
replay_status[affected_task_id] = "not_started"
|
|
2236
|
+
replay_completed_steps[affected_task_id] = set()
|
|
2237
|
+
replay_failed_steps[affected_task_id] = set()
|
|
2238
|
+
replay_passed_test_ids[affected_task_id] = set()
|
|
2239
|
+
replay_dependencies[affected_task_id] = {
|
|
2240
|
+
(str(dependency.get("task_id")), str(dependency.get("type"))): (
|
|
2241
|
+
"satisfied"
|
|
2242
|
+
if dependency.get("type") == "contract"
|
|
2243
|
+
and manifest.get("status") == "READY"
|
|
2244
|
+
else "pending",
|
|
2245
|
+
None,
|
|
2246
|
+
)
|
|
2247
|
+
for dependency in manifest_tasks[affected_task_id].get("depends_on", [])
|
|
2248
|
+
if isinstance(dependency, dict)
|
|
2249
|
+
}
|
|
2250
|
+
continue
|
|
2251
|
+
event_task_id = event.get("task_id")
|
|
2252
|
+
if event_task_id not in manifest_tasks:
|
|
2253
|
+
continue
|
|
2254
|
+
event_evidence = (
|
|
2255
|
+
event.get("evidence", [])
|
|
2256
|
+
if isinstance(event.get("evidence", []), list)
|
|
2257
|
+
else []
|
|
2258
|
+
)
|
|
2259
|
+
event_passed_test_ids = {
|
|
2260
|
+
str(value.get("test_id"))
|
|
2261
|
+
for value in event_evidence
|
|
2262
|
+
if isinstance(value, dict)
|
|
2263
|
+
and value.get("kind") == "test"
|
|
2264
|
+
and value.get("status") == "passed"
|
|
2265
|
+
and isinstance(value.get("test_id"), str)
|
|
2266
|
+
}
|
|
2267
|
+
if event_type == "task_status_changed":
|
|
2268
|
+
if event.get("from_status") != replay_status[event_task_id]:
|
|
2269
|
+
_issue(
|
|
2270
|
+
issues,
|
|
2271
|
+
"execution.event_chain",
|
|
2272
|
+
f"execution.events[{event_index}].from_status 与前一事件状态不一致",
|
|
2273
|
+
event_task_id,
|
|
2274
|
+
)
|
|
2275
|
+
from_status = replay_status[event_task_id]
|
|
2276
|
+
to_status = event.get("to_status")
|
|
2277
|
+
if to_status == "in_progress" and from_status in {"completed", "cancelled"}:
|
|
2278
|
+
replay_passed_test_ids[event_task_id] = set()
|
|
2279
|
+
replay_passed_test_ids[event_task_id].update(event_passed_test_ids)
|
|
2280
|
+
if event.get("design_revision") == manifest.get("revision") and to_status in {
|
|
2281
|
+
"in_progress",
|
|
2282
|
+
"implemented",
|
|
2283
|
+
"verified",
|
|
2284
|
+
"completed",
|
|
2285
|
+
}:
|
|
2286
|
+
unsatisfied_dependencies: list[str] = []
|
|
2287
|
+
for dependency in manifest_tasks[event_task_id].get("depends_on", []):
|
|
2288
|
+
if not isinstance(dependency, dict) or dependency.get("type") not in {
|
|
2289
|
+
"hard",
|
|
2290
|
+
"contract",
|
|
2291
|
+
}:
|
|
2292
|
+
continue
|
|
2293
|
+
dependency_key = (
|
|
2294
|
+
str(dependency.get("task_id")),
|
|
2295
|
+
str(dependency.get("type")),
|
|
2296
|
+
)
|
|
2297
|
+
satisfied = replay_dependencies[event_task_id].get(
|
|
2298
|
+
dependency_key, ("pending", None)
|
|
2299
|
+
)[0] == "satisfied"
|
|
2300
|
+
if dependency.get("type") == "contract":
|
|
2301
|
+
satisfied = manifest.get("status") == "READY"
|
|
2302
|
+
elif dependency.get("type") == "hard" and not satisfied:
|
|
2303
|
+
satisfied = (
|
|
2304
|
+
replay_status.get(str(dependency.get("task_id"))) == "completed"
|
|
2305
|
+
)
|
|
2306
|
+
if not satisfied:
|
|
2307
|
+
unsatisfied_dependencies.append(
|
|
2308
|
+
f"{dependency.get('type')}:{dependency.get('task_id')}"
|
|
2309
|
+
)
|
|
2310
|
+
if unsatisfied_dependencies:
|
|
2311
|
+
_issue(
|
|
2312
|
+
issues,
|
|
2313
|
+
"execution.event_chain",
|
|
2314
|
+
f"execution.events[{event_index}] 执行时前置依赖未满足:{', '.join(unsatisfied_dependencies)}",
|
|
2315
|
+
event_task_id,
|
|
2316
|
+
)
|
|
2317
|
+
if event.get("design_revision") == manifest.get("revision") and to_status in {
|
|
2318
|
+
"implemented",
|
|
2319
|
+
"verified",
|
|
2320
|
+
"completed",
|
|
2321
|
+
}:
|
|
2322
|
+
expected_steps = set(_string_list(manifest_tasks[event_task_id], "step_ids"))
|
|
2323
|
+
if (
|
|
2324
|
+
replay_completed_steps[event_task_id] != expected_steps
|
|
2325
|
+
or replay_failed_steps[event_task_id]
|
|
2326
|
+
):
|
|
2327
|
+
_issue(
|
|
2328
|
+
issues,
|
|
2329
|
+
"execution.event_chain",
|
|
2330
|
+
f"execution.events[{event_index}] 执行时尚未完成全部 Step",
|
|
2331
|
+
event_task_id,
|
|
2332
|
+
)
|
|
2333
|
+
if event.get("design_revision") == manifest.get("revision") and to_status in {
|
|
2334
|
+
"verified",
|
|
2335
|
+
"completed",
|
|
2336
|
+
}:
|
|
2337
|
+
expected_tests = set(_string_list(manifest_tasks[event_task_id], "test_ids"))
|
|
2338
|
+
missing_tests = expected_tests - replay_passed_test_ids[event_task_id]
|
|
2339
|
+
if missing_tests:
|
|
2340
|
+
_issue(
|
|
2341
|
+
issues,
|
|
2342
|
+
"execution.event_chain",
|
|
2343
|
+
f"execution.events[{event_index}] 执行时缺少 Test 证据:{', '.join(sorted(missing_tests))}",
|
|
2344
|
+
event_task_id,
|
|
2345
|
+
)
|
|
2346
|
+
if event.get("design_revision") == manifest.get("revision") and to_status == "completed":
|
|
2347
|
+
pending_integration = [
|
|
2348
|
+
str(dependency.get("task_id"))
|
|
2349
|
+
for dependency in manifest_tasks[event_task_id].get("depends_on", [])
|
|
2350
|
+
if isinstance(dependency, dict)
|
|
2351
|
+
and dependency.get("type") == "integration"
|
|
2352
|
+
and replay_dependencies[event_task_id].get(
|
|
2353
|
+
(str(dependency.get("task_id")), "integration"),
|
|
2354
|
+
("pending", None),
|
|
2355
|
+
)[0]
|
|
2356
|
+
!= "satisfied"
|
|
2357
|
+
]
|
|
2358
|
+
if pending_integration:
|
|
2359
|
+
_issue(
|
|
2360
|
+
issues,
|
|
2361
|
+
"execution.event_chain",
|
|
2362
|
+
f"execution.events[{event_index}] 执行时 integration 依赖未满足:{', '.join(pending_integration)}",
|
|
2363
|
+
event_task_id,
|
|
2364
|
+
)
|
|
2365
|
+
if to_status in VALID_EXECUTION_TASK_STATUSES:
|
|
2366
|
+
replay_status[event_task_id] = str(to_status)
|
|
2367
|
+
continue
|
|
2368
|
+
if event_type == "step_status_changed":
|
|
2369
|
+
event_step_id = str(event.get("step_id"))
|
|
2370
|
+
step = manifest_steps.get(event_step_id)
|
|
2371
|
+
if step is None or step.get("task_id") != event_task_id:
|
|
2372
|
+
continue
|
|
2373
|
+
if event.get("design_revision") == manifest.get("revision"):
|
|
2374
|
+
if replay_status[event_task_id] != "in_progress":
|
|
2375
|
+
_issue(
|
|
2376
|
+
issues,
|
|
2377
|
+
"execution.event_chain",
|
|
2378
|
+
f"execution.events[{event_index}] 写入 Step 时任务不是 in_progress",
|
|
2379
|
+
event_task_id,
|
|
2380
|
+
)
|
|
2381
|
+
missing_predecessors = set(
|
|
2382
|
+
_string_list(step, "depends_on_step_ids")
|
|
2383
|
+
) - replay_completed_steps[event_task_id]
|
|
2384
|
+
if missing_predecessors:
|
|
2385
|
+
_issue(
|
|
2386
|
+
issues,
|
|
2387
|
+
"execution.event_chain",
|
|
2388
|
+
f"execution.events[{event_index}] 越过前置 Step:{', '.join(sorted(missing_predecessors))}",
|
|
2389
|
+
event_task_id,
|
|
2390
|
+
)
|
|
2391
|
+
replay_passed_test_ids[event_task_id].update(event_passed_test_ids)
|
|
2392
|
+
if event.get("step_status") == "completed":
|
|
2393
|
+
replay_completed_steps[event_task_id].add(event_step_id)
|
|
2394
|
+
replay_failed_steps[event_task_id].discard(event_step_id)
|
|
2395
|
+
elif event.get("step_status") == "failed":
|
|
2396
|
+
replay_failed_steps[event_task_id].add(event_step_id)
|
|
2397
|
+
replay_completed_steps[event_task_id].discard(event_step_id)
|
|
2398
|
+
replay_status[event_task_id] = "blocked"
|
|
2399
|
+
continue
|
|
2400
|
+
if event_type == "dependency_status_changed":
|
|
2401
|
+
dependency_key = (
|
|
2402
|
+
str(event.get("dependency_task_id")),
|
|
2403
|
+
str(event.get("dependency_type")),
|
|
2404
|
+
)
|
|
2405
|
+
if dependency_key not in replay_dependencies[event_task_id]:
|
|
2406
|
+
continue
|
|
2407
|
+
dependency_status = event.get("dependency_status")
|
|
2408
|
+
if (
|
|
2409
|
+
event.get("design_revision") == manifest.get("revision")
|
|
2410
|
+
and dependency_status == "pending"
|
|
2411
|
+
and replay_status[event_task_id] == "completed"
|
|
2412
|
+
):
|
|
2413
|
+
_issue(
|
|
2414
|
+
issues,
|
|
2415
|
+
"execution.event_chain",
|
|
2416
|
+
f"execution.events[{event_index}] 不能直接重开 completed 任务的依赖",
|
|
2417
|
+
event_task_id,
|
|
2418
|
+
)
|
|
2419
|
+
replay_passed_test_ids[event_task_id].update(event_passed_test_ids)
|
|
2420
|
+
if dependency_status in VALID_EXECUTION_DEPENDENCY_STATUSES:
|
|
2421
|
+
replay_dependencies[event_task_id][dependency_key] = (
|
|
2422
|
+
str(dependency_status),
|
|
2423
|
+
str(event.get("event_id")) if dependency_status == "satisfied" else None,
|
|
2424
|
+
)
|
|
2425
|
+
if dependency_status == "pending" and replay_status[event_task_id] in {
|
|
2426
|
+
"in_progress",
|
|
2427
|
+
"implemented",
|
|
2428
|
+
"verified",
|
|
2429
|
+
}:
|
|
2430
|
+
replay_status[event_task_id] = "blocked"
|
|
2431
|
+
|
|
2432
|
+
for task_id, snapshot in snapshot_by_id.items():
|
|
2433
|
+
if snapshot.get("status") != replay_status[task_id]:
|
|
2434
|
+
_issue(
|
|
2435
|
+
issues,
|
|
2436
|
+
"execution.event_chain",
|
|
2437
|
+
f"{task_id}.status 与事件链重放结果 {replay_status[task_id]} 不一致",
|
|
2438
|
+
task_id,
|
|
2439
|
+
)
|
|
2440
|
+
if set(_string_list(snapshot, "completed_step_ids")) != replay_completed_steps[task_id]:
|
|
2441
|
+
_issue(
|
|
2442
|
+
issues,
|
|
2443
|
+
"execution.event_chain",
|
|
2444
|
+
f"{task_id}.completed_step_ids 与事件链不一致",
|
|
2445
|
+
task_id,
|
|
2446
|
+
)
|
|
2447
|
+
if set(_string_list(snapshot, "failed_step_ids")) != replay_failed_steps[task_id]:
|
|
2448
|
+
_issue(
|
|
2449
|
+
issues,
|
|
2450
|
+
"execution.event_chain",
|
|
2451
|
+
f"{task_id}.failed_step_ids 与事件链不一致",
|
|
2452
|
+
task_id,
|
|
2453
|
+
)
|
|
2454
|
+
actual_dependency_state = {
|
|
2455
|
+
(str(dependency.get("task_id")), str(dependency.get("type"))): (
|
|
2456
|
+
str(dependency.get("status")),
|
|
2457
|
+
dependency.get("evidence_event_id"),
|
|
2458
|
+
)
|
|
2459
|
+
for dependency in (
|
|
2460
|
+
snapshot.get("dependencies", [])
|
|
2461
|
+
if isinstance(snapshot.get("dependencies", []), list)
|
|
2462
|
+
else []
|
|
2463
|
+
)
|
|
2464
|
+
if isinstance(dependency, dict)
|
|
2465
|
+
}
|
|
2466
|
+
if actual_dependency_state != replay_dependencies[task_id]:
|
|
2467
|
+
_issue(
|
|
2468
|
+
issues,
|
|
2469
|
+
"execution.event_chain",
|
|
2470
|
+
f"{task_id}.dependencies 与事件链重放结果不一致",
|
|
2471
|
+
task_id,
|
|
2472
|
+
)
|
|
2473
|
+
for task_id, snapshot in snapshot_by_id.items():
|
|
2474
|
+
manifest_task = manifest_tasks[task_id]
|
|
2475
|
+
task_status = snapshot.get("status")
|
|
2476
|
+
completed_steps = set(_string_list(snapshot, "completed_step_ids"))
|
|
2477
|
+
failed_steps = set(_string_list(snapshot, "failed_step_ids"))
|
|
2478
|
+
expected_steps = set(_string_list(manifest_task, "step_ids"))
|
|
2479
|
+
if (
|
|
2480
|
+
manifest.get("status") != "READY" or manifest_task.get("status") != "READY"
|
|
2481
|
+
) and task_status not in {"not_started", "cancelled"}:
|
|
2482
|
+
_issue(
|
|
2483
|
+
issues,
|
|
2484
|
+
"execution.task_state",
|
|
2485
|
+
f"非 READY 设计中的任务 {task_id} 不能保存开发执行状态 {task_status}",
|
|
2486
|
+
task_id,
|
|
2487
|
+
)
|
|
2488
|
+
if task_status == "not_started" and (completed_steps or failed_steps):
|
|
2489
|
+
_issue(
|
|
2490
|
+
issues,
|
|
2491
|
+
"execution.task_state",
|
|
2492
|
+
f"not_started 任务 {task_id} 不能包含 Step 结果",
|
|
2493
|
+
task_id,
|
|
2494
|
+
)
|
|
2495
|
+
blockers = snapshot.get("blockers", [])
|
|
2496
|
+
if task_status == "blocked" and not blockers:
|
|
2497
|
+
_issue(
|
|
2498
|
+
issues,
|
|
2499
|
+
"execution.task_state",
|
|
2500
|
+
f"blocked 任务 {task_id} 必须记录 blocker",
|
|
2501
|
+
task_id,
|
|
2502
|
+
)
|
|
2503
|
+
if task_status != "blocked" and blockers:
|
|
2504
|
+
_issue(
|
|
2505
|
+
issues,
|
|
2506
|
+
"execution.task_state",
|
|
2507
|
+
f"非 blocked 任务 {task_id} 不得保留 blocker",
|
|
2508
|
+
task_id,
|
|
2509
|
+
)
|
|
2510
|
+
if task_status in {"implemented", "verified", "completed"} and (
|
|
2511
|
+
completed_steps != expected_steps or failed_steps
|
|
2512
|
+
):
|
|
2513
|
+
_issue(
|
|
2514
|
+
issues,
|
|
2515
|
+
"execution.task_state",
|
|
2516
|
+
f"任务 {task_id} 进入 {task_status} 前必须完成全部 Step 且没有失败 Step",
|
|
2517
|
+
task_id,
|
|
2518
|
+
)
|
|
2519
|
+
if task_status in {"implemented", "verified", "completed"}:
|
|
2520
|
+
passed_test_ids = {
|
|
2521
|
+
str(value.get("test_id"))
|
|
2522
|
+
for value in (
|
|
2523
|
+
snapshot.get("evidence", [])
|
|
2524
|
+
if isinstance(snapshot.get("evidence", []), list)
|
|
2525
|
+
else []
|
|
2526
|
+
)
|
|
2527
|
+
if isinstance(value, dict)
|
|
2528
|
+
and value.get("kind") == "test"
|
|
2529
|
+
and value.get("status") == "passed"
|
|
2530
|
+
}
|
|
2531
|
+
missing_test_ids = set(_string_list(manifest_task, "test_ids")) - passed_test_ids
|
|
2532
|
+
if missing_test_ids:
|
|
2533
|
+
_issue(
|
|
2534
|
+
issues,
|
|
2535
|
+
"execution.task_state",
|
|
2536
|
+
f"任务 {task_id} 进入 {task_status} 前缺少 Test 证据:{', '.join(sorted(missing_test_ids))}",
|
|
2537
|
+
task_id,
|
|
2538
|
+
)
|
|
2539
|
+
dependencies = [
|
|
2540
|
+
dependency
|
|
2541
|
+
for dependency in (
|
|
2542
|
+
snapshot.get("dependencies", [])
|
|
2543
|
+
if isinstance(snapshot.get("dependencies", []), list)
|
|
2544
|
+
else []
|
|
2545
|
+
)
|
|
2546
|
+
if isinstance(dependency, dict)
|
|
2547
|
+
]
|
|
2548
|
+
if manifest.get("status") == "READY" and any(
|
|
2549
|
+
dependency.get("type") == "contract"
|
|
2550
|
+
and dependency.get("status") != "satisfied"
|
|
2551
|
+
for dependency in dependencies
|
|
2552
|
+
):
|
|
2553
|
+
_issue(
|
|
2554
|
+
issues,
|
|
2555
|
+
"execution.dependency",
|
|
2556
|
+
f"READY 设计中的 {task_id} contract 依赖必须是 satisfied",
|
|
2557
|
+
task_id,
|
|
2558
|
+
)
|
|
2559
|
+
if task_status == "completed":
|
|
2560
|
+
unsatisfied: list[str] = []
|
|
2561
|
+
for dependency in dependencies:
|
|
2562
|
+
dependency_type = dependency.get("type")
|
|
2563
|
+
dependency_task_id = str(dependency.get("task_id"))
|
|
2564
|
+
satisfied = dependency.get("status") == "satisfied"
|
|
2565
|
+
if dependency_type == "contract":
|
|
2566
|
+
satisfied = manifest.get("status") == "READY"
|
|
2567
|
+
elif dependency_type == "hard" and not satisfied:
|
|
2568
|
+
satisfied = (
|
|
2569
|
+
snapshot_by_id.get(dependency_task_id, {}).get("status")
|
|
2570
|
+
== "completed"
|
|
2571
|
+
)
|
|
2572
|
+
if not satisfied:
|
|
2573
|
+
unsatisfied.append(f"{dependency_type}:{dependency_task_id}")
|
|
2574
|
+
if unsatisfied:
|
|
2575
|
+
_issue(
|
|
2576
|
+
issues,
|
|
2577
|
+
"execution.task_state",
|
|
2578
|
+
f"completed 任务 {task_id} 仍有未满足依赖:{', '.join(unsatisfied)}",
|
|
2579
|
+
task_id,
|
|
2580
|
+
)
|
|
2581
|
+
|
|
2582
|
+
|
|
2583
|
+
def _execution_projection(
|
|
2584
|
+
manifest: dict[str, Any],
|
|
2585
|
+
execution: dict[str, Any] | None,
|
|
2586
|
+
selected_task_ids: set[str],
|
|
2587
|
+
direct_dependency_ids: set[str],
|
|
2588
|
+
) -> dict[str, Any]:
|
|
2589
|
+
relevant_task_ids = selected_task_ids | direct_dependency_ids
|
|
2590
|
+
if execution is None:
|
|
2591
|
+
return {
|
|
2592
|
+
"schema": EXECUTION_SCHEMA,
|
|
2593
|
+
"available": False,
|
|
2594
|
+
"execution_revision": None,
|
|
2595
|
+
"updated_at": None,
|
|
2596
|
+
"tasks": [],
|
|
2597
|
+
"dependency_status": [],
|
|
2598
|
+
}
|
|
2599
|
+
snapshot_by_id = {
|
|
2600
|
+
str(snapshot["task_id"]): snapshot
|
|
2601
|
+
for snapshot in execution.get("tasks", [])
|
|
2602
|
+
if isinstance(snapshot, dict) and isinstance(snapshot.get("task_id"), str)
|
|
2603
|
+
}
|
|
2604
|
+
task_by_id = {
|
|
2605
|
+
str(task["task_id"]): task
|
|
2606
|
+
for task in manifest.get("tasks", [])
|
|
2607
|
+
if isinstance(task, dict) and isinstance(task.get("task_id"), str)
|
|
2608
|
+
}
|
|
2609
|
+
dependency_status: list[dict[str, Any]] = []
|
|
2610
|
+
for source_task_id in sorted(selected_task_ids):
|
|
2611
|
+
snapshot = snapshot_by_id.get(source_task_id, {})
|
|
2612
|
+
dependency_snapshot_by_key = {
|
|
2613
|
+
(str(value.get("task_id")), str(value.get("type"))): value
|
|
2614
|
+
for value in snapshot.get("dependencies", [])
|
|
2615
|
+
if isinstance(value, dict)
|
|
2616
|
+
}
|
|
2617
|
+
for dependency in task_by_id.get(source_task_id, {}).get("depends_on", []):
|
|
2618
|
+
dependency_task_id = str(dependency.get("task_id"))
|
|
2619
|
+
dependency_type = str(dependency.get("type"))
|
|
2620
|
+
recorded = dependency_snapshot_by_key.get((dependency_task_id, dependency_type), {})
|
|
2621
|
+
dependency_task_status = snapshot_by_id.get(dependency_task_id, {}).get("status")
|
|
2622
|
+
if dependency_type == "contract":
|
|
2623
|
+
satisfied = manifest.get("status") == "READY"
|
|
2624
|
+
basis = "design-ready" if satisfied else "design-not-ready"
|
|
2625
|
+
elif dependency_type == "hard":
|
|
2626
|
+
satisfied = (
|
|
2627
|
+
recorded.get("status") == "satisfied"
|
|
2628
|
+
or dependency_task_status == "completed"
|
|
2629
|
+
)
|
|
2630
|
+
basis = (
|
|
2631
|
+
"recorded-evidence"
|
|
2632
|
+
if recorded.get("status") == "satisfied"
|
|
2633
|
+
else "dependency-task-completed"
|
|
2634
|
+
if dependency_task_status == "completed"
|
|
2635
|
+
else "pending"
|
|
2636
|
+
)
|
|
2637
|
+
else:
|
|
2638
|
+
satisfied = recorded.get("status") == "satisfied"
|
|
2639
|
+
basis = "recorded-evidence" if satisfied else "pending-integration"
|
|
2640
|
+
dependency_status.append(
|
|
2641
|
+
{
|
|
2642
|
+
"source_task_id": source_task_id,
|
|
2643
|
+
"task_id": dependency_task_id,
|
|
2644
|
+
"type": dependency_type,
|
|
2645
|
+
"status": "satisfied" if satisfied else "pending",
|
|
2646
|
+
"basis": basis,
|
|
2647
|
+
"required_evidence": dependency.get("required_evidence"),
|
|
2648
|
+
"evidence_event_id": recorded.get("evidence_event_id"),
|
|
2649
|
+
}
|
|
2650
|
+
)
|
|
2651
|
+
return {
|
|
2652
|
+
"schema": EXECUTION_SCHEMA,
|
|
2653
|
+
"available": True,
|
|
2654
|
+
"execution_revision": execution.get("execution_revision"),
|
|
2655
|
+
"updated_at": execution.get("updated_at"),
|
|
2656
|
+
"tasks": [
|
|
2657
|
+
snapshot_by_id[task_id]
|
|
2658
|
+
for task_id in sorted(relevant_task_ids)
|
|
2659
|
+
if task_id in snapshot_by_id
|
|
2660
|
+
],
|
|
2661
|
+
"dependency_status": dependency_status,
|
|
2662
|
+
}
|
|
2663
|
+
|
|
2664
|
+
|
|
1378
2665
|
def validate_model(
|
|
1379
2666
|
manifest: dict[str, Any],
|
|
1380
2667
|
sections: dict[str, Section] | dict[str, str],
|
|
@@ -1666,16 +2953,32 @@ def validate_model(
|
|
|
1666
2953
|
if not _has_labeled_value(contract_body, marker):
|
|
1667
2954
|
_issue(issues, "java.contract", f"{task_id} 的 Java 契约缺少带实际值的字段:{marker}", task_id)
|
|
1668
2955
|
|
|
2956
|
+
deliverable_text = "\n".join(
|
|
2957
|
+
(
|
|
2958
|
+
text,
|
|
2959
|
+
json.dumps(manifest, ensure_ascii=False, sort_keys=True),
|
|
2960
|
+
*(section.content for section in section_objects.values()),
|
|
2961
|
+
)
|
|
2962
|
+
)
|
|
2963
|
+
for code, pattern in FORBIDDEN_READY_PATTERNS.items():
|
|
2964
|
+
match = pattern.search(deliverable_text)
|
|
2965
|
+
if match and status in {"DRAFT", "BLOCKED"}:
|
|
2966
|
+
_issue(
|
|
2967
|
+
issues,
|
|
2968
|
+
f"deliverable.{code}",
|
|
2969
|
+
f"{status} 文档也不得包含留白或未展开表达:{match.group(0)}",
|
|
2970
|
+
)
|
|
2971
|
+
|
|
2972
|
+
blocked_requested = status == "BLOCKED" or any(
|
|
2973
|
+
task.get("status") == "BLOCKED" for task in task_by_id.values()
|
|
2974
|
+
)
|
|
2975
|
+
if blocked_requested:
|
|
2976
|
+
_validate_blocked_evidence(section_objects, issues)
|
|
2977
|
+
|
|
1669
2978
|
if require_ready and status != "READY":
|
|
1670
2979
|
_issue(issues, "ready.required", f"要求 READY,但文档状态是 {status!r}", "status")
|
|
1671
2980
|
if ready_requested:
|
|
1672
|
-
ready_text =
|
|
1673
|
-
(
|
|
1674
|
-
text,
|
|
1675
|
-
json.dumps(manifest, ensure_ascii=False, sort_keys=True),
|
|
1676
|
-
*(section.content for section in section_objects.values()),
|
|
1677
|
-
)
|
|
1678
|
-
)
|
|
2981
|
+
ready_text = deliverable_text
|
|
1679
2982
|
_validate_ready_manifest_values(
|
|
1680
2983
|
manifest,
|
|
1681
2984
|
repositories,
|
|
@@ -1742,16 +3045,33 @@ def validate_model(
|
|
|
1742
3045
|
)
|
|
1743
3046
|
|
|
1744
3047
|
|
|
1745
|
-
def validate_spec(
|
|
3048
|
+
def validate_spec(
|
|
3049
|
+
source: str | Path,
|
|
3050
|
+
require_ready: bool = False,
|
|
3051
|
+
require_execution: bool = False,
|
|
3052
|
+
) -> ValidationReport:
|
|
1746
3053
|
"""Validate a path or document string and return a structured report."""
|
|
1747
3054
|
|
|
1748
3055
|
text, _ = _read_source(source)
|
|
3056
|
+
document_digest = hashlib.sha256(text.encode("utf-8")).hexdigest()
|
|
3057
|
+
try:
|
|
3058
|
+
design_text, execution = split_execution_region(text)
|
|
3059
|
+
except CanonicalSpecError as exc:
|
|
3060
|
+
return ValidationReport(
|
|
3061
|
+
protocol="canonical-invalid",
|
|
3062
|
+
status=None,
|
|
3063
|
+
document_sha256=document_digest,
|
|
3064
|
+
issues=[ValidationIssue("execution.parse", str(exc))],
|
|
3065
|
+
)
|
|
3066
|
+
design_digest = hashlib.sha256(design_text.encode("utf-8")).hexdigest()
|
|
1749
3067
|
try:
|
|
1750
|
-
manifest = parse_manifest(
|
|
3068
|
+
manifest = parse_manifest(design_text)
|
|
1751
3069
|
except CanonicalSpecError as exc:
|
|
1752
3070
|
return ValidationReport(
|
|
1753
3071
|
protocol="canonical-invalid",
|
|
1754
3072
|
status=None,
|
|
3073
|
+
design_sha256=design_digest,
|
|
3074
|
+
document_sha256=document_digest,
|
|
1755
3075
|
issues=[ValidationIssue("manifest.parse", str(exc))],
|
|
1756
3076
|
)
|
|
1757
3077
|
if manifest is None:
|
|
@@ -1760,29 +3080,68 @@ def validate_spec(source: str | Path, require_ready: bool = False) -> Validation
|
|
|
1760
3080
|
issues.append(
|
|
1761
3081
|
ValidationIssue("ready.legacy", "legacy Dev Spec 不具备 Canonical v1 READY 证明")
|
|
1762
3082
|
)
|
|
1763
|
-
|
|
3083
|
+
if require_execution:
|
|
3084
|
+
issues.append(
|
|
3085
|
+
ValidationIssue("execution.legacy", "legacy Dev Spec 不支持共享执行状态")
|
|
3086
|
+
)
|
|
3087
|
+
return ValidationReport(
|
|
3088
|
+
protocol="legacy",
|
|
3089
|
+
status=None,
|
|
3090
|
+
design_sha256=design_digest,
|
|
3091
|
+
document_sha256=document_digest,
|
|
3092
|
+
issues=issues,
|
|
3093
|
+
)
|
|
1764
3094
|
try:
|
|
1765
|
-
sections = _parse_section_objects(
|
|
3095
|
+
sections = _parse_section_objects(design_text)
|
|
1766
3096
|
except CanonicalSpecError as exc:
|
|
1767
3097
|
return ValidationReport(
|
|
1768
3098
|
protocol="canonical-v1",
|
|
1769
3099
|
status=manifest.get("status") if isinstance(manifest.get("status"), str) else None,
|
|
1770
3100
|
manifest=manifest,
|
|
3101
|
+
execution=execution,
|
|
3102
|
+
design_sha256=design_digest,
|
|
3103
|
+
document_sha256=document_digest,
|
|
1771
3104
|
issues=[ValidationIssue("section.parse", str(exc))],
|
|
1772
3105
|
)
|
|
1773
|
-
model_report = validate_model(
|
|
3106
|
+
model_report = validate_model(
|
|
3107
|
+
manifest,
|
|
3108
|
+
sections,
|
|
3109
|
+
text=design_text,
|
|
3110
|
+
require_ready=require_ready,
|
|
3111
|
+
)
|
|
1774
3112
|
issues = model_report.issues
|
|
1775
|
-
first_section =
|
|
1776
|
-
if first_section != -1 and
|
|
3113
|
+
first_section = design_text.find("<!-- EDS:SECTION:BEGIN")
|
|
3114
|
+
if first_section != -1 and design_text.find(MANIFEST_BEGIN) > first_section:
|
|
1777
3115
|
issues.append(
|
|
1778
3116
|
ValidationIssue("manifest.position", "manifest 必须位于所有 EDS 正文区域之前")
|
|
1779
3117
|
)
|
|
3118
|
+
warnings: list[ValidationIssue] = []
|
|
3119
|
+
if execution is None:
|
|
3120
|
+
warnings.append(
|
|
3121
|
+
ValidationIssue(
|
|
3122
|
+
"execution.missing",
|
|
3123
|
+
"文档未初始化共享执行状态;旧版 Canonical v1 仍可只读消费",
|
|
3124
|
+
)
|
|
3125
|
+
)
|
|
3126
|
+
if require_execution:
|
|
3127
|
+
issues.append(
|
|
3128
|
+
ValidationIssue(
|
|
3129
|
+
"execution.required",
|
|
3130
|
+
"要求共享执行状态,但文档缺少 EDS:EXECUTION 区域",
|
|
3131
|
+
)
|
|
3132
|
+
)
|
|
3133
|
+
else:
|
|
3134
|
+
_validate_execution_state(manifest, execution, design_digest, issues)
|
|
1780
3135
|
return ValidationReport(
|
|
1781
3136
|
protocol="canonical-v1",
|
|
1782
3137
|
status=manifest.get("status") if isinstance(manifest.get("status"), str) else None,
|
|
1783
3138
|
manifest=manifest,
|
|
1784
3139
|
sections=sections,
|
|
3140
|
+
execution=execution,
|
|
3141
|
+
design_sha256=design_digest,
|
|
3142
|
+
document_sha256=document_digest,
|
|
1785
3143
|
issues=issues,
|
|
3144
|
+
warnings=warnings,
|
|
1786
3145
|
)
|
|
1787
3146
|
|
|
1788
3147
|
|
|
@@ -1884,13 +3243,17 @@ def select_scope(
|
|
|
1884
3243
|
selected_steps = [item for item in manifest["steps"] if item.get("task_id") in selected_set]
|
|
1885
3244
|
selected_tests = [item for item in manifest["tests"] if item.get("task_id") in selected_set]
|
|
1886
3245
|
related_contracts = [contract_by_id[contract_id] for contract_id in related_contract_ids]
|
|
1887
|
-
|
|
3246
|
+
execution = _execution_projection(
|
|
3247
|
+
manifest,
|
|
3248
|
+
report.execution,
|
|
3249
|
+
selected_set,
|
|
3250
|
+
direct_dependency_ids,
|
|
3251
|
+
)
|
|
3252
|
+
static_scope: dict[str, Any] = {
|
|
1888
3253
|
"schema": SCHEMA,
|
|
1889
3254
|
"spec_id": manifest["spec_id"],
|
|
1890
3255
|
"revision": manifest["revision"],
|
|
1891
3256
|
"status": manifest["status"],
|
|
1892
|
-
"source_path": source_path,
|
|
1893
|
-
"source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
1894
3257
|
"repo": repo_by_id[repo_id],
|
|
1895
3258
|
"selected_task_ids": selected_ids,
|
|
1896
3259
|
"selected_tasks": selected_tasks,
|
|
@@ -1908,6 +3271,32 @@ def select_scope(
|
|
|
1908
3271
|
sections["integration-plan"].content, relevant_task_ids
|
|
1909
3272
|
),
|
|
1910
3273
|
}
|
|
3274
|
+
design_scope_digest = hashlib.sha256(
|
|
3275
|
+
json.dumps(
|
|
3276
|
+
static_scope,
|
|
3277
|
+
ensure_ascii=False,
|
|
3278
|
+
sort_keys=True,
|
|
3279
|
+
separators=(",", ":"),
|
|
3280
|
+
).encode("utf-8")
|
|
3281
|
+
).hexdigest()
|
|
3282
|
+
execution_scope_digest = hashlib.sha256(
|
|
3283
|
+
json.dumps(
|
|
3284
|
+
execution,
|
|
3285
|
+
ensure_ascii=False,
|
|
3286
|
+
sort_keys=True,
|
|
3287
|
+
separators=(",", ":"),
|
|
3288
|
+
).encode("utf-8")
|
|
3289
|
+
).hexdigest()
|
|
3290
|
+
payload: dict[str, Any] = {
|
|
3291
|
+
**static_scope,
|
|
3292
|
+
"source_path": source_path,
|
|
3293
|
+
"source_sha256": hashlib.sha256(text.encode("utf-8")).hexdigest(),
|
|
3294
|
+
"document_sha256": report.document_sha256,
|
|
3295
|
+
"design_sha256": report.design_sha256,
|
|
3296
|
+
"design_scope_sha256": design_scope_digest,
|
|
3297
|
+
"execution_scope_sha256": execution_scope_digest,
|
|
3298
|
+
"execution": execution,
|
|
3299
|
+
}
|
|
1911
3300
|
if output_format == "json":
|
|
1912
3301
|
return payload
|
|
1913
3302
|
if output_format != "markdown":
|
|
@@ -1922,6 +3311,10 @@ def select_scope(
|
|
|
1922
3311
|
"status",
|
|
1923
3312
|
"source_path",
|
|
1924
3313
|
"source_sha256",
|
|
3314
|
+
"document_sha256",
|
|
3315
|
+
"design_sha256",
|
|
3316
|
+
"design_scope_sha256",
|
|
3317
|
+
"execution_scope_sha256",
|
|
1925
3318
|
"repo",
|
|
1926
3319
|
"selected_task_ids",
|
|
1927
3320
|
"selected_tasks",
|
|
@@ -1931,6 +3324,7 @@ def select_scope(
|
|
|
1931
3324
|
"direct_dependency_summaries",
|
|
1932
3325
|
"related_contract_ids",
|
|
1933
3326
|
"related_contracts",
|
|
3327
|
+
"execution",
|
|
1934
3328
|
)
|
|
1935
3329
|
}
|
|
1936
3330
|
manifest_json = json.dumps(routing_manifest, ensure_ascii=False, indent=2)
|
|
@@ -1943,7 +3337,10 @@ def select_scope(
|
|
|
1943
3337
|
"```",
|
|
1944
3338
|
"<!-- EDS:CONSUMPTION-SCOPE:END -->",
|
|
1945
3339
|
"",
|
|
1946
|
-
f">
|
|
3340
|
+
f"> 文档 SHA-256:`{payload['document_sha256']}`",
|
|
3341
|
+
f"> 设计 SHA-256:`{payload['design_sha256']}`",
|
|
3342
|
+
f"> 设计范围 SHA-256:`{payload['design_scope_sha256']}`",
|
|
3343
|
+
f"> 执行修订:`{payload['execution']['execution_revision']}`",
|
|
1947
3344
|
f"> 选择任务:{', '.join(selected_ids)}",
|
|
1948
3345
|
"",
|
|
1949
3346
|
]
|
|
@@ -1961,6 +3358,17 @@ def select_scope(
|
|
|
1961
3358
|
)
|
|
1962
3359
|
else:
|
|
1963
3360
|
output.append("- 无直接依赖任务。")
|
|
3361
|
+
execution_json = json.dumps(execution, ensure_ascii=False, indent=2)
|
|
3362
|
+
output.extend(
|
|
3363
|
+
[
|
|
3364
|
+
"",
|
|
3365
|
+
"## 共享执行状态",
|
|
3366
|
+
"",
|
|
3367
|
+
"```json",
|
|
3368
|
+
execution_json,
|
|
3369
|
+
"```",
|
|
3370
|
+
]
|
|
3371
|
+
)
|
|
1964
3372
|
integration = Section(
|
|
1965
3373
|
section_id="integration-plan",
|
|
1966
3374
|
content=payload["integration_plan"],
|