boomtick-cli 0.2.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 (103) hide show
  1. boomtick_cli-0.2.1.dist-info/METADATA +20 -0
  2. boomtick_cli-0.2.1.dist-info/RECORD +103 -0
  3. boomtick_cli-0.2.1.dist-info/WHEEL +5 -0
  4. boomtick_cli-0.2.1.dist-info/entry_points.txt +3 -0
  5. boomtick_cli-0.2.1.dist-info/top_level.txt +1 -0
  6. dev_tools/__init__.py +1 -0
  7. dev_tools/cli-schema.json +2028 -0
  8. dev_tools/cli.py +1542 -0
  9. dev_tools/config.py +235 -0
  10. dev_tools/constants.py +14 -0
  11. dev_tools/daemon.py +146 -0
  12. dev_tools/dist/config.js +108 -0
  13. dev_tools/dist/index.js +10 -0
  14. dev_tools/dist/lib/error_utils.js +8 -0
  15. dev_tools/dist/lib/git.js +31 -0
  16. dev_tools/dist/lib/result.js +21 -0
  17. dev_tools/dist/lib/shell.js +91 -0
  18. dev_tools/dist/lib/shell.test.js +10 -0
  19. dev_tools/dist/lib/td-cli.js +27 -0
  20. dev_tools/dist/lib/test-utils.js +20 -0
  21. dev_tools/dist/mcp/definitions.js +242 -0
  22. dev_tools/dist/mcp/server.js +317 -0
  23. dev_tools/dist/mcp/tools.js +18 -0
  24. dev_tools/dist/tools/contract.js +2069 -0
  25. dev_tools/dist/tools/ddgs.search.js +50 -0
  26. dev_tools/dist/tools/ddgs.search.test.js +67 -0
  27. dev_tools/dist/tools/ddgs_search.py +23 -0
  28. dev_tools/dist/tools/github.checkout_branch.js +20 -0
  29. dev_tools/dist/tools/github.comment_triage_summary.js +25 -0
  30. dev_tools/dist/tools/github.create_issue.js +28 -0
  31. dev_tools/dist/tools/github.create_issue.test.js +54 -0
  32. dev_tools/dist/tools/github.create_pull_request.js +44 -0
  33. dev_tools/dist/tools/github.get_merge_conflict_files.js +26 -0
  34. dev_tools/dist/tools/github.get_pr.js +18 -0
  35. dev_tools/dist/tools/github.get_pr_diff.js +24 -0
  36. dev_tools/dist/tools/github.issue_comment.js +24 -0
  37. dev_tools/dist/tools/github.issue_update.js +29 -0
  38. dev_tools/dist/tools/github.issue_view.js +28 -0
  39. dev_tools/dist/tools/github.open_replacement_pr.js +41 -0
  40. dev_tools/dist/tools/github.search_open_prs.js +31 -0
  41. dev_tools/dist/tools/github.search_open_prs.test.js +51 -0
  42. dev_tools/dist/tools/jules/cancel-session.js +20 -0
  43. dev_tools/dist/tools/jules/create-session.js +20 -0
  44. dev_tools/dist/tools/jules/create-session.test.js +99 -0
  45. dev_tools/dist/tools/jules/get-messages.js +19 -0
  46. dev_tools/dist/tools/jules/get-messages.test.js +45 -0
  47. dev_tools/dist/tools/jules/get-pr.js +14 -0
  48. dev_tools/dist/tools/jules/get-pr.test.js +37 -0
  49. dev_tools/dist/tools/jules/get-session.js +10 -0
  50. dev_tools/dist/tools/jules/list-sessions.js +43 -0
  51. dev_tools/dist/tools/jules/send-message.js +22 -0
  52. dev_tools/dist/tools/jules/send-message.test.js +56 -0
  53. dev_tools/dist/tools/jules/shared.js +26 -0
  54. dev_tools/dist/tools/jules/trigger-feedback.js +16 -0
  55. dev_tools/dist/tools/jules/trigger-feedback.test.js +42 -0
  56. dev_tools/dist/tools/repo.commit_patch.js +33 -0
  57. dev_tools/dist/tools/repo.create_branch.js +21 -0
  58. dev_tools/dist/tools/repo.create_branch.test.js +42 -0
  59. dev_tools/dist/tools/repo.create_repair_branch.js +38 -0
  60. dev_tools/dist/tools/repo.get_changed_files.js +21 -0
  61. dev_tools/dist/tools/repo.get_command_schema.js +27 -0
  62. dev_tools/dist/tools/repo.get_package_scripts.js +35 -0
  63. dev_tools/dist/tools/repo.get_package_scripts.test.js +39 -0
  64. dev_tools/dist/tools/repo.get_route_map.js +37 -0
  65. dev_tools/dist/tools/repo.logs.js +16 -0
  66. dev_tools/dist/tools/repo.read_agent_context.js +20 -0
  67. dev_tools/dist/tools/repo.read_ci_logs.js +18 -0
  68. dev_tools/dist/tools/repo.run_lighthouse.js +37 -0
  69. dev_tools/dist/tools/repo.run_playwright.js +29 -0
  70. dev_tools/dist/tools/repo.run_tests.js +43 -0
  71. dev_tools/dist/tools/types.js +1 -0
  72. dev_tools/get_ai_context.py +63 -0
  73. dev_tools/handlers/__init__.py +1 -0
  74. dev_tools/handlers/command_handler.py +69 -0
  75. dev_tools/mcp_server.py +36 -0
  76. dev_tools/models.py +354 -0
  77. dev_tools/orchestrator.py +2841 -0
  78. dev_tools/pr_overlap.py +139 -0
  79. dev_tools/resources/__init__.py +1 -0
  80. dev_tools/resources/build-repo-context.py +182 -0
  81. dev_tools/resources/prompt_constants.json +5 -0
  82. dev_tools/resources/review_template.md +41 -0
  83. dev_tools/resources/ux-audit.config.json +15 -0
  84. dev_tools/resources/visual_guidelines.json +3 -0
  85. dev_tools/review_read_pass.py +232 -0
  86. dev_tools/schema_gen.py +55 -0
  87. dev_tools/schema_utils.py +125 -0
  88. dev_tools/scope_check.py +85 -0
  89. dev_tools/services/__init__.py +1 -0
  90. dev_tools/services/ai_service.py +816 -0
  91. dev_tools/services/dependency_graph.py +123 -0
  92. dev_tools/services/github.py +783 -0
  93. dev_tools/services/jules.py +181 -0
  94. dev_tools/services/repair_service.py +199 -0
  95. dev_tools/services/vector_store.py +82 -0
  96. dev_tools/services/vision_service.py +91 -0
  97. dev_tools/td_cli.py +28 -0
  98. dev_tools/utils/__init__.py +1035 -0
  99. dev_tools/utils/git.py +68 -0
  100. dev_tools/ux_report.py +213 -0
  101. dev_tools/verify_infra.py +91 -0
  102. dev_tools/verify_versions.py +191 -0
  103. dev_tools/version_utils.py +175 -0
