cowork-flow 0.0.32 → 0.0.34
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/package.json +1 -1
- package/template/.claude/agents/cowork-check.md +1 -1
- package/template/.claude/agents/cowork-implement.md +2 -2
- package/template/.cowork-flow/.version +1 -1
- package/template/.cowork-flow/scripts/commands/task.py +75 -6
- package/template/.cowork-flow/scripts/common/core/execution_context.py +2 -2
- package/template/.cowork-flow/scripts/common/core/files.py +8 -0
- package/template/.cowork-flow/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.cowork-flow/scripts/common/gates/gates.py +5 -0
- package/template/.cowork-flow/scripts/common/gates/spec_validator.py +53 -0
- package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +10 -8
- package/template/.cowork-flow/scripts/common/gates/test_intent.py +1 -5
- package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +170 -131
- package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +40 -15
- package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +32 -0
- package/template/.cowork-flow/scripts/common/gates/validate_rules.py +1 -12
- package/template/.cowork-flow/scripts/common/git/git_context.py +4 -4
- package/template/.cowork-flow/scripts/common/task/batch_mode.py +145 -0
- package/template/.cowork-flow/scripts/common/task/readiness.py +29 -11
- package/template/.cowork-flow/spec/backend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
- package/template/.cowork-flow/spec/contracts/error-output-as-data.md +54 -0
- package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.cowork-flow/spec/frontend/quality-guidelines.md +2 -0
- package/template/.cowork-flow/spec/index.md +16 -0
- package/template/.cowork-flow/spec/references/definition-of-done.md +45 -0
- package/template/.cowork-flow/spec/references/index.md +20 -0
- package/template/.cowork-flow/spec/references/orchestration-patterns.md +139 -0
- package/template/.cowork-flow/spec/references/security-checklist.md +51 -0
- package/template/.cowork-flow/spec/references/testing-checklist.md +45 -0
- package/template/.cowork-flow/spec/runtime/contract-registry.json +57 -0
- package/template/.cowork-flow/spec/runtime/rules.json +60 -0
- package/template/.cowork-flow/spec/schemas/rules.schema.json +1 -1
- package/template/.cowork-flow/workflow.md +9 -4
- package/template/.opencode/agents/cowork-check.md +1 -1
- package/template/.opencode/agents/cowork-implement.md +2 -2
- package/template/.opencode/agents/cowork-research.md +1 -1
- package/template/.zcode/.zcode-plugin/plugin.json +1 -1
- package/template/.zcode/hooks/runtime/scripts/commands/task.py +47 -5
- package/template/.zcode/hooks/runtime/scripts/common/core/execution_context.py +2 -2
- package/template/.zcode/hooks/runtime/scripts/common/core/files.py +8 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/anti_rationalization.py +55 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/gates.py +5 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/tdd_evidence.py +10 -8
- package/template/.zcode/hooks/runtime/scripts/common/gates/test_intent.py +1 -5
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standards.py +294 -133
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_implementation.py +40 -15
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_rules.py +2 -13
- package/template/.zcode/hooks/runtime/scripts/common/git/git_context.py +4 -4
- package/template/.zcode/hooks/runtime/scripts/common/task/readiness.py +11 -11
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +68 -0
- package/template/.zcode/scaffold/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
- package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +60 -0
- package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +1 -1
- package/template/.zcode/scaffold/.cowork-flow/workflow.md +9 -4
- package/template/AGENTS.md +9 -0
- package/template/skills/batch-mode/SKILL.md +117 -0
- package/template/skills/before-dev/SKILL.md +11 -3
- package/template/skills/break-loop/SKILL.md +53 -0
- package/template/skills/check/SKILL.md +63 -0
- package/template/skills/continue/SKILL.md +1 -1
- package/template/skills/doubt-review/SKILL.md +118 -0
- package/template/skills/tdd/SKILL.md +13 -0
- package/template/skills/writing-plans/SKILL.md +23 -0
- package/template/.zcode/hooks/runtime/scripts/common/gates/validate_coding_standaries.py +0 -316
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ the task.
|
|
|
35
35
|
|
|
36
36
|
Load context before checking:
|
|
37
37
|
1. Read the task directory from the bound runtime context.
|
|
38
|
-
2. Read `<task>/
|
|
38
|
+
2. Read `<task>/decision-anchor.md`.
|
|
39
39
|
3. Read the plan file linked from this task (check `<task>/task.json` `relatedFiles` for a plan path, or search `.cowork-flow/plans/` for a plan file referencing this task directory). Use the plan's Verify commands and Expected results as the checklist for each step.
|
|
40
40
|
4. Read `<task>/check.jsonl`.
|
|
41
41
|
5. Read `git diff`.
|
|
@@ -35,8 +35,8 @@ the task.
|
|
|
35
35
|
|
|
36
36
|
Load context before editing:
|
|
37
37
|
1. Read the task directory from the bound runtime context.
|
|
38
|
-
2. Read `<task>/
|
|
39
|
-
3. Read
|
|
38
|
+
2. Read `<task>/decision-anchor.md`.
|
|
39
|
+
3. Read .
|
|
40
40
|
4. Read `<task>/implement.jsonl`.
|
|
41
41
|
5. Read each JSONL `file` entry.
|
|
42
42
|
6. Read the plan file linked from this task (check `<task>/task.json` `relatedFiles` for a plan path, or search `.cowork-flow/plans/` for a plan file referencing this task directory). Follow the plan steps in order — each step has Files, Action, Verify, and Expected fields.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.0.
|
|
1
|
+
0.0.34
|
|
@@ -485,9 +485,13 @@ def _task_start_blockers(task_dir: Path) -> list[str]:
|
|
|
485
485
|
if not task_json.is_file():
|
|
486
486
|
blockers.append("task.json is missing")
|
|
487
487
|
|
|
488
|
-
|
|
489
|
-
if
|
|
490
|
-
|
|
488
|
+
# 只检查存在性 — R-WF-008 负责章节内容完整性
|
|
489
|
+
if _read_text_if_present(task_dir / "prd.md"):
|
|
490
|
+
# 旧格式:提示需要迁移(cmd_start 会自动迁移)
|
|
491
|
+
if not _read_text_if_present(task_dir / "decision-anchor.md"):
|
|
492
|
+
blockers.append("prd.md found; task start will auto-migrate to decision-anchor.md")
|
|
493
|
+
elif not _read_text_if_present(task_dir / "decision-anchor.md"):
|
|
494
|
+
blockers.append("decision-anchor.md is missing or empty")
|
|
491
495
|
|
|
492
496
|
for jsonl_name in CONTEXT_JSONL_FILES:
|
|
493
497
|
jsonl_file = task_dir / jsonl_name
|
|
@@ -661,10 +665,29 @@ def cmd_create(args: argparse.Namespace) -> int:
|
|
|
661
665
|
|
|
662
666
|
print(colored(f"Linked as child of: {parent_dir.name}", Colors.GREEN), file=sys.stderr)
|
|
663
667
|
|
|
668
|
+
# --from-plan: 自动生成 decision-anchor.md 骨架
|
|
669
|
+
if getattr(args, "from_plan", None):
|
|
670
|
+
plan_path = Path(args.from_plan)
|
|
671
|
+
if plan_path.is_file():
|
|
672
|
+
plan_text = plan_path.read_text(encoding="utf-8")
|
|
673
|
+
goal = next(
|
|
674
|
+
(line.split(":", 1)[1].strip() for line in plan_text.splitlines()
|
|
675
|
+
if line.startswith("**Goal:**") or line.startswith("**目标:**")),
|
|
676
|
+
None,
|
|
677
|
+
)
|
|
678
|
+
if goal:
|
|
679
|
+
anchor_file = task_dir / "decision-anchor.md"
|
|
680
|
+
if not anchor_file.exists():
|
|
681
|
+
anchor_file.write_text(
|
|
682
|
+
f"## 目标\n\n{goal}\n\n## 验收标准\n- [ ] \n",
|
|
683
|
+
encoding="utf-8",
|
|
684
|
+
)
|
|
685
|
+
print(f" {colored('[OK]', Colors.GREEN)} Generated decision-anchor.md from plan")
|
|
686
|
+
|
|
664
687
|
print(colored(f"Created task: {dir_name}", Colors.GREEN), file=sys.stderr)
|
|
665
688
|
print("", file=sys.stderr)
|
|
666
689
|
print(colored("Next steps:", Colors.BLUE), file=sys.stderr)
|
|
667
|
-
print(" 1. Create
|
|
690
|
+
print(" 1. Create decision-anchor.md with requirements", file=sys.stderr)
|
|
668
691
|
print(" 2. Run: ./.cowork-flow/run task init-context <dir> <dev_type>", file=sys.stderr)
|
|
669
692
|
print(" 3. Run: ./.cowork-flow/run task start <dir>", file=sys.stderr)
|
|
670
693
|
print("", file=sys.stderr)
|
|
@@ -961,11 +984,29 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
961
984
|
for blocker in blockers:
|
|
962
985
|
print(f" - {blocker}", file=sys.stderr)
|
|
963
986
|
print(
|
|
964
|
-
"Hint: write
|
|
987
|
+
"Hint: write decision-anchor.md, run ./.cowork-flow/run task init-context <dir> <dev_type>, then retry",
|
|
965
988
|
file=sys.stderr,
|
|
966
989
|
)
|
|
967
990
|
return 1
|
|
968
991
|
|
|
992
|
+
# 自动迁移旧 prd.md 到 decision-anchor.md
|
|
993
|
+
prd_file = full_path / "prd.md"
|
|
994
|
+
anchor_file = full_path / "decision-anchor.md"
|
|
995
|
+
if prd_file.exists() and not anchor_file.exists():
|
|
996
|
+
content = prd_file.read_text(encoding="utf-8").strip()
|
|
997
|
+
if not content:
|
|
998
|
+
content = "(empty legacy prd.md)"
|
|
999
|
+
if "## 目标" not in content and "## Goal" not in content:
|
|
1000
|
+
content = "## 目标\n\n" + content
|
|
1001
|
+
if "## 验收标准" not in content and "## Acceptance" not in content:
|
|
1002
|
+
content += "\n\n## 验收标准\n- [ ] \n"
|
|
1003
|
+
anchor_file.write_text(content, encoding="utf-8")
|
|
1004
|
+
prd_file.unlink()
|
|
1005
|
+
print(
|
|
1006
|
+
colored(" [迁移] prd.md → decision-anchor.md", Colors.YELLOW),
|
|
1007
|
+
file=sys.stderr,
|
|
1008
|
+
)
|
|
1009
|
+
|
|
969
1010
|
validation_issues = _task_context_validation_issues(full_path, repo_root)
|
|
970
1011
|
if validation_issues:
|
|
971
1012
|
print(colored("Error: Task context validation failed", Colors.RED), file=sys.stderr)
|
|
@@ -988,6 +1029,10 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
988
1029
|
)
|
|
989
1030
|
return 1
|
|
990
1031
|
|
|
1032
|
+
if getattr(args, "auto", False):
|
|
1033
|
+
from common.task.batch_mode import run_batch_entry
|
|
1034
|
+
return run_batch_entry(repo_root, full_path, args)
|
|
1035
|
+
|
|
991
1036
|
state_blockers = transition_blockers(_load_task_status(full_path), "in_progress")
|
|
992
1037
|
if state_blockers:
|
|
993
1038
|
_print_transition_blockers(state_blockers)
|
|
@@ -1351,6 +1396,17 @@ def _print_blockers(blockers: list[str]) -> None:
|
|
|
1351
1396
|
print(f" - {blocker}")
|
|
1352
1397
|
|
|
1353
1398
|
|
|
1399
|
+
def _is_git_dirty(repo_root: Path) -> bool:
|
|
1400
|
+
"""Check if the git working tree has uncommitted changes."""
|
|
1401
|
+
try:
|
|
1402
|
+
rc, _, _ = _run_git_command(
|
|
1403
|
+
["status", "--porcelain"], cwd=repo_root
|
|
1404
|
+
)
|
|
1405
|
+
return rc != 0
|
|
1406
|
+
except (OSError, Exception):
|
|
1407
|
+
return False
|
|
1408
|
+
|
|
1409
|
+
|
|
1354
1410
|
def _linked_active_changes_for_task(repo_root: Path, task_dir: Path) -> list[str]:
|
|
1355
1411
|
from commands.change import linked_active_changes_for_task
|
|
1356
1412
|
|
|
@@ -1479,7 +1535,7 @@ def cmd_next(args: argparse.Namespace) -> int:
|
|
|
1479
1535
|
return 0
|
|
1480
1536
|
|
|
1481
1537
|
if status in DONE_STATUSES:
|
|
1482
|
-
print("Next action: finalize,
|
|
1538
|
+
print("Next action: finalize, archive, commit, and record session")
|
|
1483
1539
|
print("Command: git status --short")
|
|
1484
1540
|
linked_changes = _linked_active_changes_for_task(repo_root, task_dir)
|
|
1485
1541
|
print(f"Then: ./.cowork-flow/run task archive {Path(task_path).name}")
|
|
@@ -1546,6 +1602,14 @@ def cmd_archive(args: argparse.Namespace) -> int:
|
|
|
1546
1602
|
if linked_changes and not _linked_changes_ready_for_archive(repo_root, linked_changes):
|
|
1547
1603
|
return 1
|
|
1548
1604
|
|
|
1605
|
+
# R-WF-011: warn if uncommitted changes detected (archive should come before commit)
|
|
1606
|
+
if _is_git_dirty(repo_root):
|
|
1607
|
+
print(colored(
|
|
1608
|
+
"Warning: Uncommitted changes detected. "
|
|
1609
|
+
"Archive the task first, then commit the archived result.",
|
|
1610
|
+
Colors.YELLOW,
|
|
1611
|
+
), file=sys.stderr)
|
|
1612
|
+
|
|
1549
1613
|
# Archive
|
|
1550
1614
|
result = archive_task_complete(task_dir, repo_root)
|
|
1551
1615
|
if "archived_to" not in result:
|
|
@@ -1914,6 +1978,7 @@ def main() -> int:
|
|
|
1914
1978
|
p_create.add_argument("--priority", "-p", default="P2", help="Priority (P0-P3)")
|
|
1915
1979
|
p_create.add_argument("--description", "-d", help="Task description")
|
|
1916
1980
|
p_create.add_argument("--parent", help="Parent task directory (establishes subtask link)")
|
|
1981
|
+
p_create.add_argument("--from-plan", "-f", help="Path to plan file (auto-generate decision-anchor skeleton)")
|
|
1917
1982
|
|
|
1918
1983
|
# init-context
|
|
1919
1984
|
p_init = subparsers.add_parser("init-context", help="Initialize context files")
|
|
@@ -1938,6 +2003,10 @@ def main() -> int:
|
|
|
1938
2003
|
# start
|
|
1939
2004
|
p_start = subparsers.add_parser("start", help="Set active session task")
|
|
1940
2005
|
p_start.add_argument("dir", help="Task directory")
|
|
2006
|
+
p_start.add_argument("--auto", action="store_true",
|
|
2007
|
+
help="Enable batch mode (requires --approved)")
|
|
2008
|
+
p_start.add_argument("--approved", action="store_true",
|
|
2009
|
+
help="User has approved the plan")
|
|
1941
2010
|
|
|
1942
2011
|
# current
|
|
1943
2012
|
subparsers.add_parser("current", help="Show active session task")
|
|
@@ -268,8 +268,8 @@ def build_worker_resume_text(
|
|
|
268
268
|
if isinstance(file_value, str) and file_value.strip():
|
|
269
269
|
suffix = f" - {reason}" if isinstance(reason, str) and reason.strip() else ""
|
|
270
270
|
lines.append(f"- {file_value}{suffix}")
|
|
271
|
-
if context.task_dir and (repo_root / context.task_dir / "
|
|
272
|
-
lines.append(f"- Read task PRD: {context.task_dir}/
|
|
271
|
+
if context.task_dir and (repo_root / context.task_dir / "decision-anchor.md").is_file():
|
|
272
|
+
lines.append(f"- Read task PRD: {context.task_dir}/decision-anchor.md")
|
|
273
273
|
lines.append("- Follow only the files, steps, and commands named in the worker brief.")
|
|
274
274
|
|
|
275
275
|
forbidden_actions = context_data.get("forbiddenActions")
|
|
@@ -25,6 +25,14 @@ def read_json_file(path: Path) -> dict | None:
|
|
|
25
25
|
return None
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def read_text_utf8(path: Path) -> str:
|
|
29
|
+
"""Read a text file as UTF-8; return "" if missing or unreadable."""
|
|
30
|
+
try:
|
|
31
|
+
return path.read_text(encoding="utf-8")
|
|
32
|
+
except (OSError, UnicodeDecodeError):
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
|
|
28
36
|
def write_json_file(path: Path, data: dict) -> bool:
|
|
29
37
|
"""Write dict to JSON file atomically (temp + os.replace)."""
|
|
30
38
|
try:
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Anti-rationalization pattern detection for TDD exemptions."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
RATIONALIZATION_PATTERNS = (
|
|
7
|
+
"太简单",
|
|
8
|
+
"显而易",
|
|
9
|
+
"没必要",
|
|
10
|
+
"不需要测试",
|
|
11
|
+
"已经有其他测试",
|
|
12
|
+
"肉眼",
|
|
13
|
+
"self-evident",
|
|
14
|
+
"trivial",
|
|
15
|
+
"obvious",
|
|
16
|
+
"no need to test",
|
|
17
|
+
"covered elsewhere",
|
|
18
|
+
"already tested",
|
|
19
|
+
"just a",
|
|
20
|
+
"only a",
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def validate_exemption_rationalization(reason: str) -> list[dict]:
|
|
25
|
+
"""Detect anti-rationalization patterns in TDD exemption reasons.
|
|
26
|
+
|
|
27
|
+
Returns violation dicts with rule_id TDD-RATIONALIZE-001.
|
|
28
|
+
Severity is warn (does not block task start/review).
|
|
29
|
+
"""
|
|
30
|
+
violations: list[dict] = []
|
|
31
|
+
if not reason:
|
|
32
|
+
return violations
|
|
33
|
+
lower = reason.lower()
|
|
34
|
+
for pattern in RATIONALIZATION_PATTERNS:
|
|
35
|
+
if pattern.lower() in lower:
|
|
36
|
+
violations.append({
|
|
37
|
+
"rule_id": "TDD-RATIONALIZE-001",
|
|
38
|
+
"type": "tdd_evidence",
|
|
39
|
+
"severity": "warn",
|
|
40
|
+
"passed": False,
|
|
41
|
+
"message": (
|
|
42
|
+
f"TDD exemption reason contains rationalization pattern: "
|
|
43
|
+
f"'{pattern}'. Replace with a concrete behavior test or "
|
|
44
|
+
f"specific verification command."
|
|
45
|
+
),
|
|
46
|
+
"file": "<task>/tdd.jsonl",
|
|
47
|
+
"fix_hint": (
|
|
48
|
+
"Remove rationalization from exemption reason. "
|
|
49
|
+
"Either write a failing behavior test for this AC "
|
|
50
|
+
"or provide a verification command that exercises "
|
|
51
|
+
"the claimed unchanged behavior."
|
|
52
|
+
),
|
|
53
|
+
})
|
|
54
|
+
break
|
|
55
|
+
return violations
|
|
@@ -58,6 +58,11 @@ class GateRunner:
|
|
|
58
58
|
violations.extend(
|
|
59
59
|
validate_coding_standards(self.repo_root, normalized_task_dir)
|
|
60
60
|
)
|
|
61
|
+
from .validate_coding_standards import validate_complexity_signals
|
|
62
|
+
|
|
63
|
+
violations.extend(
|
|
64
|
+
validate_complexity_signals(self.repo_root, normalized_task_dir)
|
|
65
|
+
)
|
|
61
66
|
return GateResult.from_violations(scope, violations, normalized_task_dir)
|
|
62
67
|
|
|
63
68
|
def log(self, result: GateResult) -> None:
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Spec markdown section validator for cowork-flow."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import re
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
SECTION_CONFIG: dict[str, tuple[str, ...]] = {
|
|
10
|
+
"contract": ("Goal", "When to Use", "Rules", "Examples"),
|
|
11
|
+
"skill": ("Overview", "When to Use", "Steps", "Verification"),
|
|
12
|
+
"guide": ("Goal", "When to Use", "Process", "Anti-Rationalization"),
|
|
13
|
+
"default": ("Goal", "Rules"),
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
HEADER_RE = re.compile(r"^##\s+(.+)$", re.MULTILINE)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def validate_sections(file_path: Path | str, spec_type: str = "default") -> tuple[bool, list[str]]:
|
|
20
|
+
"""Validate a spec markdown file has all required sections.
|
|
21
|
+
|
|
22
|
+
Returns (is_valid, error_messages).
|
|
23
|
+
"""
|
|
24
|
+
path = Path(file_path)
|
|
25
|
+
errors: list[str] = []
|
|
26
|
+
|
|
27
|
+
if not path.is_file():
|
|
28
|
+
return False, [f"File not found: {path}"]
|
|
29
|
+
|
|
30
|
+
content = path.read_text(encoding="utf-8").strip()
|
|
31
|
+
if not content:
|
|
32
|
+
return False, ["File is empty"]
|
|
33
|
+
|
|
34
|
+
required = SECTION_CONFIG.get(spec_type)
|
|
35
|
+
if required is None:
|
|
36
|
+
return False, [f"Unknown spec type: {spec_type}"]
|
|
37
|
+
|
|
38
|
+
matches = list(HEADER_RE.finditer(content))
|
|
39
|
+
found: dict[str, str] = {}
|
|
40
|
+
for i, m in enumerate(matches):
|
|
41
|
+
name = m.group(1).strip()
|
|
42
|
+
start = m.end()
|
|
43
|
+
end = matches[i + 1].start() if i + 1 < len(matches) else len(content)
|
|
44
|
+
body = content[start:end].strip()
|
|
45
|
+
found[name] = body
|
|
46
|
+
|
|
47
|
+
for req in required:
|
|
48
|
+
if req not in found:
|
|
49
|
+
errors.append(f"Missing: {req}")
|
|
50
|
+
elif not found[req]:
|
|
51
|
+
errors.append(f"Empty: {req}")
|
|
52
|
+
|
|
53
|
+
return (not errors, errors)
|
|
@@ -82,7 +82,7 @@ SETUP_FAILURE_MARKERS = (
|
|
|
82
82
|
def validate_tdd_evidence(task_dir: Path) -> list[dict]:
|
|
83
83
|
"""Return TDD evidence violations for a task directory."""
|
|
84
84
|
task_dir = Path(task_dir)
|
|
85
|
-
prd_text = _read_text(task_dir / "
|
|
85
|
+
prd_text = _read_text(task_dir / "decision-anchor.md")
|
|
86
86
|
evidence_path = task_dir / EVIDENCE_FILE
|
|
87
87
|
|
|
88
88
|
if not evidence_path.is_file():
|
|
@@ -127,7 +127,7 @@ def validate_tdd_evidence(task_dir: Path) -> list[dict]:
|
|
|
127
127
|
def validate_tdd_red_evidence(task_dir: Path) -> list[dict]:
|
|
128
128
|
"""Return violations when implementation would start without red evidence."""
|
|
129
129
|
task_dir = Path(task_dir)
|
|
130
|
-
prd_text = _read_text(task_dir / "
|
|
130
|
+
prd_text = _read_text(task_dir / "decision-anchor.md")
|
|
131
131
|
if not _task_requires_tdd(prd_text):
|
|
132
132
|
return []
|
|
133
133
|
|
|
@@ -161,11 +161,7 @@ def _task_requires_tdd(prd_text: str) -> bool:
|
|
|
161
161
|
return any(marker.lower() in lower for marker in TDD_REQUIRED_MARKERS)
|
|
162
162
|
|
|
163
163
|
|
|
164
|
-
|
|
165
|
-
try:
|
|
166
|
-
return path.read_text(encoding="utf-8")
|
|
167
|
-
except (OSError, UnicodeDecodeError):
|
|
168
|
-
return ""
|
|
164
|
+
from common.core.files import read_text_utf8 as _read_text
|
|
169
165
|
|
|
170
166
|
|
|
171
167
|
def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
|
|
@@ -214,7 +210,10 @@ def _read_jsonl(path: Path) -> tuple[list[dict], list[dict]]:
|
|
|
214
210
|
|
|
215
211
|
|
|
216
212
|
def _acceptance_ids(prd_text: str) -> set[str]:
|
|
217
|
-
|
|
213
|
+
"""提取验收标准 ID,支持 AC-001、AC-1、验收标准:1 等格式。"""
|
|
214
|
+
ids = set(re.findall(r"\bAC-(\d{1,4})\b", prd_text, re.IGNORECASE))
|
|
215
|
+
ids |= set(re.findall(r"验收标准[::]\s*(\d+)", prd_text))
|
|
216
|
+
return {f"AC-{n}" for n in ids}
|
|
218
217
|
|
|
219
218
|
|
|
220
219
|
def _validate_evidence(
|
|
@@ -343,6 +342,9 @@ def _validate_exemption(
|
|
|
343
342
|
)
|
|
344
343
|
)
|
|
345
344
|
|
|
345
|
+
from .anti_rationalization import validate_exemption_rationalization
|
|
346
|
+
violations.extend(validate_exemption_rationalization(str(entry.get("reason", ""))))
|
|
347
|
+
|
|
346
348
|
return violations
|
|
347
349
|
|
|
348
350
|
|
|
@@ -143,11 +143,7 @@ def _read_tdd_entries(path: Path) -> list[dict]:
|
|
|
143
143
|
return entries
|
|
144
144
|
|
|
145
145
|
|
|
146
|
-
|
|
147
|
-
try:
|
|
148
|
-
return path.read_text(encoding="utf-8")
|
|
149
|
-
except (OSError, UnicodeDecodeError):
|
|
150
|
-
return ""
|
|
146
|
+
from common.core.files import read_text_utf8 as _read_text
|
|
151
147
|
|
|
152
148
|
|
|
153
149
|
def _classify_test_content(content: str, test_name: str) -> str:
|