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
deviate/cli/review.py ADDED
@@ -0,0 +1,441 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import logging
5
+ import subprocess
6
+ import sys
7
+ from datetime import datetime, timezone
8
+ from pathlib import Path
9
+
10
+ import typer
11
+
12
+ from deviate.cli._common import console
13
+ from deviate.core._shared import git_env as _git_env
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ _SOURCE_EXTENSIONS: frozenset[str] = frozenset(
18
+ {
19
+ ".py",
20
+ ".scm",
21
+ ".js",
22
+ ".mjs",
23
+ ".cjs",
24
+ ".ts",
25
+ ".mts",
26
+ ".cts",
27
+ ".tsx",
28
+ ".rs",
29
+ ".go",
30
+ ".cpp",
31
+ ".cc",
32
+ ".cxx",
33
+ ".hpp",
34
+ ".h",
35
+ ".ex",
36
+ ".exs",
37
+ ".cs",
38
+ ".sh",
39
+ ".bash",
40
+ ".zsh",
41
+ ".kt",
42
+ ".kts",
43
+ ".swift",
44
+ }
45
+ )
46
+
47
+
48
+ def _is_source_file(filepath: str) -> bool:
49
+ """Check if a filepath corresponds to source code with a meaningful AST."""
50
+ stem, _, ext = filepath.rpartition(".")
51
+ return ("." + ext) in _SOURCE_EXTENSIONS if ext else False
52
+
53
+
54
+ review_app = typer.Typer(no_args_is_help=True)
55
+
56
+
57
+ @review_app.command()
58
+ def pre(
59
+ base: str = typer.Option(
60
+ "main", "--base", help="Base branch for merge-base computation"
61
+ ),
62
+ branch: str | None = typer.Option(
63
+ None, "--branch", help="Target branch for self-contained review"
64
+ ),
65
+ ) -> None:
66
+ """Gather git state and governance context for review."""
67
+ repo = Path.cwd()
68
+
69
+ target = branch or "HEAD"
70
+ branch_name = branch or _get_current_branch(repo)
71
+
72
+ diff = _compute_diff(repo, base, target)
73
+ entries = _compute_structured_diff(repo, base, target)
74
+ structured_diff_markdown = _format_structured_diff_markdown(entries)
75
+ constitution_path = _resolve_constitution_path(repo)
76
+ prd_path, prd_warning = _resolve_prd(branch_name, repo)
77
+ report_exists = _check_existing_reports(repo)
78
+
79
+ contract = {
80
+ "status": "READY",
81
+ "diff": diff,
82
+ "structured_diff": entries,
83
+ "structured_diff_markdown": structured_diff_markdown,
84
+ "constitution_path": constitution_path,
85
+ "constitution_warning": constitution_path is None,
86
+ "prd_path": prd_path,
87
+ "prd_warning": prd_warning,
88
+ "base_branch": base,
89
+ "report_exists": report_exists,
90
+ "timestamp": datetime.now(timezone.utc).isoformat(),
91
+ }
92
+
93
+ print(json.dumps(contract, indent=2))
94
+
95
+
96
+ def _get_current_branch(repo: Path) -> str | None:
97
+ """Get current git branch name."""
98
+ try:
99
+ return subprocess.run(
100
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
101
+ cwd=repo,
102
+ env=_git_env(),
103
+ capture_output=True,
104
+ text=True,
105
+ check=True,
106
+ ).stdout.strip()
107
+ except (subprocess.CalledProcessError, FileNotFoundError):
108
+ return None
109
+
110
+
111
+ def _compute_merge_base(commit_a: str, commit_b: str, repo: Path) -> str:
112
+ """Compute merge base between two commits."""
113
+ try:
114
+ return subprocess.run(
115
+ ["git", "merge-base", commit_a, commit_b],
116
+ cwd=repo,
117
+ env=_git_env(),
118
+ capture_output=True,
119
+ text=True,
120
+ check=True,
121
+ ).stdout.strip()
122
+ except (subprocess.CalledProcessError, FileNotFoundError):
123
+ return ""
124
+
125
+
126
+ def _gather_diff(base: str, head: str, repo: Path) -> str:
127
+ """Gather unified diff between base and head commits."""
128
+ try:
129
+ return subprocess.run(
130
+ ["git", "diff", f"{base}..{head}"],
131
+ cwd=repo,
132
+ env=_git_env(),
133
+ capture_output=True,
134
+ text=True,
135
+ check=True,
136
+ ).stdout
137
+ except (subprocess.CalledProcessError, FileNotFoundError):
138
+ return ""
139
+
140
+
141
+ def _compute_diff(repo: Path, base: str = "main", target_branch: str = "HEAD") -> str:
142
+ """Compute unified diff against merge-base with given base branch."""
143
+ merge_base = _compute_merge_base(base, target_branch, repo)
144
+ if not merge_base:
145
+ return ""
146
+ return _gather_diff(merge_base, target_branch, repo)
147
+
148
+
149
+ def _resolve_constitution_path(repo: Path) -> str | None:
150
+ """Resolve specs/constitution.md path if it exists."""
151
+ path = repo / "specs" / "constitution.md"
152
+ if path.exists():
153
+ return str(path.resolve())
154
+ return None
155
+
156
+
157
+ def _resolve_prd(branch_name: str | None, repo: Path) -> tuple[str | None, bool]:
158
+ """Resolve PRD path with epic priority over adhoc fallback."""
159
+ epic_slug = None
160
+ if branch_name:
161
+ parts = branch_name.split("/")
162
+ if len(parts) > 1:
163
+ epic_slug = parts[1]
164
+
165
+ if epic_slug:
166
+ epic_prd = repo / "specs" / epic_slug / "prd.md"
167
+ if epic_prd.exists():
168
+ return str(epic_prd.resolve()), False
169
+
170
+ adhoc_prd = repo / "specs" / "adhoc" / "prd.md"
171
+ if adhoc_prd.exists():
172
+ return str(adhoc_prd.resolve()), False
173
+
174
+ return None, True
175
+
176
+
177
+ def _reports_dir(repo: Path) -> Path:
178
+ """Resolve the .deviate/review/reports/ directory path."""
179
+ return repo / ".deviate" / "review" / "reports"
180
+
181
+
182
+ def _check_existing_reports(repo: Path) -> bool:
183
+ """Check if review reports already exist under .deviate/review/reports/."""
184
+ reports_dir = _reports_dir(repo)
185
+ if not reports_dir.is_dir():
186
+ return False
187
+ return any(reports_dir.iterdir())
188
+
189
+
190
+ def _parse_diff_filepaths(diff_text: str) -> list[str]:
191
+ paths: list[str] = []
192
+ for line in diff_text.splitlines():
193
+ if line.startswith("diff --git"):
194
+ parts = line.split()
195
+ if len(parts) >= 4:
196
+ b_path = parts[-1].lstrip("b/")
197
+ paths.append(b_path)
198
+ return paths
199
+
200
+
201
+ def _compute_file_stats(diff_text: str, target_filepath: str) -> dict:
202
+ """Compute file-level stats: net_lines_changed, chunks_changed, chunks."""
203
+ added = 0
204
+ removed = 0
205
+ chunks = 0
206
+ in_target = False
207
+ for line in diff_text.splitlines():
208
+ if line.startswith("diff --git"):
209
+ parts = line.split()
210
+ b_path = parts[-1].lstrip("b/") if len(parts) >= 4 else ""
211
+ in_target = b_path == target_filepath
212
+ continue
213
+ if not in_target:
214
+ continue
215
+ if line.startswith("@@"):
216
+ chunks += 1
217
+ continue
218
+ if (
219
+ line.startswith("--- ")
220
+ or line.startswith("+++ ")
221
+ or line.startswith("index ")
222
+ ):
223
+ continue
224
+ if line.startswith("new file"):
225
+ continue
226
+ if line.startswith("deleted file"):
227
+ continue
228
+ if line.startswith("+"):
229
+ added += 1
230
+ elif line.startswith("-"):
231
+ removed += 1
232
+ net_str = f"+{added}/-{removed}"
233
+ return {
234
+ "net_lines_changed": net_str,
235
+ "lines_added": added,
236
+ "lines_removed": removed,
237
+ "chunks_changed": chunks,
238
+ }
239
+
240
+
241
+ def _parse_diff_imports(
242
+ diff_text: str, target_filepath: str, extract_imports_fn
243
+ ) -> dict:
244
+ """Parse added/removed import lines from diff for target filepath."""
245
+ added: list[str] = []
246
+ removed: list[str] = []
247
+ in_target = False
248
+ for line in diff_text.splitlines():
249
+ if line.startswith("diff --git"):
250
+ parts = line.split()
251
+ b_path = parts[-1].lstrip("b/") if len(parts) >= 4 else ""
252
+ in_target = b_path == target_filepath
253
+ continue
254
+ if not in_target:
255
+ continue
256
+ if (
257
+ line.startswith("--- ")
258
+ or line.startswith("+++ ")
259
+ or line.startswith("index ")
260
+ ):
261
+ continue
262
+ if line.startswith("@@"):
263
+ continue
264
+ content = line[1:] if len(line) > 1 else ""
265
+ stripped = content.strip()
266
+ if not stripped:
267
+ continue
268
+ if line.startswith("+"):
269
+ if extract_imports_fn(stripped):
270
+ added.append(stripped)
271
+ elif line.startswith("-"):
272
+ if extract_imports_fn(stripped):
273
+ removed.append(stripped)
274
+ return {"imports_added": added, "imports_removed": removed}
275
+
276
+
277
+ def _is_import_line(line: str) -> bool:
278
+ """Heuristic check if a line looks like an import/include/using directive."""
279
+ lower = line.lower().strip()
280
+ if lower.startswith(
281
+ (
282
+ "import ",
283
+ "from ",
284
+ "using ",
285
+ "include ",
286
+ "#include",
287
+ "use ",
288
+ "extern crate",
289
+ "require(",
290
+ )
291
+ ):
292
+ return True
293
+ if lower.startswith("const ") and ("require(" in lower or "import(" in lower):
294
+ return True
295
+ if lower.startswith("#") and "include" in lower:
296
+ return True
297
+ return False
298
+
299
+
300
+ def _build_file_entry(
301
+ filepath: str, language: str, symbols_raw, diff_text: str
302
+ ) -> dict:
303
+ """Build a file entry dict for the structured diff contract."""
304
+ stats = _compute_file_stats(diff_text, filepath)
305
+ entry: dict = {
306
+ "file": filepath,
307
+ "language": language,
308
+ "net_lines_changed": stats["net_lines_changed"],
309
+ "lines_added": stats["lines_added"],
310
+ "lines_removed": stats["lines_removed"],
311
+ "chunks_changed": stats["chunks_changed"],
312
+ }
313
+ symbols_list: list[dict] = []
314
+ for sc in symbols_raw:
315
+ sym: dict[str, str | int] = {
316
+ "k": sc.kind,
317
+ "n": sc.name,
318
+ "c": sc.change,
319
+ }
320
+ if sc.start_line or sc.end_line:
321
+ sym["L"] = f"{sc.start_line}-{sc.end_line}"
322
+ if sc.old_start_line or sc.old_end_line:
323
+ sym["LO"] = f"{sc.old_start_line}-{sc.old_end_line}"
324
+ if sc.old_signature:
325
+ sym["SO"] = sc.old_signature[:80]
326
+ if sc.new_signature:
327
+ sym["SN"] = sc.new_signature[:80]
328
+ if sc.old_line_count or sc.new_line_count:
329
+ sym["S"] = f"{sc.old_line_count}→{sc.new_line_count}"
330
+ symbols_list.append(sym)
331
+ entry["symbols"] = symbols_list
332
+ imports = _parse_diff_imports(diff_text, filepath, _is_import_line)
333
+ if imports["imports_added"] or imports["imports_removed"]:
334
+ entry["+I"] = imports["imports_added"]
335
+ entry["-I"] = imports["imports_removed"]
336
+ return entry
337
+
338
+
339
+ def _format_structured_diff_markdown(entries: list[dict]) -> str:
340
+ """Render structured diff as a compact markdown table — token-efficient for LLM."""
341
+ sections: list[str] = []
342
+ for ent in entries:
343
+ fp = ent["file"]
344
+ lang = ent["language"]
345
+ net = ent["net_lines_changed"]
346
+ lines = [f"### {fp} ({lang}, {net})"]
347
+ syms = ent.get("symbols", [])
348
+ if syms:
349
+ lines.append("| k | n | c | L | LO | S | SO | SN |")
350
+ lines.append("|---|---|---|---|---|---|---|---|")
351
+ for s in syms:
352
+ lines.append(
353
+ f"| {s.get('k', '-')} "
354
+ f"| {s.get('n', '-')} "
355
+ f"| {s.get('c', '-')} "
356
+ f"| {s.get('L', '-')} "
357
+ f"| {s.get('LO', '-')} "
358
+ f"| {s.get('S', '-')} "
359
+ f"| {s.get('SO', '-')[:40]} "
360
+ f"| {s.get('SN', '-')[:40]} |"
361
+ )
362
+ added = ent.get("+I", [])
363
+ removed = ent.get("-I", [])
364
+ if added or removed:
365
+ lines.append("")
366
+ if added:
367
+ for imp in added:
368
+ lines.append(f"+ `{imp}`")
369
+ if removed:
370
+ for imp in removed:
371
+ lines.append(f"- `{imp}`")
372
+ sections.append("\n".join(lines))
373
+ return "\n\n".join(sections) if sections else "(no source changes)"
374
+
375
+
376
+ def _compute_structured_diff(repo: Path, base: str, target: str) -> list[dict]:
377
+ """Compute structured AST diff entries for ALL changed files.
378
+
379
+ Source files get full symbol-level AST diff breakdown.
380
+ Non-source files get empty symbols and ``unknown`` language.
381
+ Returns empty list when no diff or tree-sitter unavailable.
382
+ """
383
+ merge_base = _compute_merge_base(base, target, repo)
384
+ if not merge_base:
385
+ return []
386
+
387
+ diff_text = _gather_diff(merge_base, target, repo)
388
+ if not diff_text.strip():
389
+ return []
390
+
391
+ try:
392
+ from deviate.core.treesitter import extract_changed_symbols, get_language_id
393
+ except ImportError:
394
+ logger.warning("tree-sitter not available — skipping structured diff")
395
+ return []
396
+
397
+ filepaths = _parse_diff_filepaths(diff_text)
398
+ entries: list[dict] = []
399
+
400
+ for filepath in filepaths:
401
+ try:
402
+ if _is_source_file(filepath):
403
+ symbols = extract_changed_symbols(diff_text, filepath)
404
+ language = (
405
+ symbols[0].language
406
+ if symbols
407
+ else get_language_id(filepath) or "unknown"
408
+ )
409
+ else:
410
+ symbols = []
411
+ language = "unknown"
412
+ entries.append(_build_file_entry(filepath, language, symbols, diff_text))
413
+ except (LookupError, TypeError, ValueError):
414
+ logger.warning("Failed to compute structured diff for: %s", filepath)
415
+
416
+ return entries
417
+
418
+
419
+ @review_app.command()
420
+ def post(
421
+ content: str | None = typer.Argument(
422
+ None, help="Report markdown content. If not provided, reads from stdin."
423
+ ),
424
+ ) -> None:
425
+ """Persist review report and mark review complete."""
426
+ if not content:
427
+ if not sys.stdin.isatty():
428
+ content = sys.stdin.read()
429
+
430
+ if not content:
431
+ console.print("[yellow]SKIP[/] no report content provided")
432
+ raise typer.Exit(code=0)
433
+
434
+ repo = Path.cwd()
435
+ reports_dir = _reports_dir(repo)
436
+ reports_dir.mkdir(parents=True, exist_ok=True)
437
+
438
+ timestamp = datetime.now(timezone.utc).strftime("%Y%m%dT%H%M%S")
439
+ report_file = reports_dir / f"review-report-{timestamp}.md"
440
+ report_file.write_text(content, encoding="utf-8")
441
+ console.print(f"[green]OK[/] report written to {report_file}")
File without changes
@@ -0,0 +1,20 @@
1
+ from __future__ import annotations
2
+
3
+ import os
4
+
5
+
6
+ def git_env() -> dict[str, str]:
7
+ """Return os.environ with GIT_* and GH_* stripped.
8
+
9
+ Production code MUST pass this as the `env` of every `git`/`gh`/`gt`
10
+ subprocess call so child processes don't inherit the parent's git
11
+ identity, remotes, or auth state. Prefer creating branch refs
12
+ (`git branch <name>`) over `git checkout -b` in non-interactive code;
13
+ if a checkout is unavoidable, save `git rev-parse --abbrev-ref HEAD`
14
+ first and restore it afterwards.
15
+ """
16
+ return {
17
+ k: v
18
+ for k, v in os.environ.items()
19
+ if not k.startswith("GIT_") and not k.startswith("GH_")
20
+ }