cowork-flow 0.0.37 → 0.0.39

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 (111) hide show
  1. package/README.md +27 -14
  2. package/package.json +1 -1
  3. package/src/commands/init.js +39 -28
  4. package/src/commands/sync.js +2 -2
  5. package/src/lib/asset-plan.js +127 -0
  6. package/src/lib/copy-template.js +101 -141
  7. package/src/lib/host-assets.js +231 -0
  8. package/src/lib/plan-applier.js +329 -0
  9. package/src/lib/platforms.js +15 -76
  10. package/src/lib/skill-registry.js +503 -0
  11. package/template/.claude/agents/cowork-check.md +10 -1
  12. package/template/.claude/agents/cowork-implement.md +11 -2
  13. package/template/.claude/hooks/inject-workflow-state.py +51 -254
  14. package/template/.codex/agents/cowork-check.toml +12 -3
  15. package/template/.codex/agents/cowork-implement.toml +11 -2
  16. package/template/.codex/agents/default.toml +3 -2
  17. package/template/.codex/agents/explorer.toml +3 -2
  18. package/template/.codex/agents/worker.toml +2 -1
  19. package/template/.codex/hooks/inject-workflow-state.py +46 -265
  20. package/template/.cowork-flow/.version +1 -1
  21. package/template/.cowork-flow/scripts/application/__init__.py +1 -0
  22. package/template/.cowork-flow/scripts/application/batch_execution.py +743 -0
  23. package/template/.cowork-flow/scripts/application/runtime_context_service.py +365 -0
  24. package/template/.cowork-flow/scripts/application/task_archive.py +220 -0
  25. package/template/.cowork-flow/scripts/application/task_context.py +765 -0
  26. package/template/.cowork-flow/scripts/application/task_creation.py +201 -0
  27. package/template/.cowork-flow/scripts/application/task_lifecycle.py +532 -0
  28. package/template/.cowork-flow/scripts/application/task_tree.py +179 -0
  29. package/template/.cowork-flow/scripts/commands/doctor.py +78 -57
  30. package/template/.cowork-flow/scripts/commands/party_mode_v2.py +671 -425
  31. package/template/.cowork-flow/scripts/commands/subagent.py +38 -26
  32. package/template/.cowork-flow/scripts/commands/task.py +535 -1953
  33. package/template/.cowork-flow/scripts/commands/task_archive_commands.py +195 -0
  34. package/template/.cowork-flow/scripts/commands/task_context_commands.py +240 -0
  35. package/template/.cowork-flow/scripts/commands/task_create_command.py +164 -0
  36. package/template/.cowork-flow/scripts/commands/task_navigation.py +407 -0
  37. package/template/.cowork-flow/scripts/commands/task_parser.py +364 -0
  38. package/template/.cowork-flow/scripts/commands/task_support.py +76 -0
  39. package/template/.cowork-flow/scripts/commands/task_tree_commands.py +160 -0
  40. package/template/.cowork-flow/scripts/common/core/execution_context.py +161 -85
  41. package/template/.cowork-flow/scripts/common/core/host_manifest.py +393 -0
  42. package/template/.cowork-flow/scripts/common/core/skill_registry.py +568 -0
  43. package/template/.cowork-flow/scripts/common/gates/coding_standards.py +58 -7
  44. package/template/.cowork-flow/scripts/common/gates/gates.py +323 -46
  45. package/template/.cowork-flow/scripts/common/gates/models.py +196 -0
  46. package/template/.cowork-flow/scripts/common/gates/registry.py +79 -0
  47. package/template/.cowork-flow/scripts/common/gates/tdd_evidence.py +13 -13
  48. package/template/.cowork-flow/scripts/common/gates/validate_coding_standards.py +104 -62
  49. package/template/.cowork-flow/scripts/common/gates/validate_implementation.py +39 -22
  50. package/template/.cowork-flow/scripts/common/gates/validate_rules.py +223 -40
  51. package/template/.cowork-flow/scripts/common/git/git_context.py +216 -182
  52. package/template/.cowork-flow/scripts/common/host/__init__.py +1 -0
  53. package/template/.cowork-flow/scripts/common/host/workflow_state_hook.py +303 -0
  54. package/template/.cowork-flow/scripts/common/storage/__init__.py +1 -0
  55. package/template/.cowork-flow/scripts/common/storage/operation_log.py +72 -0
  56. package/template/.cowork-flow/scripts/common/storage/state_store.py +279 -0
  57. package/template/.cowork-flow/scripts/common/storage/unit_of_work.py +214 -0
  58. package/template/.cowork-flow/scripts/common/task/active_task.py +93 -135
  59. package/template/.cowork-flow/scripts/common/task/batch_mode.py +33 -120
  60. package/template/.cowork-flow/scripts/common/task/decision_review.py +85 -0
  61. package/template/.cowork-flow/scripts/common/task/readiness.py +9 -15
  62. package/template/.cowork-flow/scripts/common/task/task_repository.py +156 -0
  63. package/template/.cowork-flow/scripts/run.py +24 -0
  64. package/template/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  65. package/template/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  66. package/template/.cowork-flow/spec/contracts/workflow-state-templates.md +1 -1
  67. package/template/.cowork-flow/spec/protocols/decision-review.md +28 -0
  68. package/template/.cowork-flow/spec/protocols/review.md +34 -0
  69. package/template/.cowork-flow/spec/protocols/spec-maintenance.md +16 -0
  70. package/template/.cowork-flow/spec/protocols/tdd.md +30 -0
  71. package/template/.cowork-flow/spec/references/definition-of-done.md +4 -4
  72. package/template/.cowork-flow/spec/references/orchestration-patterns.md +1 -1
  73. package/template/.cowork-flow/spec/runtime/contract-registry.json +25 -0
  74. package/template/.cowork-flow/spec/runtime/host-assets.json +214 -0
  75. package/template/.cowork-flow/spec/runtime/index.md +8 -0
  76. package/template/.cowork-flow/spec/runtime/rules.json +61 -10
  77. package/template/.cowork-flow/spec/runtime/skill-registry.json +397 -0
  78. package/template/.cowork-flow/spec/schemas/host-assets.schema.json +173 -0
  79. package/template/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  80. package/template/.cowork-flow/spec/schemas/skill-registry.schema.json +202 -0
  81. package/template/.cowork-flow/workflow.md +18 -13
  82. package/template/.opencode/agents/cowork-check.md +11 -0
  83. package/template/.opencode/agents/cowork-implement.md +16 -1
  84. package/template/.zcode/.zcode-plugin/plugin.json +1 -1
  85. package/template/.zcode/hooks/inject-context.js +3 -85
  86. package/template/.zcode/hooks/inject-context.selfcheck.mjs +6 -6
  87. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/decision-anchor.md +8 -2
  88. package/template/.zcode/scaffold/.cowork-flow/spec/contracts/subagent-dispatch.md +2 -4
  89. package/template/.zcode/scaffold/.cowork-flow/spec/runtime/rules.json +61 -10
  90. package/template/.zcode/scaffold/.cowork-flow/spec/schemas/rules.schema.json +10 -1
  91. package/template/.zcode/scaffold/.cowork-flow/workflow.md +18 -13
  92. package/template/.zcode/scaffold/AGENTS.md +18 -8
  93. package/template/AGENTS.md +1 -1
  94. package/template/skills/batch-mode/SKILL.md +140 -76
  95. package/template/skills/brainstorming/SKILL.md +2 -2
  96. package/template/skills/cowork-flow/SKILL.md +42 -0
  97. package/template/skills/party-mode/SHARED-BOUNDARIES.md +1 -1
  98. package/template/skills/party-mode/SKILL.md +48 -119
  99. package/template/.cowork-flow/scripts/common/gates/spec_validator.py +0 -53
  100. package/template/.cowork-flow/scripts/common/gates/validate_jsonl.py +0 -32
  101. package/template/skills/before-dev/SKILL.md +0 -41
  102. package/template/skills/check/SKILL.md +0 -53
  103. package/template/skills/check/simplification-guide.md +0 -47
  104. package/template/skills/continue/SKILL.md +0 -29
  105. package/template/skills/doubt-review/SKILL.md +0 -118
  106. package/template/skills/finish-work/SKILL.md +0 -32
  107. package/template/skills/party-mode-v2/SKILL.md +0 -75
  108. package/template/skills/start/SKILL.md +0 -78
  109. package/template/skills/tdd/SKILL.md +0 -74
  110. package/template/skills/update-spec/SKILL.md +0 -39
  111. package/template/skills/using-cowork-flow/SKILL.md +0 -80
