@michaelxwb/code-flow 0.6.5 → 0.8.1
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/README.md +47 -3
- package/docs/USAGE.md +1529 -0
- package/docs/acceptance-contract.md +217 -0
- package/docs/ci/acceptance-evidence.md +77 -0
- package/docs/migrations/runtime-0.7.0.md +41 -0
- package/docs/migrations/runtime-0.8.0.md +154 -0
- package/docs/migrations/runtime-0.9.0.md +40 -0
- package/docs/spec-injection-hardening.md +54 -0
- package/docs/spec-injection-performance.json +73 -0
- package/docs/spec-optimization.md +12 -0
- package/package.json +18 -3
- package/src/adapters/claude/CLAUDE.md +1 -1
- package/src/adapters/claude/commands/cf-init.md +1 -3
- package/src/adapters/claude/commands/cf-spec.md +23 -3
- package/src/adapters/claude/commands/cf-stats.md +11 -1
- package/src/adapters/claude/commands/cf-sync.md +20 -6
- package/src/adapters/claude/commands/cf-task/align.md +25 -3
- package/src/adapters/claude/commands/cf-task/archive.md +36 -42
- package/src/adapters/claude/commands/cf-task/block.md +15 -2
- package/src/adapters/claude/commands/cf-task/graph.md +13 -1
- package/src/adapters/claude/commands/cf-task/note.md +13 -1
- package/src/adapters/claude/commands/cf-task/plan.md +137 -23
- package/src/adapters/claude/commands/cf-task/prd.md +13 -2
- package/src/adapters/claude/commands/cf-task/start.md +52 -39
- package/src/adapters/claude/commands/cf-task/status.md +12 -0
- package/src/adapters/claude/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/claude/commands/cf-validate.md +30 -5
- package/src/adapters/codex/AGENTS.md +1 -1
- package/src/adapters/codex/hooks.json +14 -10
- package/src/adapters/codex/skills/cf-init/SKILL.md +4 -4
- package/src/adapters/codex/skills/cf-spec/SKILL.md +34 -6
- package/src/adapters/codex/skills/cf-stats/SKILL.md +11 -1
- package/src/adapters/codex/skills/cf-sync/SKILL.md +21 -6
- package/src/adapters/codex/skills/cf-task-align/SKILL.md +30 -8
- package/src/adapters/codex/skills/cf-task-archive/SKILL.md +36 -42
- package/src/adapters/codex/skills/cf-task-block/SKILL.md +15 -2
- package/src/adapters/codex/skills/cf-task-graph/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-note/SKILL.md +13 -1
- package/src/adapters/codex/skills/cf-task-plan/SKILL.md +175 -21
- package/src/adapters/codex/skills/cf-task-prd/SKILL.md +13 -2
- package/src/adapters/codex/skills/cf-task-start/SKILL.md +52 -39
- package/src/adapters/codex/skills/cf-task-status/SKILL.md +12 -0
- package/src/adapters/codex/skills/cf-task-verify-e2e/SKILL.md +69 -35
- package/src/adapters/codex/skills/cf-validate/SKILL.md +31 -5
- package/src/adapters/commands.json +115 -0
- package/src/adapters/costrict/CLAUDE.md +1 -1
- package/src/adapters/costrict/commands/cf-init.md +1 -3
- package/src/adapters/costrict/commands/cf-spec.md +23 -3
- package/src/adapters/costrict/commands/cf-stats.md +11 -1
- package/src/adapters/costrict/commands/cf-sync.md +20 -6
- package/src/adapters/costrict/commands/cf-task/align.md +25 -3
- package/src/adapters/costrict/commands/cf-task/archive.md +36 -42
- package/src/adapters/costrict/commands/cf-task/block.md +15 -2
- package/src/adapters/costrict/commands/cf-task/graph.md +13 -1
- package/src/adapters/costrict/commands/cf-task/note.md +13 -1
- package/src/adapters/costrict/commands/cf-task/plan.md +137 -23
- package/src/adapters/costrict/commands/cf-task/prd.md +13 -2
- package/src/adapters/costrict/commands/cf-task/start.md +52 -39
- package/src/adapters/costrict/commands/cf-task/status.md +12 -0
- package/src/adapters/costrict/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/costrict/commands/cf-validate.md +30 -5
- package/src/adapters/install-plan.json +100 -0
- package/src/adapters/opencode/AGENTS.md +1 -1
- package/src/adapters/opencode/commands/cf-init.md +7 -7
- package/src/adapters/opencode/commands/cf-learn.md +6 -6
- package/src/adapters/opencode/commands/cf-spec.md +34 -2
- package/src/adapters/opencode/commands/cf-stats.md +17 -7
- package/src/adapters/opencode/commands/cf-sync.md +21 -6
- package/src/adapters/opencode/commands/cf-task/align.md +25 -8
- package/src/adapters/opencode/commands/cf-task/archive.md +36 -42
- package/src/adapters/opencode/commands/cf-task/block.md +16 -2
- package/src/adapters/opencode/commands/cf-task/graph.md +14 -1
- package/src/adapters/opencode/commands/cf-task/note.md +14 -1
- package/src/adapters/opencode/commands/cf-task/plan.md +137 -23
- package/src/adapters/opencode/commands/cf-task/prd.md +13 -2
- package/src/adapters/opencode/commands/cf-task/start.md +53 -45
- package/src/adapters/opencode/commands/cf-task/status.md +13 -0
- package/src/adapters/opencode/commands/cf-task/verify-e2e.md +68 -35
- package/src/adapters/opencode/commands/cf-validate.md +35 -9
- package/src/adapters/opencode/plugins/code-flow/index.js +128 -179
- package/src/adapters/platform-bindings.json +370 -0
- package/src/cli.js +369 -316
- package/src/core/code-flow/config.yml +4 -1
- package/src/core/code-flow/runtime-commands.json +259 -0
- package/src/core/code-flow/scripts/cf_acceptance_manifest.py +5 -289
- package/src/core/code-flow/scripts/cf_acceptance_runner.py +5 -187
- package/src/core/code-flow/scripts/cf_audit.py +10 -0
- package/src/core/code-flow/scripts/cf_codex_hook.py +10 -0
- package/src/core/code-flow/scripts/cf_design_lint.py +10 -0
- package/src/core/code-flow/scripts/cf_evidence_import.py +10 -0
- package/src/core/code-flow/scripts/cf_feedback.py +5 -51
- package/src/core/code-flow/scripts/cf_opencode_event.py +10 -0
- package/src/core/code-flow/scripts/cf_post_hook.py +6 -191
- package/src/core/code-flow/scripts/cf_pre_tool_hook.py +6 -99
- package/src/core/code-flow/scripts/cf_scenarios.py +10 -0
- package/src/core/code-flow/scripts/cf_spec_context.py +5 -1741
- package/src/core/code-flow/scripts/cf_spec_gate.py +5 -259
- package/src/core/code-flow/scripts/cf_spec_session.py +5 -114
- package/src/core/code-flow/scripts/cf_stats.py +5 -501
- package/src/core/code-flow/scripts/cf_stop_hook.py +6 -376
- package/src/core/code-flow/scripts/cf_sync.py +5 -135
- package/src/core/code-flow/scripts/cf_task_index.py +5 -143
- package/src/core/code-flow/scripts/cf_task_parallel.py +5 -389
- package/src/core/code-flow/scripts/cf_task_workflow.py +6 -201
- package/src/core/code-flow/scripts/cf_user_prompt_hook.py +6 -173
- package/src/core/code-flow/scripts/cf_validation.py +6 -77
- package/src/core/code-flow/scripts/codeflow/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_evidence.py +347 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_manifest_core.py +660 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_plan.py +202 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_registration.py +60 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_risk.py +73 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_schema.py +282 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_acceptance_selection.py +266 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_inputs.py +172 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_check_registration.py +326 -0
- package/src/core/code-flow/scripts/{cf_checks.py → codeflow/acceptance/cf_checks.py} +8 -11
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_plan_quality.py +197 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_quality.py +174 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_scenario_source.py +300 -0
- package/src/core/code-flow/scripts/codeflow/acceptance/cf_traceability.py +502 -0
- package/src/core/code-flow/scripts/codeflow/cli/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_manifest.py +347 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_acceptance_runner.py +1490 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_audit.py +533 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_design_lint.py +172 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_evidence_import.py +607 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_feedback.py +53 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_scenarios.py +58 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_context.py +1175 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_gate.py +60 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_spec_session.py +46 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_stats.py +540 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_sync.py +144 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_index.py +177 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_parallel.py +1142 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_task_workflow.py +742 -0
- package/src/core/code-flow/scripts/codeflow/cli/cf_validation.py +31 -0
- package/src/core/code-flow/scripts/codeflow/core/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_context_cache.py +67 -0
- package/src/core/code-flow/scripts/{cf_core.py → codeflow/core/cf_core.py} +119 -297
- package/src/core/code-flow/scripts/codeflow/core/cf_exec_base.py +325 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_fs.py +57 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_git.py +92 -0
- package/src/core/code-flow/scripts/{cf_log.py → codeflow/core/cf_log.py} +13 -7
- package/src/core/code-flow/scripts/codeflow/core/cf_markdown.py +77 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_native_paths.py +26 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_patch.py +33 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_runtime_install.py +32 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_stop_guard.py +53 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_task_state.py +9 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_validators.py +408 -0
- package/src/core/code-flow/scripts/codeflow/core/cf_worktree_env.py +253 -0
- package/src/core/code-flow/scripts/codeflow/evidence/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/evidence/cf_evidence_store.py +527 -0
- package/src/core/code-flow/scripts/codeflow/hooks/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_codex_hook.py +95 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_opencode_event.py +98 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_post_hook.py +30 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_pre_tool_hook.py +41 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_stop_hook.py +292 -0
- package/src/core/code-flow/scripts/codeflow/hooks/cf_user_prompt_hook.py +161 -0
- package/src/core/code-flow/scripts/codeflow/spec/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_active_task.py +412 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_context_refresh.py +301 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_edit_service.py +149 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_injection.py +111 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_rule_readiness.py +114 -0
- package/src/core/code-flow/scripts/{cf_scan.py → codeflow/spec/cf_scan.py} +93 -21
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_budget.py +55 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_context_model.py +477 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_impact.py +119 -0
- package/src/core/code-flow/scripts/{cf_spec_metadata.py → codeflow/spec/cf_spec_metadata.py} +5 -2
- package/src/core/code-flow/scripts/{cf_spec_migrate.py → codeflow/spec/cf_spec_migrate.py} +27 -10
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize.py +193 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_spec_optimize_apply.py +187 -0
- package/src/core/code-flow/scripts/{cf_spec_resolver.py → codeflow/spec/cf_spec_resolver.py} +39 -17
- package/src/core/code-flow/scripts/{cf_spec_router.py → codeflow/spec/cf_spec_router.py} +96 -36
- package/src/core/code-flow/scripts/{cf_spec_verify.py → codeflow/spec/cf_spec_verify.py} +163 -35
- package/src/core/code-flow/scripts/codeflow/spec/cf_stage_gate.py +330 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_task_projection.py +83 -0
- package/src/core/code-flow/scripts/codeflow/spec/cf_workflow_service.py +755 -0
- package/src/core/code-flow/scripts/{cf_workflow_transaction.py → codeflow/spec/cf_workflow_transaction.py} +29 -15
- package/src/core/code-flow/scripts/codeflow/task/__init__.py +0 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_requirement_verification.py +867 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_doc.py +178 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_merge.py +635 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_plan.py +251 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_task_runtime.py +570 -0
- package/src/core/code-flow/scripts/codeflow/task/cf_validation_service.py +73 -0
- package/src/core/code-flow/scripts/runtime-resources.json +29 -0
- package/src/core/code-flow/specs/shared/design/design-frontend.md +18 -15
- package/src/core/code-flow/specs/shared/design/design-full.md +33 -23
- package/src/core/code-flow/specs/shared/design/design-lite.md +23 -13
- package/src/core/code-flow/specs/shared/scenarios-template.md +55 -0
- package/src/core/code-flow/validation.yml +9 -3
- package/src/migrate/runtime.js +213 -0
- package/src/migrate/spec-workflow.js +16 -1
- package/src/runtime.js +219 -0
- package/src/core/code-flow/scripts/cf_acceptance_evidence.py +0 -100
- package/src/core/code-flow/scripts/cf_acceptance_schema.py +0 -83
- package/src/core/code-flow/scripts/cf_exec_base.py +0 -112
- package/src/core/code-flow/scripts/cf_task_runtime.py +0 -304
- package/src/core/code-flow/scripts/cf_task_state.py +0 -3
- package/src/core/code-flow/scripts/cf_workflow_service.py +0 -366
- /package/src/core/code-flow/scripts/{cf_session_state.py → codeflow/core/cf_session_state.py} +0 -0
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Single source of truth for parsing TASK documents.
|
|
3
|
+
|
|
4
|
+
Pure module: standard library only, no cf_* imports. Headings are anchored at
|
|
5
|
+
column 0, fenced code blocks are inert, duplicate TASK IDs fail closed, and a
|
|
6
|
+
section without a Status line projects as `draft`.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import re
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from typing import Iterator, Mapping, Optional, Union
|
|
14
|
+
|
|
15
|
+
_HEADING_RE = re.compile(r"^## (TASK-\d+):[ \t]*(.*)$")
|
|
16
|
+
_FIELD_RE = re.compile(r"^- \*\*([^*]+)\*\*:[ \t]*(.*)$")
|
|
17
|
+
_FENCE_RE = re.compile(r"^ {0,3}(`{3,}|~{3,})")
|
|
18
|
+
_TASK_ID_RE = re.compile(r"TASK-\d+")
|
|
19
|
+
_DEPENDS_SPLIT_RE = re.compile(r"[,,\s]+")
|
|
20
|
+
_SPEC_REFS_RE = re.compile(r"(?m)^- \*\*Spec-Refs\*\*:[ \t]*(.*)$")
|
|
21
|
+
_EMPTY_REF_MARKERS = frozenset(("n/a", "na", "none", "null", "-", "—", "无"))
|
|
22
|
+
_UNRESOLVED_REF_MARKERS = frozenset(("todo", "tbd", "待定", "待同步", "待确认"))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class TaskDocumentError(ValueError):
|
|
26
|
+
"""Structured parse failure with a stable error code."""
|
|
27
|
+
|
|
28
|
+
def __init__(self, code: str, message: str, details: tuple[str, ...] = ()) -> None:
|
|
29
|
+
self.code = code
|
|
30
|
+
self.details = details
|
|
31
|
+
suffix = f": {'; '.join(details)}" if details else ""
|
|
32
|
+
super().__init__(f"[{code}] {message}{suffix}")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(frozen=True)
|
|
36
|
+
class TaskSection:
|
|
37
|
+
id: str
|
|
38
|
+
title: str
|
|
39
|
+
status: str
|
|
40
|
+
depends: tuple[str, ...]
|
|
41
|
+
fields: Mapping[str, str]
|
|
42
|
+
body: str
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@dataclass(frozen=True)
|
|
46
|
+
class TaskDocument:
|
|
47
|
+
tasks: tuple[TaskSection, ...]
|
|
48
|
+
source: str = ""
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _unfenced_lines(text: str) -> Iterator[str]:
|
|
52
|
+
fence = ""
|
|
53
|
+
for line in text.splitlines():
|
|
54
|
+
match = _FENCE_RE.match(line)
|
|
55
|
+
if match is not None:
|
|
56
|
+
marker = match.group(1)
|
|
57
|
+
if not fence:
|
|
58
|
+
fence = marker
|
|
59
|
+
elif marker[0] == fence[0] and len(marker) >= len(fence) and line.strip() == marker:
|
|
60
|
+
fence = ""
|
|
61
|
+
continue
|
|
62
|
+
if not fence:
|
|
63
|
+
yield line
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _heading_spans(text: str) -> list[tuple[int, str, str]]:
|
|
67
|
+
"""Offsets/IDs/titles of real TASK headings, skipping fenced code blocks."""
|
|
68
|
+
spans: list[tuple[int, str, str]] = []
|
|
69
|
+
offset = 0
|
|
70
|
+
fence = ""
|
|
71
|
+
for line in text.splitlines(keepends=True):
|
|
72
|
+
content = line.rstrip("\n")
|
|
73
|
+
match = _FENCE_RE.match(content)
|
|
74
|
+
if match is not None:
|
|
75
|
+
marker = match.group(1)
|
|
76
|
+
if not fence:
|
|
77
|
+
fence = marker
|
|
78
|
+
elif marker[0] == fence[0] and len(marker) >= len(fence) and content.strip() == marker:
|
|
79
|
+
fence = ""
|
|
80
|
+
elif not fence:
|
|
81
|
+
heading = _HEADING_RE.match(content)
|
|
82
|
+
if heading is not None:
|
|
83
|
+
spans.append((offset, heading.group(1), heading.group(2).strip()))
|
|
84
|
+
offset += len(line)
|
|
85
|
+
return spans
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fields(body: str) -> Mapping[str, str]:
|
|
89
|
+
fields: dict[str, str] = {}
|
|
90
|
+
for line in _unfenced_lines(body):
|
|
91
|
+
match = _FIELD_RE.match(line.rstrip())
|
|
92
|
+
if match is not None:
|
|
93
|
+
name = match.group(1).strip()
|
|
94
|
+
if name == "Rule-Owner-Refs" and name in fields:
|
|
95
|
+
raise TaskDocumentError("duplicate_rule_owner_refs", "重复的 Rule-Owner-Refs 字段")
|
|
96
|
+
fields.setdefault(name, match.group(2).strip())
|
|
97
|
+
return fields
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _depends(fields: Mapping[str, str]) -> tuple[str, ...]:
|
|
101
|
+
raw = fields.get("Depends", "")
|
|
102
|
+
return tuple(
|
|
103
|
+
part.strip() for part in _DEPENDS_SPLIT_RE.split(raw)
|
|
104
|
+
if _TASK_ID_RE.fullmatch(part.strip())
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _section(text: str, start: int, end: int, task_id: str, title: str) -> TaskSection:
|
|
109
|
+
body = text[start:end]
|
|
110
|
+
fields = _fields(body)
|
|
111
|
+
return TaskSection(
|
|
112
|
+
id=task_id,
|
|
113
|
+
title=title,
|
|
114
|
+
status=fields.get("Status") or "draft",
|
|
115
|
+
depends=_depends(fields),
|
|
116
|
+
fields=fields,
|
|
117
|
+
body=body,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def parse_task_document(text: str, *, source: str = "") -> TaskDocument:
|
|
122
|
+
"""Parse `## TASK-xxx` sections into the ordered TaskDocument contract."""
|
|
123
|
+
normalized = text.replace("\r\n", "\n").replace("\r", "\n")
|
|
124
|
+
spans = _heading_spans(normalized)
|
|
125
|
+
if not spans:
|
|
126
|
+
raise TaskDocumentError("task_document_empty", "no TASK sections found", (source,) if source else ())
|
|
127
|
+
tasks: list[TaskSection] = []
|
|
128
|
+
seen: set[str] = set()
|
|
129
|
+
for index, (start, task_id, title) in enumerate(spans):
|
|
130
|
+
if task_id in seen:
|
|
131
|
+
details = (task_id, source) if source else (task_id,)
|
|
132
|
+
raise TaskDocumentError("duplicate_task_id", f"{task_id} appears more than once", details)
|
|
133
|
+
seen.add(task_id)
|
|
134
|
+
end = spans[index + 1][0] if index + 1 < len(spans) else len(normalized)
|
|
135
|
+
tasks.append(_section(normalized, start, end, task_id, title))
|
|
136
|
+
return TaskDocument(tuple(tasks), source)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def task_section(text: str, task_id: str, *, source: str = "") -> TaskSection:
|
|
140
|
+
"""Return one parsed TASK section; missing IDs fail with a stable code."""
|
|
141
|
+
document = parse_task_document(text, source=source)
|
|
142
|
+
section = next((task for task in document.tasks if task.id == task_id), None)
|
|
143
|
+
if section is None:
|
|
144
|
+
details = (task_id, source) if source else (task_id,)
|
|
145
|
+
raise TaskDocumentError("task_not_found", f"{task_id} not found", details)
|
|
146
|
+
return section
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def task_spec_refs(section: Union[TaskSection, str]) -> tuple[str, ...]:
|
|
150
|
+
"""Interpret Spec-Refs with the shared empty/unresolved marker contract."""
|
|
151
|
+
if isinstance(section, TaskSection):
|
|
152
|
+
raw = section.fields.get("Spec-Refs")
|
|
153
|
+
if raw is None:
|
|
154
|
+
raise ValueError("spec_refs_missing")
|
|
155
|
+
else:
|
|
156
|
+
match = _SPEC_REFS_RE.search(section)
|
|
157
|
+
if match is None:
|
|
158
|
+
raise ValueError("spec_refs_missing")
|
|
159
|
+
raw = match.group(1)
|
|
160
|
+
items = tuple(item.strip() for item in raw.split(",") if item.strip())
|
|
161
|
+
unresolved = tuple(item for item in items if item.lower() in _UNRESOLVED_REF_MARKERS)
|
|
162
|
+
if unresolved:
|
|
163
|
+
raise ValueError("spec_refs_unresolved: " + ", ".join(unresolved))
|
|
164
|
+
return tuple(item for item in items if item.lower() not in _EMPTY_REF_MARKERS)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def task_rule_owner_refs(section: TaskSection) -> Optional[tuple[str, ...]]:
|
|
168
|
+
"""Read explicit plan owners; None means this TASK has no owner field."""
|
|
169
|
+
raw = section.fields.get("Rule-Owner-Refs")
|
|
170
|
+
if raw is None:
|
|
171
|
+
return None
|
|
172
|
+
items = tuple(item.strip() for item in raw.split(","))
|
|
173
|
+
if len(items) == 1 and items[0].lower() in _EMPTY_REF_MARKERS:
|
|
174
|
+
return ()
|
|
175
|
+
if (not all(re.fullmatch(r"[^\s,#]+#[^\s,#]+", item) for item in items)
|
|
176
|
+
or len(items) != len(set(items))):
|
|
177
|
+
raise ValueError("rule_owner_refs_invalid: " + raw)
|
|
178
|
+
return items
|