@@ -0,0 +1,139 @@
1
+ # pylint: disable=invalid-name,line-too-long,missing-docstring,no-value-for-parameter
2
+ import sys
3
+ from collections import defaultdict
4
+ from typing import Any, Dict, List
5
+
6
+ import click
7
+ from dev_tools.services.github import GitHubClient
8
+ from dev_tools.utils import log_error, log_info
9
+
10
+
11
+ def get_pr_overlaps(github: GitHubClient, limit: int) -> List[Dict[str, Any]]:
12
+ """
13
+ Identifies structural overlaps between open Pull Requests by analyzing modified file sets.
14
+
15
+ This function fetches open PRs from the repository, retrieves the list of modified files
16
+ for each PR, and identifies groups (clusters) of PRs that share at least one common file.
17
+ These clusters are useful for identifying potential merge conflicts or functional
18
+ dependencies that might necessitate PR consolidation.
19
+
20
+ Args:
21
+ github: An instance of GitHubClient to interact with the GitHub API.
22
+ limit: The maximum number of open PRs to retrieve for analysis.
23
+
24
+ Returns:
25
+ A list of cluster dictionaries, each containing:
26
+ - 'prs': A list of PR numbers in the cluster.
27
+ - 'files': A list of filenames shared across the PRs in the cluster.
28
+ - 'metadata': A dictionary mapping PR numbers to their full PR details.
29
+ """
30
+ # 1. Fetch open PRs
31
+ log_info(f"Fetching up to {limit} open PRs...")
32
+ prs = github.list_pull_requests(state="open", limit=limit)
33
+
34
+ if not prs:
35
+ log_info("No open PRs found.")
36
+ return []
37
+
38
+ # 2. Map files to PRs
39
+ file_to_prs = defaultdict(set)
40
+ pr_metadata = {}
41
+
42
+ for pr in prs:
43
+ pr_num = pr["number"]
44
+ pr_metadata[pr_num] = pr
45
+ log_info(f"Fetching files for PR #{pr_num}...")
46
+ try:
47
+ files = github.fetch_pr_files(pr_num)
48
+ for f in files:
49
+ filename = f.get("filename")
50
+ if filename:
51
+ file_to_prs[filename].add(pr_num)
52
+ except Exception as e:
53
+ log_error(f"Failed to fetch files for PR #{pr_num}: {e}")
54
+
55
+ # 3. Identify clusters
56
+ # A cluster is a set of PRs that share at least one file.
57
+ # We use a simple disjoint-set or just group by overlap.
58
+ # For PR consolidation, we want to see pairs/groups that have overlaps.
59
+
60
+ overlaps = defaultdict(set)
61
+ for filename, pr_set in file_to_prs.items():
62
+ if len(pr_set) > 1:
63
+ sorted_prs = tuple(sorted(list(pr_set)))
64
+ overlaps[sorted_prs].add(filename)
65
+
66
+ clusters = []
67
+ for pr_set, files in overlaps.items():
68
+ clusters.append({
69
+ "prs": list(pr_set),
70
+ "files": sorted(list(files)),
71
+ "metadata": {num: pr_metadata[num] for num in pr_set}
72
+ })
73
+
74
+ # Sort clusters by number of overlapping files (descending)
75
+ clusters.sort(key=lambda x: len(x["files"]), reverse=True)
76
+ return clusters
77
+
78
+
79
+ def report_overlaps(clusters: List[Dict[str, Any]]):
80
+ """
81
+ Generates and prints a Markdown-formatted report of identified PR overlaps to stdout.
82
+
83
+ The report includes details for each cluster, such as the involved PR numbers,
84
+ the author of each PR, and a list of the primary overlapping files. It also
85
+ provides actionable recommendations for consolidation or coordination.
86
+
87
+ Args:
88
+ clusters: A list of cluster dictionaries as returned by `get_pr_overlaps`.
89
+ """
90
+ if not clusters:
91
+ print("✅ No significant PR overlaps detected.")
92
+ return
93
+
94
+ print("# Active PR Overlap Analysis")
95
+ print("Identified clusters of PRs with shared file modifications.\n")
96
+
97
+ for i, cluster in enumerate(clusters, 1):
98
+ pr_list = cluster["prs"]
99
+ files = cluster["files"]
100
+ metadata = cluster["metadata"]
101
+
102
+ print(f"## Cluster {i}: PRs {', '.join(f'#{num}' for num in pr_list)}")
103
+ print(f"**Primary Overlap:** {len(files)} files")
104
+
105
+ # List top 10 files
106
+ for f in files[:10]:
107
+ print(f"- `{f}`")
108
+ if len(files) > 10:
109
+ print(f"- ... and {len(files) - 10} more")
110
+
111
+ print("\n**Involved PRs:**")
112
+ for num in pr_list:
113
+ pr = metadata[num]
114
+ print(f"- **#{num}**: {pr.get('title')} (Author: @{pr.get('author', {}).get('login', 'unknown')})")
115
+
116
+ print("\n**Recommendation:** Merge/Coordinate")
117
+ print("**Rationale:** High file overlap suggests these PRs may have functional dependencies or cause merge conflicts.")
118
+ print("\n---\n")
119
+
120
+
121
+ @click.command()
122
+ @click.option("--limit", default=20, help="Limit the number of PRs to process")
123
+ @click.option("--no-cache", is_flag=True, help="Bypass the disk cache for GitHub API calls")
124
+ def run_cli(limit, no_cache):
125
+ try:
126
+ github = GitHubClient(no_cache=no_cache)
127
+ clusters = get_pr_overlaps(github, limit)
128
+ report_overlaps(clusters)
129
+ except Exception as e:
130
+ log_error(f"Overlap analysis failed: {e}")
131
+ sys.exit(1)
132
+
133
+
134
+ def main():
135
+ run_cli() # pylint: disable=no-value-for-parameter
136
+
137
+
138
+ if __name__ == "__main__":
139
+ main()
@@ -0,0 +1 @@
1
+ # pylint: disable=missing-docstring
@@ -0,0 +1,182 @@
1
+ # pylint: disable=import-outside-toplevel,invalid-name,line-too-long,missing-docstring,too-many-branches,too-many-locals,too-many-statements
2
+ #!/usr/bin/env python3
3
+ import json
4
+ import pathlib
5
+ import sys
6
+
7
+
8
+ def build_repo_context():
9
+ """Gathers static context about the repository."""
10
+
11
+ # Discovery: find the repo root and package root
12
+ script_path = pathlib.Path(__file__).resolve()
13
+ # boomtick-pkg/scripts/build-repo-context.py -> boomtick-pkg
14
+ package_root = script_path.parent.parent
15
+
16
+ # Check if we are in a monorepo or a standalone repo
17
+ if (package_root.parent / "package.json").exists() and (package_root.parent / "boomtick-pkg").exists():
18
+ # Monorepo mode
19
+ repo_root = package_root.parent
20
+ else:
21
+ # Standalone mode
22
+ repo_root = package_root
23
+
24
+ # If we are running from a location that isn't boomtick-pkg/scripts,
25
+ # fallback to discovery based on workspace.json
26
+ if package_root.name != "boomtick-pkg" or not (package_root / "workspace.json").exists():
27
+ # Search upwards for workspace.json using Path.parents
28
+ found_pkg = False
29
+ for parent in script_path.parents:
30
+ if (parent / "workspace.json").exists():
31
+ package_root = parent
32
+ found_pkg = True
33
+ break
34
+
35
+ if found_pkg:
36
+ if (package_root.parent / "package.json").exists() and (package_root.parent / "boomtick-pkg").exists():
37
+ repo_root = package_root.parent
38
+ else:
39
+ repo_root = package_root
40
+ else:
41
+ # Absolute fallback
42
+ repo_root = pathlib.Path(".").resolve()
43
+ package_root = repo_root if (repo_root / "workspace.json").exists() else repo_root / "boomtick-pkg"
44
+
45
+ # 1. Package JSON (Repo Root)
46
+ try:
47
+ package_json_path = repo_root / "package.json"
48
+ if not package_json_path.exists() and (repo_root / "workspace.json").exists():
49
+ # In extracted standalone mode, use workspace.json as the package authority
50
+ package_json_path = repo_root / "workspace.json"
51
+
52
+ package_json = json.loads(package_json_path.read_text())
53
+ # simplify package.json to the most important parts for context
54
+ package_summary = {
55
+ "name": package_json.get("name"),
56
+ "scripts": package_json.get("scripts", {}),
57
+ "dependencies": sorted(list(package_json.get("dependencies", {}).keys())),
58
+ "devDependencies": sorted(list(package_json.get("devDependencies", {}).keys())),
59
+ }
60
+ except Exception as e:
61
+ print(f"Error reading package.json: {e}", file=sys.stderr)
62
+ package_summary = {}
63
+
64
+ # 2. Project Config (Repo Root)
65
+ project_config = {}
66
+ try:
67
+ project_config_path = repo_root / "project_config.json"
68
+ if project_config_path.exists():
69
+ project_config = json.loads(project_config_path.read_text())
70
+ except Exception as e:
71
+ print(f"Error reading project_config.json: {e}", file=sys.stderr)
72
+
73
+ # 3. MCP Schema (Package Internal)
74
+ mcp_schema = {"tools": [], "prompts": [], "resources": []}
75
+ try:
76
+ import subprocess
77
+
78
+ mcp_dir = package_root / "mcp"
79
+ if mcp_dir.exists():
80
+ # Use npx tsx to run the export script without needing to compile it
81
+ # We run it from the mcp_dir to ensure it finds its own local config
82
+ result = subprocess.run(
83
+ ["npx", "tsx", "scripts/export-mcp-schema.ts"],
84
+ cwd=str(mcp_dir),
85
+ capture_output=True,
86
+ text=True,
87
+ check=True,
88
+ )
89
+ mcp_schema = json.loads(result.stdout)
90
+ except Exception as e:
91
+ print(f"Error gathering MCP schema: {e}", file=sys.stderr)
92
+
93
+ # 4. CLI Schema (Package Internal)
94
+ cli_schema = {
95
+ "tool_name": "td-cli",
96
+ "schema_authority": "Use 'repo.get_command_schema' or 'td-cli schema <path>' for granular discovery. This file remains for legacy fallback.",
97
+ "description": "Custom developer CLI for BoomTick repository management.",
98
+ "base_command": "td-cli",
99
+ }
100
+ try:
101
+ from dev_tools.cli import cli
102
+ from dev_tools.schema_utils import collect_commands
103
+
104
+ # We keep cli-schema.json updated but minimal for the aggregate context
105
+ # Increasing max_depth to 2 to allow discovery of nested subcommands (e.g., gh search-prs)
106
+ generated_subcommands = collect_commands(cli, max_depth=2)
107
+ cli_schema["subcommands"] = generated_subcommands
108
+
109
+ cli_schema_path = package_root / "cli" / "dev_tools" / "cli-schema.json"
110
+
111
+ # Write full schema for legacy/reference but don't bloat the agent context
112
+ full_subcommands = collect_commands(cli)
113
+ full_payload = cli_schema.copy()
114
+ full_payload["subcommands"] = full_subcommands
115
+ cli_schema_path.write_text(json.dumps(full_payload, indent=2))
116
+
117
+ # Also trigger Pydantic model contract generation
118
+ try:
119
+ import io
120
+ from contextlib import redirect_stdout
121
+
122
+ from dev_tools.schema_gen import generate_schema
123
+
124
+ # Capture stdout to ensure clean JSON output for build-repo-context.py
125
+ with redirect_stdout(io.StringIO()):
126
+ generate_schema()
127
+ # Note: sync-contracts.ts is run via pnpm run verify:schemas or manually
128
+ except Exception as schema_err:
129
+ print(f"Error generating model schemas: {schema_err}", file=sys.stderr)
130
+
131
+ except Exception as e:
132
+ print(f"Error generating cli-schema.json dynamically: {e}", file=sys.stderr)
133
+ # Fallback to reading file if generation failed
134
+ try:
135
+ cli_schema_path = package_root / "cli" / "dev_tools" / "cli-schema.json"
136
+ if cli_schema_path.exists():
137
+ cli_schema = json.loads(cli_schema_path.read_text())
138
+ except Exception as read_err:
139
+ print(f"Fallback read of cli-schema.json failed: {read_err}", file=sys.stderr)
140
+
141
+ # 5. File Tree (Repo Root)
142
+ def get_dir_structure(path, max_depth=2, current_depth=0):
143
+ if current_depth >= max_depth:
144
+ return "..."
145
+ structure = {}
146
+ try:
147
+ for item in sorted(path.iterdir()):
148
+ if item.name.startswith(".") or item.name == "node_modules" or item.name == "__pycache__":
149
+ continue
150
+ if item.is_dir():
151
+ structure[item.name + "/"] = get_dir_structure(item, max_depth, current_depth + 1)
152
+ else:
153
+ structure[item.name] = None
154
+ except Exception:
155
+ pass
156
+ return structure
157
+
158
+ file_tree = get_dir_structure(repo_root)
159
+
160
+ # Assemble context
161
+ return {
162
+ "repo": {
163
+ "name": package_summary.get("name", "Unknown Repo"),
164
+ },
165
+ "package_json": package_summary,
166
+ "project_config": project_config,
167
+ "mcp_schema": mcp_schema,
168
+ "cli_schema": cli_schema,
169
+ "file_tree": file_tree,
170
+ }
171
+
172
+
173
+ if __name__ == "__main__":
174
+ try:
175
+ context = build_repo_context()
176
+ if not context.get("package_json"):
177
+ raise ValueError("Failed to gather basic repository context (package.json missing or invalid)")
178
+ # sort_keys=True ensures the output is deterministic for revision control
179
+ print(json.dumps(context, indent=2, sort_keys=True))
180
+ except Exception as e:
181
+ print(f"FATAL: Context generation failed: {e}", file=sys.stderr)
182
+ sys.exit(1)
@@ -0,0 +1,5 @@
1
+ {
2
+ "STRICT_JSON_VERIFICATION": "Strict JSON Verification:\n- You MUST self-verify the completeness and validity of the JSON block before finishing your response.\n- Every finding MUST have an `id`, `file`, `issue`, and `status`.\n- Ensure the JSON is well-formed and contained entirely within the `<findings>` tags.\n- Ensure 'snippet' is a unique string from the diff that identifies the issue.",
3
+ "SNIPPET_AND_VERIFICATION_RULES": "Snippet and verification rules:\n- STRICT SNIPPET RULE: When citing an error or anti-pattern, you MUST quote the entire, exact line from the diff in the \"snippet\" field. Do not truncate the line.\n- Before flagging a \"syntax error\" or \"missing property/method\", re-read the diff to confirm the code isn't simply continued on the next line or truncated in the diff chunk. Hallucinating errors due to chunk truncation is a severe failure.\n- If a line appears truncated in the diff (e.g. at the edge of a chunk), DO NOT assume it is a syntax error. Assume it is valid code that continues outside the visible context.",
4
+ "COMMON_REVIEW_GUIDELINES": "Review ONLY PR changes. Assume original code worked.\nEVIDENCE RULE: Issue must point to exact line + explain runtime consequence.\nFALSE POSITIVE FILTER: No speculation. Design choices are NOT bugs.\n\nTIERED SCOPE:\n- For App/UI (src/): Flag redundant wrappers. BANNED: Raw Tailwind layout (flex/grid/px-*) in TSX (use Stack/Grid/Box).\n- For Infra/Tooling (scripts/, boomtick-pkg/cli/, .github/): Focus on portability, idempotency, and error handling. Avoid UI-specific feedback for low-level scripts.\n\nREPO RULES: Prefer removal.\nANTI-SLOP: DO NOT recommend overly complex error handling, defensive guards, extra unit tests for simple internal scripts, or boilerplate documentation/comments.\n\n- FILE NECESSITY: Question any added, moved, or removed files that look like temporary artifacts (e.g. .tmp, standalone .py in root, audit-*.md, .json dumps) or seem unrelated to the PR intent. Flag them for removal if they pollute the review context."
5
+ }
@@ -0,0 +1,41 @@
1
+ # PR Review: #{pr_num}
2
+
3
+ ## Context
4
+
5
+ - **Last Commit Tracked (SHA):** {head_sha}
6
+
7
+ ## Audit Checklist
8
+
9
+ For EVERY changed file, verify against these standards. Mark as `- [x]` when verified.
10
+
11
+ - [ ] Dead abstractions: No new class, context, or hook that a simpler primitive handles.
12
+ - [ ] Unnecessary indirection: No layer of wrapping where a direct function call suffices.
13
+ - [ ] Responsibility creep: Component does not take on state/logic belonging in parent/hook.
14
+ - [ ] Import bloat: No unnecessary `import React from 'react'` (React 17+).
15
+ - [ ] Token compliance: Uses established design tokens (no raw Tailwind values or inline styles).
16
+ - [ ] Audit ratio: If > 100 lines added, identified at least 10 lines to refactor/remove.
17
+
18
+ ## CI Log Triage
19
+
20
+ (Populated if CI failures detected)
21
+ - **Failed Checks:**
22
+ {failed_checks}
23
+ - **Detected Errors:**
24
+ {detected_errors}
25
+ - **Root Cause Analysis:**
26
+ - **Remediation Steps:**
27
+
28
+ ## Output JSON
29
+
30
+ Provide your findings and inline comments in the JSON block below.
31
+ DO NOT REMOVE THE BACKTICKS.
32
+
33
+ ```json
34
+ {{
35
+ "recommendation": "Approved",
36
+ "body": "## ANTI-AI-SLOP\\n<findings>\\n\\n## FINDINGS\\n<summary>\\n\\n## FINAL RECOMMENDATION\\n<Approved | Approved with Minor Changes | Not Approved>\\n\\n<!-- td-review-manager-comment -->",
37
+ "recommendation": "<Approved | Approved with Minor Changes | Not Approved>",
38
+ "labels": [],
39
+ "comments": []
40
+ }}
41
+ ```
@@ -0,0 +1,15 @@
1
+ {
2
+ "baseUrl": "http://localhost:3000",
3
+ "viewports": {
4
+ "desktop": [
5
+ { "name": "desktop-1280", "width": 1280, "height": 800 },
6
+ { "name": "desktop-1440", "width": 1440, "height": 900 }
7
+ ],
8
+ "mobile": [
9
+ { "name": "mobile-375", "width": 375, "height": 812 },
10
+ { "name": "mobile-390", "width": 390, "height": 844 },
11
+ { "name": "mobile-430", "width": 430, "height": 932 }
12
+ ]
13
+ },
14
+ "outputDir": "artifacts/ux-audit"
15
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "VISUAL_DESIGN_GUIDELINES": "### VISUAL & DESIGN GUIDELINES (Impeccable System)\nReviewing agents must audit pull requests against the following visual constraints:\n\nA. Color & Contrast Rules\n- WCAG Contrast: Body text against background must be >= 4.5:1. Large text (>= 18px or bold >= 14px) must be >= 3:1.\n- Theme Anchor: Midnight Luster (#020617 / oklch(10% 0.02 240)) base canvas, with luminous Cyber Cyan (#22d3ee / oklch(75% 0.18 190)) triggers. No saturated cream, sand, or paper backgrounds.\n- Color Strategy: Committed/Drenched accenting (color must feel intentional and carry the section, restricted to <= 10% on primary action areas).\n\nB. Typography Limits\n- Line Length: Max body width is 65–75ch to prevent scanning fatigue.\n- Display Headings: Letter-spacing floor >= -0.04em (prevent touching letters). Ceiling size max <= 6rem.\n- Line Balance: H1–H3 must use text-wrap: balance. Articles/prose must use text-wrap: pretty.\n\nC. Layout & Structure\n- No Card Nesting: Cards inside cards are prohibited.\n- Primitives: Flexbox/grid layouts must use standard abstractions (Stack, Grid, Box). Raw Tailwind layout rules (flex, grid, px-4) are banned in app layers.\n- Z-Index Scale: Semantic scale only (dropdown -> sticky -> modal -> toast). No magic numbers like z-[99999].\n\nD. Motion\n- Layout Animations: Do not animate CSS layout dimensions (width, height, flex) unless necessary (performance bottleneck).\n- Reduced Motion: Every transition must respect @media (prefers-reduced-motion: reduce).\n- Entrance reveals: Must degrade gracefully; never gate essential content visibility behind script-triggered animations.\n\nE. Absolute Bans (Auto-Reject Checklist)\n- Side-Stripe Borders: Banned border-left accents on list items/cards.\n- Gradient Text: Combinations of background-clip: text and gradients are banned.\n- Over-rounded Elements: Radius limit is 12–16px (no 32px+ pill cards).\n- Sketchy SVGs: Hand-drawn, sketchy, or crude illustrations are prohibited.\n- Tracked Kickers: Repeating Kickers/Eyebrows above every section is banned."
3
+ }
@@ -0,0 +1,232 @@
1
+ # pylint: disable=import-outside-toplevel,missing-docstring,redefined-outer-name,too-many-locals
2
+ #!/usr/bin/env python3
3
+ """
4
+ review_read_pass.py – Diff parser and file-chunk splitter for the piecemeal
5
+ AI review pipeline.
6
+
7
+ Key exports
8
+ -----------
9
+ parse_diff_into_file_chunks(diff_text) -> list[dict]
10
+ Groups a unified diff into one chunk per file (or multiple chunks for
11
+ large files) and applies skip rules so non-code assets are never sent
12
+ to the AI reviewer.
13
+
14
+ """
15
+
16
+ import json
17
+ import re
18
+ import sys
19
+ from typing import Optional
20
+
21
+ # ── Skip rules ────────────────────────────────────────────────────────────────
22
+ # Files matching any of these patterns will be classified as skip=True.
23
+ _SKIP_EXTENSIONS = {
24
+ ".png",
25
+ ".jpg",
26
+ ".jpeg",
27
+ ".gif",
28
+ ".svg",
29
+ ".ico",
30
+ ".webp",
31
+ ".woff",
32
+ ".woff2",
33
+ ".ttf",
34
+ ".eot",
35
+ ".otf", # images / fonts
36
+ ".mp4",
37
+ ".mov",
38
+ ".avi",
39
+ ".webm", # video
40
+ ".pdf",
41
+ ".zip",
42
+ ".tar",
43
+ ".gz",
44
+ ".br", # binary archives
45
+ ".map", # source maps
46
+ }
47
+
48
+ _SKIP_FILENAMES = {
49
+ "package-lock.json",
50
+ "yarn.lock",
51
+ "pnpm-lock.yaml",
52
+ "composer.lock",
53
+ "Gemfile.lock",
54
+ "poetry.lock", # lock files
55
+ }
56
+
57
+ _SKIP_PATTERNS = [
58
+ re.compile(r"\.min\.(js|css|mjs)$"), # minified
59
+ re.compile(r"\.snap$"), # Jest snapshots
60
+ re.compile(r"^(dist|build|\.next|out|\.nuxt)/"), # build artifacts
61
+ re.compile(r"^__generated__/"), # GraphQL / codegen
62
+ ]
63
+
64
+ # Maximum diff characters sent per AI call for a single hunk-group
65
+ MAX_CHUNK_CHARS = 8_000
66
+ # Maximum added lines before we split a file's diff into multiple chunks
67
+ HUNK_GROUP_SIZE = 50
68
+
69
+
70
+ def _should_skip(filepath: str) -> Optional[str]:
71
+ """Return a human-readable skip reason, or None if the file should be reviewed."""
72
+ import os
73
+
74
+ basename = os.path.basename(filepath)
75
+ ext = os.path.splitext(basename)[1].lower()
76
+
77
+ if ext in _SKIP_EXTENSIONS:
78
+ return f"binary/image ({ext})"
79
+ if basename in _SKIP_FILENAMES:
80
+ return "lock file"
81
+ for pat in _SKIP_PATTERNS:
82
+ if pat.search(filepath):
83
+ return f"matches skip pattern ({pat.pattern})"
84
+ return None
85
+
86
+
87
+ # ── Core diff parser ──────────────────────────────────────────────────────────
88
+
89
+
90
+ def _parse_raw_files(diff_text: str) -> list[dict]:
91
+ """
92
+ Split a unified diff into a list of per-file raw dicts:
93
+ {file, hunks: [{header, lines, added_count}]}
94
+ """
95
+ files = []
96
+ current_file: Optional[str] = None
97
+ current_hunks: list = []
98
+ current_hunk: Optional[dict] = None
99
+
100
+ for line in diff_text.splitlines(keepends=True):
101
+ if line.startswith("diff --git"):
102
+ if current_file is not None:
103
+ if current_hunk:
104
+ current_hunks.append(current_hunk)
105
+ files.append({"file": current_file, "hunks": current_hunks})
106
+ current_file = None
107
+ current_hunks = []
108
+ current_hunk = None
109
+ elif line.startswith("+++ b/"):
110
+ current_file = line[6:].rstrip("\n")
111
+ elif line.startswith("@@ "):
112
+ if current_hunk:
113
+ current_hunks.append(current_hunk)
114
+ current_hunk = {"header": line, "lines": [], "added_count": 0}
115
+ elif current_hunk is not None:
116
+ current_hunk["lines"].append(line)
117
+ if line.startswith("+") and not line.startswith("+++"):
118
+ current_hunk["added_count"] += 1
119
+
120
+ # flush last file
121
+ if current_file is not None:
122
+ if current_hunk:
123
+ current_hunks.append(current_hunk)
124
+ files.append({"file": current_file, "hunks": current_hunks})
125
+
126
+ return files
127
+
128
+
129
+ def _hunk_to_text(hunk: dict) -> str:
130
+ return hunk["header"] + "".join(hunk["lines"])
131
+
132
+
133
+ def parse_diff_into_file_chunks(diff_text: str) -> list[dict]:
134
+ """
135
+ Main entry point for the piecemeal review pipeline.
136
+
137
+ Returns a list of chunk dicts:
138
+ {
139
+ "file": str, # relative path
140
+ "chunk_index": int, # 0-based (most files have only one chunk)
141
+ "total_chunks": int,
142
+ "skip": bool,
143
+ "reason": str | None, # populated when skip=True
144
+ "diff_text": str, # the raw diff text for this chunk
145
+ "added_lines": int,
146
+ "truncated": bool, # True when diff_text was trimmed to MAX_CHUNK_CHARS
147
+ }
148
+ """
149
+ raw_files = _parse_raw_files(diff_text)
150
+ chunks = []
151
+
152
+ for rf in raw_files:
153
+ filepath = rf["file"]
154
+ hunks = rf["hunks"]
155
+
156
+ skip_reason = _should_skip(filepath)
157
+
158
+ if skip_reason or not hunks:
159
+ chunks.append(
160
+ {
161
+ "file": filepath,
162
+ "chunk_index": 0,
163
+ "total_chunks": 1,
164
+ "skip": True,
165
+ "reason": skip_reason or "no hunks",
166
+ "diff_text": "",
167
+ "added_lines": 0,
168
+ "truncated": False,
169
+ }
170
+ )
171
+ continue
172
+
173
+ # Group hunks into batches of ≤ HUNK_GROUP_SIZE added lines each
174
+ groups: list[list[dict]] = []
175
+ current_group: list[dict] = []
176
+ current_added = 0
177
+
178
+ for hunk in hunks:
179
+ if current_added + hunk["added_count"] > HUNK_GROUP_SIZE and current_group:
180
+ groups.append(current_group)
181
+ current_group = []
182
+ current_added = 0
183
+ current_group.append(hunk)
184
+ current_added += hunk["added_count"]
185
+
186
+ if current_group:
187
+ groups.append(current_group)
188
+
189
+ total_chunks = len(groups)
190
+
191
+ for idx, group in enumerate(groups):
192
+ text = f"--- a/{filepath}\n+++ b/{filepath}\n"
193
+ text += "".join(_hunk_to_text(h) for h in group)
194
+ added = sum(h["added_count"] for h in group)
195
+ truncated = False
196
+
197
+ if len(text) > MAX_CHUNK_CHARS:
198
+ text = text[:MAX_CHUNK_CHARS] + f"\n... (diff truncated at {MAX_CHUNK_CHARS} chars)"
199
+ truncated = True
200
+
201
+ chunks.append(
202
+ {
203
+ "file": filepath,
204
+ "chunk_index": idx,
205
+ "total_chunks": total_chunks,
206
+ "skip": False,
207
+ "reason": None,
208
+ "diff_text": text,
209
+ "added_lines": added,
210
+ "truncated": truncated,
211
+ }
212
+ )
213
+
214
+ return chunks
215
+
216
+
217
+ if __name__ == "__main__":
218
+ raw = sys.stdin.read()
219
+ chunks = parse_diff_into_file_chunks(raw)
220
+ reviewable = [c for c in chunks if not c["skip"]]
221
+ skipped = [c for c in chunks if c["skip"]]
222
+ print(
223
+ json.dumps(
224
+ {
225
+ "total_chunks": len(chunks),
226
+ "reviewable": len(reviewable),
227
+ "skipped": len(skipped),
228
+ "chunks": chunks,
229
+ },
230
+ indent=2,
231
+ )
232
+ )
@@ -0,0 +1,55 @@
1
+ # pylint: disable=consider-using-from-import,import-outside-toplevel,missing-docstring
2
+ import inspect
3
+ import json
4
+ import os
5
+
6
+ import dev_tools.models as models
7
+ from pydantic import BaseModel
8
+
9
+
10
+ def get_models_schema():
11
+ schemas = {}
12
+ for name, obj in inspect.getmembers(models):
13
+ if inspect.isclass(obj) and issubclass(obj, BaseModel) and obj is not BaseModel:
14
+ # Skip base classes if any
15
+ if name in ("CLIResponse", "CLIInput"):
16
+ continue
17
+ # Force export using field names (camelCase) instead of aliases (snake_case)
18
+ schemas[name] = obj.model_json_schema(by_alias=False)
19
+ return schemas
20
+
21
+
22
+ def generate_schema():
23
+ schemas = {
24
+ "_warning": "AUTO-GENERATED: DO NOT EDIT MANUALLY. Update models.py instead.",
25
+ "models": get_models_schema(),
26
+ }
27
+
28
+ # Use cli-schema.json as the unified target
29
+ output_path = os.path.join(os.path.dirname(__file__), "cli-schema.json")
30
+
31
+ # Read existing if exists to merge
32
+ existing = {}
33
+ if os.path.exists(output_path):
34
+ try:
35
+ with open(output_path, "r", encoding="utf-8") as f:
36
+ existing = json.load(f)
37
+ except Exception as e:
38
+ print(f"Warning: Could not read existing cli-schema.json: {e}")
39
+
40
+ existing.update(schemas)
41
+
42
+ with open(output_path, "w", encoding="utf-8") as f:
43
+ json.dump(existing, f, indent=2)
44
+ f.write("\n")
45
+
46
+ import sys
47
+
48
+ print(
49
+ f"Updated cli-schema.json with {len(schemas['models'])} models at {output_path}",
50
+ file=sys.stderr,
51
+ )
52
+
53
+
54
+ if __name__ == "__main__":
55
+ generate_schema()