@@ -1,71 +1,348 @@
1
1
  #!/usr/bin/env python3
2
- """Shared gate result helpers for cowork-flow lifecycle commands."""
2
+ """Typed, fail-closed gate pipeline for lifecycle commands."""
3
3
 
4
4
  from __future__ import annotations
5
5
 
6
- from dataclasses import dataclass, field
6
+ from collections.abc import Mapping
7
7
  from pathlib import Path
8
8
 
9
+ from .models import (
10
+ GateContext,
11
+ GateDefinition,
12
+ GateExecution,
13
+ GateResult,
14
+ ValidatorBinding,
15
+ Violation,
16
+ )
17
+ from .registry import GateLoadError, GateRegistry
9
18
 
10
- @dataclass
11
- class GateResult:
12
- """Normalized result returned by workflow gates."""
13
19
 
14
- scope: str
15
- task_dir: Path | None = None
16
- violations: list[dict] = field(default_factory=list)
20
+ STAGE_GATES = {
21
+ "task_start": ("runtime_rules",),
22
+ "task_review": (
23
+ "implementation",
24
+ "tdd_evidence",
25
+ "test_intent",
26
+ "runtime_rules",
27
+ "coding_standards",
28
+ "complexity",
29
+ ),
30
+ "task_complete": (
31
+ "tdd_evidence",
32
+ "test_intent",
33
+ "runtime_rules",
34
+ "coding_standards",
35
+ "complexity",
36
+ ),
37
+ }
38
+
39
+
40
+ def build_default_registry() -> GateRegistry:
41
+ registry = GateRegistry()
42
+ for binding in (
43
+ ValidatorBinding(
44
+ key="runtime_rules",
45
+ module="validate_rules",
46
+ function="validate_rules",
47
+ positional=("repo_root", "scope", "task_dir"),
48
+ ),
49
+ ValidatorBinding(
50
+ key="implementation",
51
+ module="validate_implementation",
52
+ function="validate_implementation",
53
+ positional=("repo_root", "task_dir"),
54
+ keyword={
55
+ "allow_spec_file_modifications": "allow_spec_file_modifications",
56
+ },
57
+ ),
58
+ ValidatorBinding(
59
+ key="tdd_evidence",
60
+ module="tdd_evidence",
61
+ function="validate_tdd_evidence",
62
+ positional=("task_dir",),
63
+ ),
64
+ ValidatorBinding(
65
+ key="test_intent",
66
+ module="test_intent",
67
+ function="validate_test_intent",
68
+ positional=("repo_root", "task_dir"),
69
+ ),
70
+ ValidatorBinding(
71
+ key="coding_standards",
72
+ module="validate_coding_standards",
73
+ function="validate_coding_standards",
74
+ positional=("repo_root", "task_dir"),
75
+ ),
76
+ ValidatorBinding(
77
+ key="complexity",
78
+ module="validate_coding_standards",
79
+ function="validate_complexity_signals",
80
+ positional=("repo_root", "task_dir"),
81
+ ),
82
+ ):
83
+ registry.register_validator(binding)
84
+
85
+ definitions = (
86
+ GateDefinition(
87
+ id="runtime_rules",
88
+ validator_key="runtime_rules",
89
+ required=True,
90
+ block_message="Spec enforcement blocked lifecycle transition",
91
+ log_violations=True,
92
+ ),
93
+ GateDefinition(
94
+ id="implementation",
95
+ validator_key="implementation",
96
+ required=True,
97
+ block_message="Implementation gate blocked task review",
98
+ warning_message="Implementation violations detected",
99
+ ),
100
+ GateDefinition(
101
+ id="tdd_evidence",
102
+ validator_key="tdd_evidence",
103
+ required=True,
104
+ block_message="TDD evidence gate blocked lifecycle transition",
105
+ ),
106
+ GateDefinition(
107
+ id="test_intent",
108
+ validator_key="test_intent",
109
+ required=True,
110
+ block_message="Test intent gate blocked lifecycle transition",
111
+ warning_message="Test intent review warnings",
112
+ ),
113
+ GateDefinition(
114
+ id="coding_standards",
115
+ validator_key="coding_standards",
116
+ required=True,
117
+ block_message="Coding standards gate blocked lifecycle transition",
118
+ ),
119
+ GateDefinition(
120
+ id="complexity",
121
+ validator_key="complexity",
122
+ required=False,
123
+ block_message="",
124
+ warning_message="Complexity review warnings",
125
+ ),
126
+ )
127
+ for definition in definitions:
128
+ registry.register_gate(definition)
129
+ return registry
130
+
131
+
132
+ def _pipeline_violation(
133
+ *,
134
+ rule_id: str,
135
+ context: GateContext,
136
+ definition: GateDefinition,
137
+ message: str,
138
+ fix_hint: str,
139
+ ) -> dict:
140
+ severity = "block" if definition.required else "warn"
141
+ return Violation(
142
+ rule_id=rule_id,
143
+ type="gate_pipeline",
144
+ severity=severity,
145
+ message=message,
146
+ file=definition.validator_key,
147
+ fix_hint=fix_hint,
148
+ scope=context.scope,
149
+ gate_id=definition.id,
150
+ error_code=rule_id,
151
+ ).to_dict()
17
152
 
