deviatdd 2.5.1__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 (124) hide show
  1. deviatdd-2.5.1.dist-info/METADATA +386 -0
  2. deviatdd-2.5.1.dist-info/RECORD +124 -0
  3. deviatdd-2.5.1.dist-info/WHEEL +4 -0
  4. deviatdd-2.5.1.dist-info/entry_points.txt +2 -0
  5. deviatdd-2.5.1.dist-info/licenses/LICENSE +21 -0
  6. deviate/__init__.py +3 -0
  7. deviate/cli/__init__.py +824 -0
  8. deviate/cli/_common.py +155 -0
  9. deviate/cli/adhoc.py +183 -0
  10. deviate/cli/constitution.py +113 -0
  11. deviate/cli/feature.py +94 -0
  12. deviate/cli/init.py +485 -0
  13. deviate/cli/inspect.py +208 -0
  14. deviate/cli/macro.py +937 -0
  15. deviate/cli/meso.py +1894 -0
  16. deviate/cli/micro.py +3249 -0
  17. deviate/cli/review.py +441 -0
  18. deviate/core/__init__.py +0 -0
  19. deviate/core/_shared.py +20 -0
  20. deviate/core/agent.py +505 -0
  21. deviate/core/cache_discipline.py +65 -0
  22. deviate/core/commands.py +202 -0
  23. deviate/core/commit.py +86 -0
  24. deviate/core/complexity.py +36 -0
  25. deviate/core/constitution.py +85 -0
  26. deviate/core/contract.py +17 -0
  27. deviate/core/convention.py +147 -0
  28. deviate/core/epic.py +73 -0
  29. deviate/core/issues.py +46 -0
  30. deviate/core/prd.py +18 -0
  31. deviate/core/profile.py +33 -0
  32. deviate/core/repo.py +47 -0
  33. deviate/core/run_logger.py +50 -0
  34. deviate/core/tasks_ledger.py +69 -0
  35. deviate/core/treesitter/__init__.py +31 -0
  36. deviate/core/treesitter/analysis.py +457 -0
  37. deviate/core/treesitter/models.py +35 -0
  38. deviate/core/treesitter/parser.py +184 -0
  39. deviate/core/treesitter/queries/bash.scm +7 -0
  40. deviate/core/treesitter/queries/c_sharp.scm +11 -0
  41. deviate/core/treesitter/queries/cpp.scm +9 -0
  42. deviate/core/treesitter/queries/css.scm +4 -0
  43. deviate/core/treesitter/queries/dockerfile.scm +8 -0
  44. deviate/core/treesitter/queries/elixir.scm +6 -0
  45. deviate/core/treesitter/queries/go.scm +9 -0
  46. deviate/core/treesitter/queries/hcl.scm +3 -0
  47. deviate/core/treesitter/queries/html.scm +3 -0
  48. deviate/core/treesitter/queries/javascript.scm +12 -0
  49. deviate/core/treesitter/queries/json.scm +4 -0
  50. deviate/core/treesitter/queries/kotlin.scm +8 -0
  51. deviate/core/treesitter/queries/markdown.scm +4 -0
  52. deviate/core/treesitter/queries/python.scm +10 -0
  53. deviate/core/treesitter/queries/rust.scm +12 -0
  54. deviate/core/treesitter/queries/sql.scm +7 -0
  55. deviate/core/treesitter/queries/swift.scm +10 -0
  56. deviate/core/treesitter/queries/toml.scm +3 -0
  57. deviate/core/treesitter/queries/tsx.scm +14 -0
  58. deviate/core/treesitter/queries/typescript.scm +14 -0
  59. deviate/core/treesitter/queries/yaml.scm +4 -0
  60. deviate/core/validation.py +153 -0
  61. deviate/core/worktree.py +141 -0
  62. deviate/main.py +4 -0
  63. deviate/prompts/__init__.py +0 -0
  64. deviate/prompts/assembly.py +122 -0
  65. deviate/prompts/auto/__init__.py +1 -0
  66. deviate/prompts/auto/execute.md +62 -0
  67. deviate/prompts/auto/explore.md +103 -0
  68. deviate/prompts/auto/green.md +137 -0
  69. deviate/prompts/auto/judge.md +260 -0
  70. deviate/prompts/auto/plan.md +127 -0
  71. deviate/prompts/auto/prd.md +108 -0
  72. deviate/prompts/auto/red.md +156 -0
  73. deviate/prompts/auto/refactor.md +166 -0
  74. deviate/prompts/auto/research.md +111 -0
  75. deviate/prompts/auto/shard.md +90 -0
  76. deviate/prompts/auto/specify.md +77 -0
  77. deviate/prompts/auto/tasks.md +191 -0
  78. deviate/prompts/commands/deviate-adhoc.md +216 -0
  79. deviate/prompts/commands/deviate-architecture.md +147 -0
  80. deviate/prompts/commands/deviate-constitution.md +215 -0
  81. deviate/prompts/commands/deviate-e2e.md +264 -0
  82. deviate/prompts/commands/deviate-execute.md +223 -0
  83. deviate/prompts/commands/deviate-explore.md +253 -0
  84. deviate/prompts/commands/deviate-flows.md +226 -0
  85. deviate/prompts/commands/deviate-green.md +217 -0
  86. deviate/prompts/commands/deviate-hotfix.md +188 -0
  87. deviate/prompts/commands/deviate-init.md +170 -0
  88. deviate/prompts/commands/deviate-judge.md +193 -0
  89. deviate/prompts/commands/deviate-merge.md +221 -0
  90. deviate/prompts/commands/deviate-plan.md +158 -0
  91. deviate/prompts/commands/deviate-pr.md +144 -0
  92. deviate/prompts/commands/deviate-prd.md +216 -0
  93. deviate/prompts/commands/deviate-prune.md +260 -0
  94. deviate/prompts/commands/deviate-red.md +231 -0
  95. deviate/prompts/commands/deviate-refactor.md +211 -0
  96. deviate/prompts/commands/deviate-release.md +123 -0
  97. deviate/prompts/commands/deviate-research.md +359 -0
  98. deviate/prompts/commands/deviate-review.md +289 -0
  99. deviate/prompts/commands/deviate-shard.md +226 -0
  100. deviate/prompts/commands/deviate-tasks.md +281 -0
  101. deviate/prompts/commands/deviate-triage.md +141 -0
  102. deviate/prompts/constitution_seed.md +51 -0
  103. deviate/prompts/core/core.md +21 -0
  104. deviate/prompts/core/macro-auto.md +59 -0
  105. deviate/prompts/core/macro-command.md +54 -0
  106. deviate/prompts/core/macro-skill.md +54 -0
  107. deviate/prompts/core/meso-auto.md +63 -0
  108. deviate/prompts/core/meso-command.md +58 -0
  109. deviate/prompts/core/meso-skill.md +58 -0
  110. deviate/prompts/core/micro-auto.md +76 -0
  111. deviate/prompts/core/micro-command.md +67 -0
  112. deviate/prompts/core/micro-skill.md +67 -0
  113. deviate/prompts/extras/deviate-pr-graphite-routing.md +20 -0
  114. deviate/prompts/governance/__init__.py +0 -0
  115. deviate/prompts/governance/agents_seed.md +1 -0
  116. deviate/prompts/governance/claudemd_seed.md +1 -0
  117. deviate/prompts/governance/graphite_seed.md +18 -0
  118. deviate/prompts/governance/libref_seed.md +3 -0
  119. deviate/state/__init__.py +0 -0
  120. deviate/state/config.py +257 -0
  121. deviate/state/ledger.py +407 -0
  122. deviate/ui/__init__.py +5 -0
  123. deviate/ui/monitor.py +187 -0
  124. deviate/ui/render.py +26 -0
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+
6
+ @dataclass
7
+ class SymbolChange:
8
+ language: str
9
+ kind: str
10
+ name: str
11
+ change: str
12
+ old_name: str = ""
13
+ start_line: int = 0
14
+ end_line: int = 0
15
+ old_start_line: int = 0
16
+ old_end_line: int = 0
17
+ old_signature: str = ""
18
+ new_signature: str = ""
19
+ old_line_count: int = 0
20
+ new_line_count: int = 0
21
+
22
+
23
+ @dataclass
24
+ class DuplicateBlock:
25
+ lines: int
26
+ locations: list[str]
27
+ similarity: float
28
+
29
+
30
+ @dataclass
31
+ class FileStructure:
32
+ filepath: str
33
+ language: str
34
+ symbols: list[dict[str, str | int]] = field(default_factory=list)
35
+ imports: list[str] = field(default_factory=list)
@@ -0,0 +1,184 @@
1
+ from __future__ import annotations
2
+
3
+ import importlib
4
+ import logging
5
+ import os
6
+ import pathlib
7
+
8
+ from typing import Any
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ _QUERIES_DIR: pathlib.Path = pathlib.Path(__file__).resolve().parent / "queries"
13
+
14
+ EXTENSION_MAP: dict[str, str] = {
15
+ ".py": "python",
16
+ ".js": "javascript",
17
+ ".mjs": "javascript",
18
+ ".cjs": "javascript",
19
+ ".ts": "typescript",
20
+ ".mts": "typescript",
21
+ ".cts": "typescript",
22
+ ".tsx": "tsx",
23
+ ".rs": "rust",
24
+ ".go": "go",
25
+ ".cpp": "cpp",
26
+ ".cc": "cpp",
27
+ ".cxx": "cpp",
28
+ ".hpp": "cpp",
29
+ ".h": "cpp",
30
+ ".ex": "elixir",
31
+ ".exs": "elixir",
32
+ ".cs": "c_sharp",
33
+ ".md": "markdown",
34
+ ".mdx": "markdown",
35
+ ".sh": "bash",
36
+ ".bash": "bash",
37
+ ".zsh": "bash",
38
+ ".json": "json",
39
+ ".toml": "toml",
40
+ ".yaml": "yaml",
41
+ ".yml": "yaml",
42
+ ".html": "html",
43
+ ".htm": "html",
44
+ ".css": "css",
45
+ ".scss": "css",
46
+ ".less": "css",
47
+ ".sql": "sql",
48
+ "Dockerfile": "dockerfile",
49
+ ".dockerfile": "dockerfile",
50
+ ".tf": "hcl",
51
+ ".tfvars": "hcl",
52
+ ".kt": "kotlin",
53
+ ".kts": "kotlin",
54
+ ".swift": "swift",
55
+ }
56
+
57
+ _GRAMMAR_PACKAGES: dict[str, str] = {
58
+ "python": "tree_sitter_python",
59
+ "javascript": "tree_sitter_javascript",
60
+ "typescript": "tree_sitter_typescript",
61
+ "tsx": "tree_sitter_typescript",
62
+ "rust": "tree_sitter_rust",
63
+ "go": "tree_sitter_go",
64
+ "cpp": "tree_sitter_cpp",
65
+ "elixir": "tree_sitter_elixir",
66
+ "c_sharp": "tree_sitter_c_sharp",
67
+ "markdown": "tree_sitter_markdown",
68
+ "bash": "tree_sitter_bash",
69
+ "json": "tree_sitter_json",
70
+ "toml": "tree_sitter_toml",
71
+ "yaml": "tree_sitter_yaml",
72
+ "html": "tree_sitter_html",
73
+ "css": "tree_sitter_css",
74
+ "sql": "tree_sitter_sql",
75
+ "dockerfile": "tree_sitter_dockerfile",
76
+ "hcl": "tree_sitter_hcl",
77
+ "kotlin": "tree_sitter_kotlin",
78
+ "swift": "tree_sitter_swift",
79
+ }
80
+
81
+ _LANGUAGE_ATTRS: dict[str, str] = {
82
+ "typescript": "language_typescript",
83
+ "tsx": "language_tsx",
84
+ }
85
+
86
+ _CAP_KIND: dict[str, str] = {
87
+ "function": "function",
88
+ "method": "function",
89
+ "class": "class",
90
+ "struct": "class",
91
+ "interface": "interface",
92
+ "entry": "entry",
93
+ }
94
+
95
+ _tree_sitter_available = True
96
+ _parser_cache: dict[str, Any] = {}
97
+ _query_cache: dict[str, Any] = {}
98
+ _grammar_cache: dict[str, Any] = {}
99
+
100
+ try:
101
+ from tree_sitter import Language, Parser, Query, QueryCursor, Tree
102
+ except ImportError:
103
+ _tree_sitter_available = False
104
+ Language = None
105
+ Parser = None
106
+ Query = None
107
+ QueryCursor = None
108
+ Tree = None
109
+
110
+
111
+ def get_language_id(filepath: str) -> str | None:
112
+ ext = _get_extension(filepath)
113
+ return EXTENSION_MAP.get(ext)
114
+
115
+
116
+ def get_parser(filepath: str) -> Any | None:
117
+ if not _tree_sitter_available or Parser is None:
118
+ return None
119
+ lang_id = get_language_id(filepath)
120
+ if lang_id is None:
121
+ return None
122
+ if lang_id in _parser_cache:
123
+ return _parser_cache[lang_id]
124
+ lang_obj = _load_language(lang_id)
125
+ if lang_obj is None:
126
+ return None
127
+ try:
128
+ parser = Parser(lang_obj)
129
+ _parser_cache[lang_id] = parser
130
+ return parser
131
+ except Exception:
132
+ logger.warning("Failed to create parser for %s", lang_id)
133
+ return None
134
+
135
+
136
+ def _load_language(grammar_id: str) -> Any | None:
137
+ if not _tree_sitter_available or Language is None:
138
+ return None
139
+ if grammar_id in _grammar_cache:
140
+ return _grammar_cache[grammar_id]
141
+ pkg_name = _GRAMMAR_PACKAGES.get(grammar_id)
142
+ if pkg_name is None:
143
+ return None
144
+ try:
145
+ pkg = importlib.import_module(pkg_name)
146
+ lang_attr = _LANGUAGE_ATTRS.get(grammar_id, "language")
147
+ cap_fn = getattr(pkg, lang_attr, None) or pkg.language
148
+ capsule = cap_fn()
149
+ lang_obj = Language(capsule)
150
+ _grammar_cache[grammar_id] = lang_obj
151
+ return lang_obj
152
+ except Exception:
153
+ logger.warning("Failed to load language %s", grammar_id)
154
+ return None
155
+
156
+
157
+ def _get_extension(filepath: str) -> str:
158
+ name = os.path.basename(filepath)
159
+ if name == "Dockerfile":
160
+ return "Dockerfile"
161
+ _, ext = os.path.splitext(name)
162
+ return ext.lower() if ext else ""
163
+
164
+
165
+ def _load_queries(grammar_id: str) -> Any | None:
166
+ if not _tree_sitter_available or Query is None:
167
+ return None
168
+ if grammar_id in _query_cache:
169
+ return _query_cache[grammar_id]
170
+ qfile = _QUERIES_DIR / f"{grammar_id}.scm"
171
+ if not qfile.exists():
172
+ _query_cache[grammar_id] = None
173
+ return None
174
+ lang_obj = _load_language(grammar_id)
175
+ if lang_obj is None:
176
+ return None
177
+ try:
178
+ qtext = qfile.read_text()
179
+ query = Query(lang_obj, qtext)
180
+ _query_cache[grammar_id] = query
181
+ return query
182
+ except Exception:
183
+ logger.warning("Failed to compile query for %s", grammar_id)
184
+ return None
@@ -0,0 +1,7 @@
1
+ ; captures: function, call, import, conditional, loop
2
+ (function_definition name: (word) @function)
3
+ (command name: (command_name) @call)
4
+ (if_statement) @conditional
5
+ (for_statement) @loop
6
+ (while_statement) @loop
7
+ (case_statement) @conditional
@@ -0,0 +1,11 @@
1
+ ; captures: function, class, interface, call, import, conditional, loop
2
+ (method_declaration name: (identifier) @method)
3
+ (class_declaration name: (identifier) @class)
4
+ (struct_declaration name: (identifier) @struct)
5
+ (interface_declaration name: (identifier) @interface)
6
+ (record_declaration name: (identifier) @struct)
7
+ (invocation_expression) @call
8
+ (if_statement) @conditional
9
+ (for_statement) @loop
10
+ (while_statement) @loop
11
+ (switch_statement) @conditional
@@ -0,0 +1,9 @@
1
+ ; captures: function, class, call, import, conditional, loop
2
+ (function_definition declarator: (function_declarator declarator: (identifier) @function))
3
+ (class_specifier name: (type_identifier) @class)
4
+ (call_expression) @call
5
+ (using_declaration) @import
6
+ (if_statement) @conditional
7
+ (for_statement) @loop
8
+ (while_statement) @loop
9
+ (switch_statement) @conditional
@@ -0,0 +1,4 @@
1
+ ; captures: entry
2
+ (rule_set) @entry
3
+ (at_rule) @entry
4
+ (declaration) @entry
@@ -0,0 +1,8 @@
1
+ ; captures: entry
2
+ (from_instruction) @entry
3
+ (run_instruction) @entry
4
+ (cmd_instruction) @entry
5
+ (env_instruction) @entry
6
+ (expose_instruction) @entry
7
+ (copy_instruction) @entry
8
+ (entrypoint_instruction) @entry
@@ -0,0 +1,6 @@
1
+ ; captures: function, call, import, class, interface
2
+ ; Elixir uses 'call' nodes for all def/defmodule/defstruct etc.
3
+ ; We capture the first argument as the name
4
+ (call target: (identifier) @call)
5
+ (unary_operator operator: "@" (call target: (identifier) @call))
6
+ (alias) @import
@@ -0,0 +1,9 @@
1
+ ; captures: function, class, call, import, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (method_declaration name: (field_identifier) @method)
4
+ (call_expression) @call
5
+ (import_declaration) @import
6
+ (if_statement) @conditional
7
+ (for_statement) @loop
8
+ (expression_switch_statement) @conditional
9
+ (type_switch_statement) @conditional
@@ -0,0 +1,3 @@
1
+ ; captures: entry
2
+ (block) @entry
3
+ (attribute) @entry
@@ -0,0 +1,3 @@
1
+ ; captures: entry
2
+ (element) @entry
3
+ (attribute) @entry
@@ -0,0 +1,12 @@
1
+ ; captures: function, class, call, import, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (class_declaration name: (identifier) @class)
4
+ (method_definition name: (property_identifier) @method)
5
+ (arrow_function) @function
6
+ (call_expression) @call
7
+ (import_statement) @import
8
+ (export_statement) @function
9
+ (if_statement) @conditional
10
+ (for_statement) @loop
11
+ (while_statement) @loop
12
+ (switch_statement) @conditional
@@ -0,0 +1,4 @@
1
+ ; captures: entry
2
+ (pair) @entry
3
+ (object) @entry
4
+ (array) @entry
@@ -0,0 +1,8 @@
1
+ ; captures: function, class, call, interface, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (class_declaration name: (identifier) @class)
4
+ (object_declaration name: (identifier) @class)
5
+ (call_expression) @call
6
+ (if_expression) @conditional
7
+ (for_statement) @loop
8
+ (while_statement) @loop
@@ -0,0 +1,4 @@
1
+ ; captures: entry
2
+ (section) @entry
3
+ (fenced_code_block) @entry
4
+ (list_item) @entry
@@ -0,0 +1,10 @@
1
+ ; captures: function, class, call, import, conditional, loop
2
+ (function_definition name: (identifier) @function)
3
+ (class_definition name: (identifier) @class)
4
+ (call (identifier) @call)
5
+ (import_statement) @import
6
+ (import_from_statement) @import
7
+ (if_statement) @conditional
8
+ (for_statement) @loop
9
+ (while_statement) @loop
10
+ (match_statement) @conditional
@@ -0,0 +1,12 @@
1
+ ; captures: function, class, interface, call, import, conditional, loop
2
+ (function_item name: (identifier) @function)
3
+ (struct_item name: (type_identifier) @struct)
4
+ (enum_item name: (type_identifier) @enum)
5
+ (trait_item name: (type_identifier) @interface)
6
+ (impl_item) @class
7
+ (call_expression) @call
8
+ (use_declaration) @import
9
+ (if_expression) @conditional
10
+ (for_expression) @loop
11
+ (while_expression) @loop
12
+ (match_expression) @conditional
@@ -0,0 +1,7 @@
1
+ ; captures: function, entry
2
+ (create_function (object_reference) @function)
3
+ (create_table name: (identifier) @entry)
4
+ (select) @entry
5
+ (insert) @entry
6
+ (update) @entry
7
+ (delete) @entry
@@ -0,0 +1,10 @@
1
+ ; captures: function, class, interface, call, import, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (class_declaration name: (type_identifier) @class)
4
+ (protocol_declaration name: (type_identifier) @interface)
5
+ (call_expression) @call
6
+ (import_declaration) @import
7
+ (if_statement) @conditional
8
+ (for_statement) @loop
9
+ (while_statement) @loop
10
+ (switch_statement) @conditional
@@ -0,0 +1,3 @@
1
+ ; captures: entry
2
+ (table) @entry
3
+ (pair) @entry
@@ -0,0 +1,14 @@
1
+ ; captures: function, class, interface, call, import, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (class_declaration name: (type_identifier) @class)
4
+ (method_definition name: (property_identifier) @method)
5
+ (interface_declaration name: (type_identifier) @interface)
6
+ (type_alias_declaration name: (type_identifier) @interface)
7
+ (arrow_function) @function
8
+ (call_expression) @call
9
+ (import_statement) @import
10
+ (export_statement) @function
11
+ (if_statement) @conditional
12
+ (for_statement) @loop
13
+ (while_statement) @loop
14
+ (switch_statement) @conditional
@@ -0,0 +1,14 @@
1
+ ; captures: function, class, interface, call, import, conditional, loop
2
+ (function_declaration name: (identifier) @function)
3
+ (class_declaration name: (type_identifier) @class)
4
+ (method_definition name: (property_identifier) @method)
5
+ (interface_declaration name: (type_identifier) @interface)
6
+ (type_alias_declaration name: (type_identifier) @interface)
7
+ (arrow_function) @function
8
+ (call_expression) @call
9
+ (import_statement) @import
10
+ (export_statement) @function
11
+ (if_statement) @conditional
12
+ (for_statement) @loop
13
+ (while_statement) @loop
14
+ (switch_statement) @conditional
@@ -0,0 +1,4 @@
1
+ ; captures: entry
2
+ (block_mapping_pair) @entry
3
+ (block_sequence) @entry
4
+ (flow_mapping) @entry
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from collections import namedtuple
5
+
6
+ import yaml
7
+
8
+ ValidationResult = namedtuple("ValidationResult", ["passed", "errors", "warnings"])
9
+
10
+ ARTIFACT_VALIDATORS: dict[str, list[str]] = {
11
+ "explore": [
12
+ "Problem Definition",
13
+ "Discovery Audit Results",
14
+ "Constitution Quotes",
15
+ "Architectural Baselines",
16
+ "Ecosystem Research",
17
+ "File Registry",
18
+ "Status Summary",
19
+ ],
20
+ "design": [
21
+ "Recommended Architecture",
22
+ "Options Matrix",
23
+ "Rejected Options",
24
+ "Design Trade-Offs",
25
+ "Contrarian Viewpoints",
26
+ "Risk Register",
27
+ "Constitutional Alignment Audit",
28
+ "Pending HITL Decisions",
29
+ "Source Registry",
30
+ "Status Summary",
31
+ ],
32
+ "data_model": [
33
+ "Entity Definitions",
34
+ "Relationship Graph",
35
+ "Schema Tables",
36
+ "State Transitions",
37
+ "Data Flow",
38
+ "Source Registry",
39
+ ],
40
+ "prd": [
41
+ "Document Control and Metadata",
42
+ "System Objectives and Scope Boundary",
43
+ "Architectural Constraints and Prerequisites",
44
+ "Functional Flow and Sequence Architecture",
45
+ "Functional Requirements and Epics",
46
+ "Issue Sharding Strategy",
47
+ ],
48
+ }
49
+
50
+
51
+ def validate_artifact(content: str | None, artifact_type: str) -> ValidationResult:
52
+ required = ARTIFACT_VALIDATORS.get(artifact_type)
53
+ if required is None:
54
+ return ValidationResult(
55
+ passed=False,
56
+ errors=[f"unknown artifact type: {artifact_type}"],
57
+ warnings=[],
58
+ )
59
+ missing = validate_sections(content, required)
60
+ passed = len(missing) == 0
61
+ return ValidationResult(passed=passed, errors=missing, warnings=[])
62
+
63
+
64
+ def extract_section_body(content: str, header: str) -> str | None:
65
+ escaped_header = re.escape(header)
66
+ pattern = rf"^## {escaped_header}\s*$(.*?)(?=^## |\Z)"
67
+ match = re.search(pattern, content, re.MULTILINE | re.DOTALL)
68
+ if match:
69
+ return match.group(1)
70
+ return None
71
+
72
+
73
+ def validate_gherkin_syntax(content: str) -> list[str]:
74
+ errors: list[str] = []
75
+ scenario_pattern = re.compile(r"\*\*(?P<label>(?:Scenario \d+|AC-\d+-\d+)):.*?\*\*")
76
+ scenarios = list(scenario_pattern.finditer(content))
77
+ if not scenarios:
78
+ return errors
79
+ for i, match in enumerate(scenarios):
80
+ start = match.end()
81
+ end = scenarios[i + 1].start() if i + 1 < len(scenarios) else len(content)
82
+ body = content[start:end]
83
+ label = match.group("label")
84
+ if "**Given**" not in body:
85
+ errors.append(f"{label}: missing 'Given'")
86
+ if "**When**" not in body:
87
+ errors.append(f"{label}: missing 'When'")
88
+ if "**Then**" not in body:
89
+ errors.append(f"{label}: missing 'Then'")
90
+ return errors
91
+
92
+
93
+ def validate_sections(content: str | None, required: list[str]) -> list[str]:
94
+ if not content or not content.strip():
95
+ return list(required)
96
+ missing: list[str] = []
97
+ for section in required:
98
+ pattern = rf"^##\s+\[?{re.escape(section)}\]?\s*$"
99
+ if not re.search(pattern, content, re.MULTILINE):
100
+ missing.append(section)
101
+ return missing
102
+
103
+
104
+ def validate_yaml_frontmatter(content: str) -> bool:
105
+ if not content.lstrip().startswith("---"):
106
+ return False
107
+ end_idx = content.find("---", 3)
108
+ if end_idx == -1:
109
+ return False
110
+ frontmatter = content[3:end_idx].strip()
111
+ try:
112
+ yaml.safe_load(frontmatter)
113
+ return True
114
+ except yaml.YAMLError:
115
+ return False
116
+
117
+
118
+ def validate_task_id(task_id: str) -> bool:
119
+ if not task_id:
120
+ return False
121
+ return bool(re.match(r"^TSK-\d{3}-\d{2}$", task_id))
122
+
123
+
124
+ def validate_source_file(source_file: str, epic_slug: str) -> bool:
125
+ """Validate a shard manifest's ``source_file`` against the issue registry pattern.
126
+
127
+ The downstream ``deviate meso run`` command parses ``source_file`` via
128
+ ``PurePosixPath(source_file).parent.parent.name`` to derive the epic bucket
129
+ slug and ``PurePosixPath(source_file).stem`` to derive the issue slug used
130
+ for branch naming. Both rely on the strict shape
131
+ ``specs/<epic_slug>/issues/<file>.md``. Any deviation (e.g. a PRD or
132
+ design reference) silently produces wrong branch names and downstream
133
+ worktree failures.
134
+
135
+ Returns ``True`` only when *source_file* matches the expected pattern for
136
+ *epic_slug*; ``False`` otherwise.
137
+ """
138
+ if not source_file or not epic_slug:
139
+ return False
140
+ if source_file.startswith("/"):
141
+ return False
142
+ parts = source_file.split("/")
143
+ if len(parts) != 4:
144
+ return False
145
+ if parts[0] != "specs":
146
+ return False
147
+ if parts[1] != epic_slug:
148
+ return False
149
+ if parts[2] != "issues":
150
+ return False
151
+ if not parts[3].endswith(".md"):
152
+ return False
153
+ return True