taskledger 0.1.0__py3-none-any.whl

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.
Files changed (67) hide show
  1. taskledger/__init__.py +5 -0
  2. taskledger/__main__.py +6 -0
  3. taskledger/_version.py +24 -0
  4. taskledger/api/__init__.py +13 -0
  5. taskledger/api/handoff.py +247 -0
  6. taskledger/api/introductions.py +9 -0
  7. taskledger/api/locks.py +4 -0
  8. taskledger/api/plans.py +31 -0
  9. taskledger/api/project.py +185 -0
  10. taskledger/api/questions.py +19 -0
  11. taskledger/api/search.py +87 -0
  12. taskledger/api/task_runs.py +38 -0
  13. taskledger/api/tasks.py +61 -0
  14. taskledger/cli.py +600 -0
  15. taskledger/cli_actor.py +196 -0
  16. taskledger/cli_common.py +617 -0
  17. taskledger/cli_implement.py +409 -0
  18. taskledger/cli_migrate.py +328 -0
  19. taskledger/cli_misc.py +984 -0
  20. taskledger/cli_plan.py +478 -0
  21. taskledger/cli_question.py +350 -0
  22. taskledger/cli_task.py +257 -0
  23. taskledger/cli_validate.py +285 -0
  24. taskledger/command_inventory.py +125 -0
  25. taskledger/domain/__init__.py +2 -0
  26. taskledger/domain/models.py +1697 -0
  27. taskledger/domain/policies.py +542 -0
  28. taskledger/domain/states.py +320 -0
  29. taskledger/errors.py +165 -0
  30. taskledger/exchange.py +343 -0
  31. taskledger/ids.py +19 -0
  32. taskledger/py.typed +0 -0
  33. taskledger/search.py +349 -0
  34. taskledger/services/__init__.py +1 -0
  35. taskledger/services/actors.py +245 -0
  36. taskledger/services/dashboard.py +306 -0
  37. taskledger/services/doctor.py +435 -0
  38. taskledger/services/handoff.py +1029 -0
  39. taskledger/services/handoff_lifecycle.py +154 -0
  40. taskledger/services/navigation.py +930 -0
  41. taskledger/services/phase5_lock_transfer.py +96 -0
  42. taskledger/services/plan_lint.py +397 -0
  43. taskledger/services/serve_read_model.py +852 -0
  44. taskledger/services/tasks.py +4224 -0
  45. taskledger/services/validation.py +221 -0
  46. taskledger/services/web_dashboard.py +1742 -0
  47. taskledger/storage/__init__.py +39 -0
  48. taskledger/storage/atomic.py +57 -0
  49. taskledger/storage/common.py +90 -0
  50. taskledger/storage/events.py +98 -0
  51. taskledger/storage/frontmatter.py +57 -0
  52. taskledger/storage/indexes.py +42 -0
  53. taskledger/storage/init.py +187 -0
  54. taskledger/storage/locks.py +83 -0
  55. taskledger/storage/meta.py +103 -0
  56. taskledger/storage/migrations.py +207 -0
  57. taskledger/storage/paths.py +166 -0
  58. taskledger/storage/project_config.py +393 -0
  59. taskledger/storage/repos.py +256 -0
  60. taskledger/storage/task_store.py +836 -0
  61. taskledger/timeutils.py +7 -0
  62. taskledger-0.1.0.dist-info/METADATA +411 -0
  63. taskledger-0.1.0.dist-info/RECORD +67 -0
  64. taskledger-0.1.0.dist-info/WHEEL +5 -0
  65. taskledger-0.1.0.dist-info/entry_points.txt +2 -0
  66. taskledger-0.1.0.dist-info/licenses/LICENSE +201 -0
  67. taskledger-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,221 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Any, cast