18
- @classmethod
19
- def from_violations(
20
- cls,
21
- scope: str,
22
- violations: list[dict],
23
- task_dir: Path | None = None,
24
- ) -> "GateResult":
25
- return cls(scope=scope, task_dir=task_dir, violations=list(violations))
26
153
 
27
- @property
28
- def blockers(self) -> list[dict]:
154
+ def _normalize_violations(
155
+ raw_result: object,
156
+ context: GateContext,
157
+ definition: GateDefinition,
158
+ ) -> list[dict]:
159
+ if not isinstance(raw_result, list):
29
160
  return [
30
- violation
31
- for violation in self.violations
32
- if violation.get("severity") == "block"
161
+ _pipeline_violation(
162
+ rule_id="GATE-PROTOCOL-001",
163
+ context=context,
164
+ definition=definition,
165
+ message=(
166
+ f"Gate {definition.id} returned "
167
+ f"{type(raw_result).__name__}; expected a list of violations"
168
+ ),
169
+ fix_hint="Return list[dict] from the registered gate validator.",
170
+ )
33
171
  ]
34
172
 
35
- @property
36
- def blocked(self) -> bool:
37
- return bool(self.blockers)
38
-
39
- @property
40
- def exit_code(self) -> int:
41
- return 1 if self.blocked else 0
173
+ normalized: list[dict] = []
174
+ for raw_violation in raw_result:
175
+ if not isinstance(raw_violation, Mapping):
176
+ normalized.append(
177
+ _pipeline_violation(
178
+ rule_id="GATE-PROTOCOL-001",
179
+ context=context,
180
+ definition=definition,
181
+ message=(
182
+ f"Gate {definition.id} returned a non-object violation"
183
+ ),
184
+ fix_hint="Return violation dictionaries with stable metadata.",
185
+ )
186
+ )
187
+ continue
188
+ try:
189
+ normalized.append(
190
+ Violation.from_mapping(
191
+ raw_violation,
192
+ scope=context.scope,
193
+ gate_id=definition.id,
194
+ ).to_dict()
195
+ )
196
+ except ValueError as error:
197
+ normalized.append(
198
+ _pipeline_violation(
199
+ rule_id="GATE-PROTOCOL-001",
200
+ context=context,
201
+ definition=definition,
202
+ message=f"Gate {definition.id} returned invalid metadata: {error}",
203
+ fix_hint=(
204
+ "Return violations with rule_id, severity, and message."
205
+ ),
206
+ )
207
+ )
208
+ return normalized
42
209
 
43
210
 
44
- class GateRunner:
45
- """Run existing validators through a common gate interface."""
211
+ class GatePipeline:
212
+ """Execute the configured stage gates with fail-closed semantics."""
46
213
 
47
- def __init__(self, repo_root: Path) -> None:
48
- self.repo_root = Path(repo_root)
214
+ def __init__(self, registry: GateRegistry | None = None) -> None:
215
+ self.registry = registry or build_default_registry()
49
216
 
50
- def run(self, scope: str, task_dir: Path | None = None) -> GateResult:
51
- from .validate_rules import validate_rules
217
+ def run(self, context: GateContext) -> GateResult:
218
+ executions: list[GateExecution] = []
219
+ priority_violations: list[dict] = []
220
+ ordinary_violations: list[dict] = []
52
221
 
53
- normalized_task_dir = Path(task_dir) if task_dir is not None else None
54
- violations = validate_rules(self.repo_root, scope, normalized_task_dir)
55
- if scope in {"task_review", "task_complete"}:
56
- from .validate_coding_standards import validate_coding_standards
222
+ for gate_id in STAGE_GATES[context.scope]:
223
+ definition = self.registry.gate(gate_id)
224
+ try:
225
+ raw_result = self.registry.invoke(definition, context)
226
+ except GateLoadError as error:
227
+ violations = [
228
+ _pipeline_violation(
229
+ rule_id="GATE-LOAD-001",
230
+ context=context,
231
+ definition=definition,
232
+ message=f"Gate {definition.id} could not load: {error}",
233
+ fix_hint=(
234
+ "Restore the required validator module and function."
235
+ ),
236
+ )
237
+ ]
238
+ except Exception as error:
239
+ violations = [
240
+ _pipeline_violation(
241
+ rule_id="GATE-EXEC-001",
242
+ context=context,
243
+ definition=definition,
244
+ message=f"Gate {definition.id} failed during execution: {error}",
245
+ fix_hint=(
246
+ "Fix the validator exception; lifecycle gates fail closed."
247
+ ),
248
+ )
249
+ ]
250
+ else:
251
+ violations = _normalize_violations(
252
+ raw_result,
253
+ context,
254
+ definition,
255
+ )
57
256
 