5
+
6
+ from taskledger.domain.models import TaskRecord, TaskRunRecord, ValidationCheck
7
+ from taskledger.services.tasks import _dependency_blockers, _optional_run
8
+ from taskledger.storage.task_store import load_todos, resolve_plan
9
+
10
+
11
+ def build_validation_gate_report(
12
+ workspace_root: Path,
13
+ task: TaskRecord,
14
+ run: TaskRunRecord | None = None,
15
+ ) -> dict[str, object]:
16
+ """Build a comprehensive validation gate report."""
17
+ run = run or _optional_run(workspace_root, task, task.latest_validation_run)
18
+
19
+ report: dict[str, Any] = {
20
+ "kind": "validation_status",
21
+ "task_id": task.id,
22
+ "task_slug": task.slug,
23
+ "status_stage": task.status_stage,
24
+ "active_stage": None,
25
+ "run_id": run.run_id if run else None,
26
+ "can_finish_passed": False,
27
+ }
28
+
29
+ report["accepted_plan"] = {}
30
+ if task.accepted_plan_version is not None:
31
+ accepted_plan = resolve_plan(
32
+ workspace_root,
33
+ task.id,
34
+ version=task.accepted_plan_version,
35
+ )
36
+ report["accepted_plan"] = {
37
+ "version": task.accepted_plan_version,
38
+ "status": accepted_plan.status,
39
+ }
40
+
41
+ report["implementation"] = {}
42
+ impl_run = _optional_run(workspace_root, task, task.latest_implementation_run)
43
+ if impl_run:
44
+ report["implementation"] = {
45
+ "run_id": impl_run.run_id,
46
+ "status": impl_run.status,
47
+ "satisfied": impl_run.status == "finished",
48
+ }
49
+
50
+ report["criteria"] = []
51
+ missing_criteria = []
52
+ failing_criteria = []
53
+
54
+ if task.accepted_plan_version is not None:
55
+ accepted_plan = resolve_plan(
56
+ workspace_root,
57
+ task.id,
58
+ version=task.accepted_plan_version,
59
+ )
60
+
61
+ checks_by_criterion: dict[str, list[ValidationCheck]] = {}
62
+ if run:
63
+ for check in run.checks:
64
+ if check.criterion_id is not None:
65
+ checks_by_criterion.setdefault(check.criterion_id, []).append(check)
66
+
67
+ for criterion in accepted_plan.criteria:
68
+ checks = checks_by_criterion.get(criterion.id, [])
69
+
70
+ latest_check = checks[-1] if checks else None
71
+ latest_status = latest_check.status if latest_check else "not_run"
72
+ satisfied = bool(
73
+ latest_status == "pass"
74
+ or (latest_check and _criterion_has_user_waiver(latest_check))
75
+ )
76
+
77
+ has_waiver = latest_check and _criterion_has_user_waiver(latest_check)
78
+
79
+ blocker = []
80
+ if criterion.mandatory:
81
+ if latest_status == "fail":
82
+ blocker = [
83
+ {"kind": "criterion_fail", "message": "Latest check failed"}
84
+ ]
85
+ failing_criteria.append(criterion.id)
86
+ elif latest_status == "not_run":
87
+ blocker = [
88
+ {
89
+ "kind": "criterion_missing",
90
+ "message": "No passing check recorded",
91
+ }
92
+ ]
93
+ missing_criteria.append(criterion.id)
94
+ elif not satisfied and latest_status != "pass":
95
+ blocker = [
96
+ {
97
+ "kind": "criterion_unsatisfied",
98
+ "message": f"Latest check status: {latest_status}",
99
+ }
100
+ ]
101
+ missing_criteria.append(criterion.id)
102
+
103
+ criterion_report = {
104
+ "id": criterion.id,
105
+ "text": criterion.text,
106
+ "mandatory": criterion.mandatory,
107
+ "latest_check_id": latest_check.id if latest_check else None,
108
+ "latest_status": latest_status,
109
+ "satisfied": satisfied,
110
+ "has_waiver": has_waiver,
111
+ "evidence": list(latest_check.evidence) if latest_check else [],
112
+ "history": [{"check_id": c.id, "status": c.status} for c in checks],
113
+ "blockers": blocker,
114
+ }
115
+ cast(list[dict[str, object]], report["criteria"]).append(criterion_report)
116
+
117
+ report["todos"] = {"open_mandatory": []}
118
+ todos = load_todos(workspace_root, task.id).todos
119
+ open_todos = [todo.id for todo in todos if todo.mandatory and not todo.done]
120
+ cast(dict[str, object], report["todos"])["open_mandatory"] = open_todos
121
+
122
+ report["dependencies"] = {"blockers": _dependency_blockers(workspace_root, task)}
123
+
124
+ blockers: list[dict[str, object]] = []
125
+
126
+ if task.accepted_plan_version is None:
127
+ blockers.append(
128
+ {
129
+ "kind": "no_accepted_plan",
130
+ "message": "No accepted plan is recorded.",
131
+ "command_hint": (
132
+ "taskledger plan propose ... && taskledger plan approve ..."
133
+ ),
134
+ }
135
+ )
136
+ elif task.accepted_plan_version is not None:
137
+ accepted_plan = resolve_plan(
138
+ workspace_root,
139
+ task.id,
140
+ version=task.accepted_plan_version,
141
+ )
142
+ if accepted_plan.status != "accepted":
143
+ blockers.append(
144
+ {
145
+ "kind": "plan_not_accepted",
146
+ "message": (
147
+ "Accepted plan record status is "
148
+ f"{accepted_plan.status}, not accepted."
149
+ ),
150
+ }
151
+ )
152
+
153
+ if not impl_run or impl_run.status != "finished":
154
+ blockers.append(
155
+ {
156
+ "kind": "no_finished_implementation",
157
+ "message": "No finished implementation run is recorded.",
158
+ "command_hint": (
159
+ "taskledger implement start ... && taskledger implement finish ..."
160
+ ),
161
+ }
162
+ )
163
+
164
+ for missing_id in missing_criteria:
165
+ blockers.append(
166
+ {
167
+ "kind": "criterion_missing",
168
+ "ref": missing_id,
169
+ "message": f"Mandatory criterion {missing_id} has no passing check.",
170
+ "command_hint": (
171
+ f"taskledger validate check --criterion {missing_id} --status pass "
172
+ f'--evidence "..."'
173
+ ),
174
+ }
175
+ )
176
+
177
+ for failing_id in failing_criteria:
178
+ blockers.append(
179
+ {
180
+ "kind": "criterion_fail",
181
+ "ref": failing_id,
182
+ "message": f"Mandatory criterion {failing_id} has a failing check.",
183
+ "command_hint": (
184
+ f"taskledger validate check --criterion {failing_id} --status pass "
185
+ f'--evidence "..."'
186
+ ),
187
+ }
188
+ )
189
+
190
+ for todo_id in open_todos:
191
+ blockers.append(
192
+ {
193
+ "kind": "todo_open",
194
+ "ref": todo_id,
195
+ "message": f"Mandatory todo {todo_id} is not done.",
196
+ "command_hint": f'taskledger todo done {todo_id} --evidence "..."',
197
+ }
198
+ )
199
+
200
+ dependency_report = cast(dict[str, object], report["dependencies"])
201
+ dependency_blockers = cast(list[object], dependency_report["blockers"])
202
+ for dep_blocker in dependency_blockers:
203
+ blockers.append(
204
+ {
205
+ "kind": "dependency_blocker",
206
+ "ref": dep_blocker,
207
+ "message": f"Dependency {dep_blocker} blocks this task.",
208
+ }
209
+ )
210
+
211
+ report["blockers"] = blockers
212
+ report["can_finish_passed"] = len(blockers) == 0
213
+
214
+ return report
215
+
216
+
217
+ def _criterion_has_user_waiver(check: ValidationCheck) -> bool:
218
+ return check.waiver is not None and check.waiver.actor.actor_type == "user"
219
+
220
+
221
+ __all__ = ["build_validation_gate_report"]