58
- violations.extend(
59
- validate_coding_standards(self.repo_root, normalized_task_dir)
257
+ gate_result = GateResult.from_violations(
258
+ context.scope,
259
+ violations,
260
+ context.task_dir,
261
+ )
262
+ executions.append(
263
+ GateExecution(
264
+ definition=definition,
265
+ result=gate_result,
266
+ )
60
267
  )
61
- from .validate_coding_standards import validate_complexity_signals
268
+ for violation in gate_result.violations:
269
+ if str(violation.get("rule_id", "")).startswith("GATE-"):
270
+ priority_violations.append(violation)
271
+ else:
272
+ ordinary_violations.append(violation)
273
+
274
+ return GateResult(
275
+ scope=context.scope,
276
+ task_dir=context.task_dir,
277
+ violations=[*priority_violations, *ordinary_violations],
278
+ executions=executions,
279
+ )
280
+
62
281
 
63
- violations.extend(
64
- validate_complexity_signals(self.repo_root, normalized_task_dir)
282
+ class GateRunner:
283
+ """Compatibility facade used by lifecycle commands and tests."""
284
+
285
+ def __init__(
286
+ self,
287
+ repo_root: Path,
288
+ registry: GateRegistry | None = None,
289
+ ) -> None:
290
+ self.repo_root = Path(repo_root)
291
+ self.pipeline = GatePipeline(registry)
292
+
293
+ def run(
294
+ self,
295
+ scope: str,
296
+ task_dir: Path | None = None,
297
+ *,
298
+ allow_spec_file_modifications: bool = False,
299
+ ) -> GateResult:
300
+ return self.pipeline.run(
301
+ GateContext(
302
+ repo_root=self.repo_root,
303
+ scope=scope,
304
+ task_dir=task_dir,
305
+ allow_spec_file_modifications=allow_spec_file_modifications,
65
306
  )
66
- return GateResult.from_violations(scope, violations, normalized_task_dir)
307
+ )
308
+
309
+ def coding_standards_summary(self, task_dir: Path | None = None) -> str:
310
+ module = __import__(
311
+ "validate_coding_standards",
312
+ globals(),
313
+ locals(),
314
+ ["get_coding_standards_summary"],
315
+ 1,
316
+ )
317
+ return module.get_coding_standards_summary(self.repo_root, task_dir)
67
318
 
68
319
  def log(self, result: GateResult) -> None:
69
- from .validate_rules import log_violations
320
+ try:
321
+ module = __import__(
322
+ "validate_rules",
323
+ globals(),
324
+ locals(),
325
+ ["log_violations"],
326
+ 1,
327
+ )
328
+ except ImportError:
329
+ return
330
+ module.log_violations(
331
+ result.violations,
332
+ result.scope,
333
+ result.task_dir,
334
+ self.repo_root,
335
+ )
336
+
70
337
 
71
- log_violations(result.violations, result.scope, result.task_dir, self.repo_root)
338
+ __all__ = [
339
+ "GateContext",
340
+ "GateDefinition",
341
+ "GateExecution",
342
+ "GatePipeline",
343
+ "GateRegistry",
344
+ "GateResult",
345
+ "GateRunner",
346
+ "Violation",
347
+ "build_default_registry",
348
+ ]
@@ -0,0 +1,196 @@
1
+ #!/usr/bin/env python3
2
+ """Typed models shared by the cowork-flow gate pipeline."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Mapping
9
+
10
+
11
+ GATE_SCOPES = frozenset({"task_start", "task_review", "task_complete"})
12
+ GATE_SEVERITIES = frozenset({"block", "warn", "advisory"})
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class GateContext:
17
+ """Runtime inputs available to every gate validator."""
18
+
19
+ repo_root: Path
20
+ scope: str
21
+ task_dir: Path | None = None
22
+ allow_spec_file_modifications: bool = False
23
+
24
+ def __post_init__(self) -> None:
25
+ if self.scope not in GATE_SCOPES:
26
+ raise ValueError(f"unsupported gate scope: {self.scope}")
27
+ object.__setattr__(self, "repo_root", Path(self.repo_root))
28
+ if self.task_dir is not None:
29
+ object.__setattr__(self, "task_dir", Path(self.task_dir))
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class ValidatorBinding:
34
+ """Lazy module/function binding plus its GateContext argument mapping."""
35
+
36
+ key: str
37
+ module: str
38
+ function: str
39
+ positional: tuple[str, ...] = ()
40
+ keyword: Mapping[str, str] = field(default_factory=dict)
41
+
42
+
43
+ @dataclass(frozen=True)
44
+ class GateDefinition:
45
+ """Declarative gate definition registered once and reused by stages."""
46
+
47
+ id: str
48
+ validator_key: str
49
+ required: bool
50
+ block_message: str
51
+ warning_message: str | None = None
52
+ log_violations: bool = False
53
+
54
+ def __post_init__(self) -> None:
55
+ if not self.id.strip():
56
+ raise ValueError("gate id must be non-empty")
57
+ if not self.validator_key.strip():
58
+ raise ValueError(f"gate {self.id} validator_key must be non-empty")
59
+ if self.required and not self.block_message.strip():
60
+ raise ValueError(f"required gate {self.id} needs a block message")
61
+
62
+
63
+ @dataclass(frozen=True)
64
+ class Violation:
65
+ """Stable typed representation of a validator failure."""
66
+
67
+ rule_id: str
68
+ type: str
69
+ severity: str
70
+ message: str
71
+ file: str
72
+ fix_hint: str
73
+ scope: str
74
+ gate_id: str
75
+ passed: bool = False
76
+ error_code: str | None = None
77
+ extra: Mapping[str, object] = field(default_factory=dict)
78
+
79
+ def __post_init__(self) -> None:
80
+ if not self.rule_id.strip():
81
+ raise ValueError("violation rule_id must be non-empty")
82
+ if self.severity not in GATE_SEVERITIES:
83
+ raise ValueError(f"unsupported violation severity: {self.severity}")
84
+ if self.scope not in GATE_SCOPES:
85
+ raise ValueError(f"unsupported violation scope: {self.scope}")
86
+
87
+ @classmethod
88
+ def from_mapping(
89
+ cls,
90
+ raw: Mapping[str, object],
91
+ *,
92
+ scope: str,
93
+ gate_id: str,
94
+ ) -> "Violation":
95
+ rule_id = str(raw.get("rule_id") or raw.get("id") or "").strip()
96
+ severity = str(raw.get("severity") or "").strip()
97
+ message = str(raw.get("message") or "").strip()
98
+ if not rule_id or not severity or not message:
99
+ raise ValueError("validator violation must define rule_id, severity, and message")
100
+
101
+ known_fields = {
102
+ "rule_id",
103
+ "id",
104
+ "type",
105
+ "severity",
106
+ "message",
107
+ "file",
108
+ "fix_hint",
109
+ "scope",
110
+ "gate_id",
111
+ "passed",
112
+ "error_code",
113
+ }
114
+ extra = {key: value for key, value in raw.items() if key not in known_fields}
115
+ return cls(
116
+ rule_id=rule_id,
117
+ type=str(raw.get("type") or "gate"),
118
+ severity=severity,
119
+ message=message,
120
+ file=str(raw.get("file") or ""),
121
+ fix_hint=str(raw.get("fix_hint") or ""),
122
+ scope=scope,
123
+ gate_id=gate_id,
124
+ passed=bool(raw.get("passed", False)),
125
+ error_code=str(raw.get("error_code") or rule_id),
126
+ extra=extra,
127
+ )
128
+
129
+ def to_dict(self) -> dict:
130
+ result = {
131
+ "rule_id": self.rule_id,
132
+ "type": self.type,
133
+ "severity": self.severity,
134
+ "passed": self.passed,
135
+ "message": self.message,
136
+ "file": self.file,
137
+ "fix_hint": self.fix_hint,
138
+ "scope": self.scope,
139
+ "gate_id": self.gate_id,
140
+ "error_code": self.error_code or self.rule_id,
141
+ }
142
+ result.update(self.extra)
143
+ return result
144
+
145
+
146
+ @dataclass
147
+ class GateResult:
148
+ """Normalized aggregate or per-gate result."""
149
+
150
+ scope: str
151
+ task_dir: Path | None = None
152
+ violations: list[dict] = field(default_factory=list)
153
+ executions: list["GateExecution"] = field(default_factory=list)
154
+
155
+ @classmethod
156
+ def from_violations(
157
+ cls,
158
+ scope: str,
159
+ violations: list[dict],
160
+ task_dir: Path | None = None,
161
+ ) -> "GateResult":
162
+ normalized = []
163
+ for violation in violations:
164
+ item = dict(violation)
165
+ item.setdefault("scope", scope)
166
+ item.setdefault("error_code", item.get("rule_id") or item.get("id"))
167
+ normalized.append(item)
168
+ return cls(scope=scope, task_dir=task_dir, violations=normalized)
169
+
170
+ @property
171
+ def blockers(self) -> list[dict]:
172
+ return [
173
+ violation
174
+ for violation in self.violations
175
+ if violation.get("severity") == "block"
176
+ ]
177
+
178
+ @property
179
+ def blocked(self) -> bool:
180
+ return bool(self.blockers)
181
+
182
+ @property
183
+ def exit_code(self) -> int:
184
+ return 1 if self.blocked else 0
185
+
186
+
187
+ @dataclass(frozen=True)
188
+ class GateExecution:
189
+ """One gate definition evaluated within a pipeline stage."""
190
+
191
+ definition: GateDefinition
192
+ result: GateResult
193
+
194
+ @property
195
+ def blocked(self) -> bool:
196
+ return self.result.blocked
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ """Gate definition and validator registry."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from collections import OrderedDict
7
+ from collections.abc import Callable
8
+
9
+ from .models import GateContext, GateDefinition, ValidatorBinding
10
+
11
+
12
+ class GateRegistryError(ValueError):
13
+ """Raised when registry declarations are internally inconsistent."""
14
+
15
+
16
+ class GateLoadError(RuntimeError):
17
+ """Raised when a registered validator cannot be loaded."""
18
+
19
+
20
+ class GateRegistry:
21
+ """Register gate definitions and lazily resolve validator functions."""
22
+
23
+ def __init__(self) -> None:
24
+ self._bindings: OrderedDict[str, ValidatorBinding] = OrderedDict()
25
+ self._definitions: OrderedDict[str, GateDefinition] = OrderedDict()
26
+
27
+ def register_validator(self, binding: ValidatorBinding) -> None:
28
+ if not binding.key.strip():
29
+ raise GateRegistryError("validator key must be non-empty")
30
+ if binding.key in self._bindings:
31
+ raise GateRegistryError(f"duplicate validator key: {binding.key}")
32
+ self._bindings[binding.key] = binding
33
+
34
+ def register_gate(self, definition: GateDefinition) -> None:
35
+ if definition.id in self._definitions:
36
+ raise GateRegistryError(f"duplicate gate id: {definition.id}")
37
+ if definition.validator_key not in self._bindings:
38
+ raise GateRegistryError(
39
+ f"gate {definition.id} references unknown validator: "
40
+ f"{definition.validator_key}"
41
+ )
42
+ self._definitions[definition.id] = definition
43
+
44
+ def gate(self, gate_id: str) -> GateDefinition:
45
+ try:
46
+ return self._definitions[gate_id]
47
+ except KeyError as error:
48
+ raise GateRegistryError(f"unknown gate id: {gate_id}") from error
49
+
50
+ def invoke(self, definition: GateDefinition, context: GateContext) -> object:
51
+ binding = self._bindings[definition.validator_key]
52
+ validator = self._load(binding)
53
+ args = [getattr(context, name) for name in binding.positional]
54
+ kwargs = {
55
+ argument: getattr(context, context_field)
56
+ for argument, context_field in binding.keyword.items()
57
+ }
58
+ return validator(*args, **kwargs)
59
+
60
+ @staticmethod
61
+ def _load(binding: ValidatorBinding) -> Callable:
62
+ try:
63
+ module = __import__(
64
+ binding.module,
65
+ globals(),
66
+ locals(),
67
+ [binding.function],
68
+ 1,
69
+ )
70
+ validator = getattr(module, binding.function)
71
+ except (ImportError, AttributeError) as error:
72
+ raise GateLoadError(
73
+ f"{binding.module}.{binding.function}: {error}"
74
+ ) from error
75
+ if not callable(validator):
76
+ raise GateLoadError(
77
+ f"{binding.module}.{binding.function} is not callable"
78
+ )
79
+ return validator