gator-command 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- gator_command/__init__.py +2 -0
- gator_command/cli.py +137 -0
- gator_command/scripts/crawler.py +633 -0
- gator_command/scripts/dashboard/dashboard.css +982 -0
- gator_command/scripts/dashboard/dashboard.html +84 -0
- gator_command/scripts/dashboard/dashboard.js +419 -0
- gator_command/scripts/dashboard/views/audit.js +270 -0
- gator_command/scripts/dashboard/views/fleet.js +307 -0
- gator_command/scripts/dashboard/views/repo.js +599 -0
- gator_command/scripts/dashboard/views/settings.js +173 -0
- gator_command/scripts/dashboard/views/updates.js +308 -0
- gator_command/scripts/enforcer-prompt.md +22 -0
- gator_command/scripts/extract-claude-sessions.py +489 -0
- gator_command/scripts/extract-codex-sessions.py +477 -0
- gator_command/scripts/extract-gemini-sessions.py +410 -0
- gator_command/scripts/gator-audit.py +956 -0
- gator_command/scripts/gator-charter-draft.py +919 -0
- gator_command/scripts/gator-charter-lint.py +427 -0
- gator_command/scripts/gator-charter-verify.py +606 -0
- gator_command/scripts/gator-dashboard.py +1271 -0
- gator_command/scripts/gator-deploy.py +916 -0
- gator_command/scripts/gator-drift.py +569 -0
- gator_command/scripts/gator-enforce.py +82 -0
- gator_command/scripts/gator-fleet-intel.py +460 -0
- gator_command/scripts/gator-fleet-report.py +615 -0
- gator_command/scripts/gator-init-command-post.py +315 -0
- gator_command/scripts/gator-init.py +434 -0
- gator_command/scripts/gator-machine-id.py +153 -0
- gator_command/scripts/gator-policy-status.py +631 -0
- gator_command/scripts/gator-pulse.py +459 -0
- gator_command/scripts/gator-repo-status.py +649 -0
- gator_command/scripts/gator-session-common.py +372 -0
- gator_command/scripts/gator-session-sink.py +831 -0
- gator_command/scripts/gator-sessions.py +1244 -0
- gator_command/scripts/gator-update.py +615 -0
- gator_command/scripts/gator-version.py +38 -0
- gator_command/scripts/gator_core.py +489 -0
- gator_command/scripts/gator_remote.py +381 -0
- gator_command/scripts/gator_runtime.py +142 -0
- gator_command/scripts/gatorize-actions.sh +989 -0
- gator_command/scripts/gatorize-lib.sh +166 -0
- gator_command/scripts/gatorize-post.sh +394 -0
- gator_command/scripts/gatorize.py +1163 -0
- gator_command/scripts/gatorize.sh +185 -0
- gator_command/scripts/generate_markdown.py +212 -0
- gator_command/scripts/generate_wiki.py +424 -0
- gator_command/scripts/graph_health.py +780 -0
- gator_command/scripts/memex-lint.py +286 -0
- gator_command/scripts/memex-lint.sh +205 -0
- gator_command/scripts/memex.py +1472 -0
- gator_command/scripts/memex_formatters.py +191 -0
- gator_command/scripts/memex_state.py +236 -0
- gator_command/scripts/spawn.py +650 -0
- gator_command/templates/gator-starter/blueprints/README.md +32 -0
- gator_command/templates/gator-starter/charterignore +53 -0
- gator_command/templates/gator-starter/charters/README.md +178 -0
- gator_command/templates/gator-starter/charters/_template.md +31 -0
- gator_command/templates/gator-starter/commands/commit.md +33 -0
- gator_command/templates/gator-starter/commands/init.md +11 -0
- gator_command/templates/gator-starter/commands/update.md +5 -0
- gator_command/templates/gator-starter/constitution.md +165 -0
- gator_command/templates/gator-starter/field-guides/README.md +25 -0
- gator_command/templates/gator-starter/gator-start-up.md +119 -0
- gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
- gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
- gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
- gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
- gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
- gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
- gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
- gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
- gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
- gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
- gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
- gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
- gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
- gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
- gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
- gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
- gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
- gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
- gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
- gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
- gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
- gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
- gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
- gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
- gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
- gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
- gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
- gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
- gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
- gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
- gator_command/templates/gator-starter/sessions/.gitignore +7 -0
- gator_command/templates/gator-starter/vault/.gitkeep +0 -0
- gator_command/templates/gator-starter/whiteboard.md +5 -0
- gator_command-1.0.0.dist-info/METADATA +122 -0
- gator_command-1.0.0.dist-info/RECORD +110 -0
- gator_command-1.0.0.dist-info/WHEEL +5 -0
- gator_command-1.0.0.dist-info/entry_points.txt +2 -0
- gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
- gator_command-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,919 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator charter-draft — Mechanical charter scaffold generator.
|
|
4
|
+
|
|
5
|
+
Uses Python's ast module to extract code structure and generate charter
|
|
6
|
+
drafts. Machine extracts structure; agent writes meaning; enforcer checks both.
|
|
7
|
+
|
|
8
|
+
Usage:
|
|
9
|
+
python gator-command/scripts/gator-charter-draft.py --path <repo>
|
|
10
|
+
python gator-command/scripts/gator-charter-draft.py --path <repo> --dirs src lib
|
|
11
|
+
python gator-command/scripts/gator-charter-draft.py --path <repo> --write
|
|
12
|
+
python gator-command/scripts/gator-charter-draft.py --path <repo> --write --output-dir gator-command/charters
|
|
13
|
+
python gator-command/scripts/gator-charter-draft.py --path <repo> --json
|
|
14
|
+
|
|
15
|
+
@reads: source files (via ast), .gator/.charterignore, git ls-files
|
|
16
|
+
@writes: charter directory (--write mode only, never overwrites existing).
|
|
17
|
+
Default: .gator/charters/. Override with --output-dir for repos
|
|
18
|
+
where the authoritative charter surface is elsewhere (e.g.
|
|
19
|
+
gator-command/charters/ in the command post repo).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
import argparse
|
|
23
|
+
import ast
|
|
24
|
+
import fnmatch
|
|
25
|
+
import json
|
|
26
|
+
import re
|
|
27
|
+
import sys
|
|
28
|
+
from datetime import date
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
|
|
31
|
+
from gator_core import get_version, find_gator_root, ensure_utf8_stdout, git
|
|
32
|
+
|
|
33
|
+
VERSION = get_version()
|
|
34
|
+
|
|
35
|
+
# All source file extensions eligible for charter coverage.
|
|
36
|
+
# Files with these extensions are discovered and included in coverage checks.
|
|
37
|
+
SUPPORTED_EXTENSIONS = {
|
|
38
|
+
".py", ".js", ".ts", ".jsx", ".tsx",
|
|
39
|
+
".sh", ".bash",
|
|
40
|
+
".go", ".rs",
|
|
41
|
+
".rb", ".java", ".cs", ".cpp", ".c", ".h",
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
# Extensions with structural analyzers (ast/regex extraction).
|
|
45
|
+
# Files not in this set still appear in coverage-gap checks but
|
|
46
|
+
# get a minimal scaffold without function/class inventory.
|
|
47
|
+
ANALYZABLE_EXTENSIONS = {".py", ".js", ".sh"}
|
|
48
|
+
|
|
49
|
+
# Built-in exclusions applied when no .charterignore exists
|
|
50
|
+
FALLBACK_EXCLUSIONS = [
|
|
51
|
+
"tests/", "test/", "__tests__/", "__pycache__/",
|
|
52
|
+
"node_modules/", "vendor/", "dist/", "build/",
|
|
53
|
+
".gator/", ".git/", ".venv/", "venv/",
|
|
54
|
+
]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# ---------------------------------------------------------------------------
|
|
58
|
+
# .charterignore
|
|
59
|
+
# ---------------------------------------------------------------------------
|
|
60
|
+
|
|
61
|
+
def load_charterignore(gator_dir):
|
|
62
|
+
"""Load .charterignore patterns from .gator/.charterignore.
|
|
63
|
+
|
|
64
|
+
Returns a list of gitignore-style patterns. Falls back to
|
|
65
|
+
FALLBACK_EXCLUSIONS if the file doesn't exist.
|
|
66
|
+
|
|
67
|
+
@reads: .gator/.charterignore
|
|
68
|
+
"""
|
|
69
|
+
ignore_file = gator_dir / ".charterignore"
|
|
70
|
+
if not ignore_file.exists():
|
|
71
|
+
return FALLBACK_EXCLUSIONS
|
|
72
|
+
|
|
73
|
+
patterns = []
|
|
74
|
+
try:
|
|
75
|
+
text = ignore_file.read_text(encoding="utf-8", errors="replace")
|
|
76
|
+
for line in text.splitlines():
|
|
77
|
+
stripped = line.strip()
|
|
78
|
+
if stripped and not stripped.startswith("#"):
|
|
79
|
+
patterns.append(stripped)
|
|
80
|
+
except OSError:
|
|
81
|
+
return FALLBACK_EXCLUSIONS
|
|
82
|
+
|
|
83
|
+
return patterns
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def is_ignored(rel_path, patterns):
|
|
87
|
+
"""Check if a relative file path matches any charterignore pattern.
|
|
88
|
+
|
|
89
|
+
Supports:
|
|
90
|
+
- directory patterns ending in / (match any path component)
|
|
91
|
+
- glob patterns with * and **
|
|
92
|
+
- basename patterns (no / in pattern matches against filename)
|
|
93
|
+
"""
|
|
94
|
+
parts = Path(rel_path).parts
|
|
95
|
+
name = Path(rel_path).name
|
|
96
|
+
|
|
97
|
+
for pattern in patterns:
|
|
98
|
+
# Directory pattern: tests/ matches any path containing that dir
|
|
99
|
+
if pattern.endswith("/"):
|
|
100
|
+
dir_name = pattern.rstrip("/")
|
|
101
|
+
# Multi-component: match as path prefix (e.g. gator-command/templates/)
|
|
102
|
+
if "/" in dir_name:
|
|
103
|
+
if rel_path.startswith(dir_name + "/") or rel_path == dir_name:
|
|
104
|
+
return True
|
|
105
|
+
else:
|
|
106
|
+
# Single component: match any path part
|
|
107
|
+
if dir_name in parts:
|
|
108
|
+
return True
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
# Pattern with path separator: match against full path
|
|
112
|
+
if "/" in pattern:
|
|
113
|
+
if fnmatch.fnmatch(rel_path, pattern):
|
|
114
|
+
return True
|
|
115
|
+
# Also try without leading **/ for convenience
|
|
116
|
+
if pattern.startswith("**/"):
|
|
117
|
+
if fnmatch.fnmatch(rel_path, pattern[3:]):
|
|
118
|
+
return True
|
|
119
|
+
# Match against each suffix of the path
|
|
120
|
+
for i in range(len(parts)):
|
|
121
|
+
suffix = "/".join(parts[i:])
|
|
122
|
+
if fnmatch.fnmatch(suffix, pattern[3:]):
|
|
123
|
+
return True
|
|
124
|
+
continue
|
|
125
|
+
|
|
126
|
+
# Basename pattern: match against filename only
|
|
127
|
+
if fnmatch.fnmatch(name, pattern):
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
# File discovery
|
|
135
|
+
# ---------------------------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
def discover_files(repo_root, gator_dir, dirs=None):
|
|
138
|
+
"""Discover charterable source files.
|
|
139
|
+
|
|
140
|
+
Uses git ls-files for tracked files, then filters by:
|
|
141
|
+
1. --dirs scope (if given)
|
|
142
|
+
2. .charterignore patterns
|
|
143
|
+
3. Supported file extensions
|
|
144
|
+
|
|
145
|
+
@reads: git index, .gator/.charterignore
|
|
146
|
+
"""
|
|
147
|
+
# Get tracked files
|
|
148
|
+
ls_output, ok = git("ls-files", cwd=repo_root)
|
|
149
|
+
if not ok or not ls_output:
|
|
150
|
+
return []
|
|
151
|
+
|
|
152
|
+
all_files = ls_output.splitlines()
|
|
153
|
+
patterns = load_charterignore(gator_dir)
|
|
154
|
+
|
|
155
|
+
result = []
|
|
156
|
+
for rel_path in all_files:
|
|
157
|
+
# Scope to --dirs if specified
|
|
158
|
+
if dirs:
|
|
159
|
+
if not any(rel_path.startswith(d.rstrip("/") + "/") or rel_path == d for d in dirs):
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
# Apply charterignore
|
|
163
|
+
if is_ignored(rel_path, patterns):
|
|
164
|
+
continue
|
|
165
|
+
|
|
166
|
+
# Filter to supported extensions
|
|
167
|
+
ext = Path(rel_path).suffix.lower()
|
|
168
|
+
if ext not in SUPPORTED_EXTENSIONS:
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
result.append(rel_path)
|
|
172
|
+
|
|
173
|
+
return sorted(result)
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
# ---------------------------------------------------------------------------
|
|
177
|
+
# Python analyzer (ast-based)
|
|
178
|
+
# ---------------------------------------------------------------------------
|
|
179
|
+
|
|
180
|
+
def analyze_python(file_path, rel_path):
|
|
181
|
+
"""Extract structural information from a Python file using ast.
|
|
182
|
+
|
|
183
|
+
Returns a dict with: functions, classes, imports, complexity signals,
|
|
184
|
+
entrypoint detection. Returns None if the file cannot be parsed.
|
|
185
|
+
|
|
186
|
+
@reads: single Python source file
|
|
187
|
+
"""
|
|
188
|
+
try:
|
|
189
|
+
source = file_path.read_text(encoding="utf-8", errors="replace")
|
|
190
|
+
except OSError:
|
|
191
|
+
return None
|
|
192
|
+
|
|
193
|
+
try:
|
|
194
|
+
tree = ast.parse(source, filename=str(rel_path))
|
|
195
|
+
except SyntaxError:
|
|
196
|
+
return None
|
|
197
|
+
|
|
198
|
+
line_count = len(source.splitlines())
|
|
199
|
+
|
|
200
|
+
# Module-level docstring
|
|
201
|
+
module_docstring = ast.get_docstring(tree) or ""
|
|
202
|
+
module_summary = module_docstring.split("\n")[0].strip() if module_docstring else ""
|
|
203
|
+
|
|
204
|
+
functions = []
|
|
205
|
+
classes = []
|
|
206
|
+
imports = []
|
|
207
|
+
has_main_guard = False
|
|
208
|
+
has_argparse = False
|
|
209
|
+
|
|
210
|
+
for node in ast.iter_child_nodes(tree):
|
|
211
|
+
# Top-level functions
|
|
212
|
+
if isinstance(node, ast.FunctionDef) or isinstance(node, ast.AsyncFunctionDef):
|
|
213
|
+
functions.append(_extract_function(node))
|
|
214
|
+
|
|
215
|
+
# Classes
|
|
216
|
+
elif isinstance(node, ast.ClassDef):
|
|
217
|
+
cls_info = _extract_class(node)
|
|
218
|
+
classes.append(cls_info)
|
|
219
|
+
|
|
220
|
+
# Imports
|
|
221
|
+
elif isinstance(node, ast.Import):
|
|
222
|
+
for alias in node.names:
|
|
223
|
+
imports.append(alias.name)
|
|
224
|
+
elif isinstance(node, ast.ImportFrom):
|
|
225
|
+
if node.module:
|
|
226
|
+
imports.append(node.module)
|
|
227
|
+
|
|
228
|
+
# if __name__ == "__main__" guard
|
|
229
|
+
elif isinstance(node, ast.If):
|
|
230
|
+
if _is_main_guard(node):
|
|
231
|
+
has_main_guard = True
|
|
232
|
+
|
|
233
|
+
# Check for argparse usage
|
|
234
|
+
has_argparse = "argparse" in imports
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
"rel_path": rel_path,
|
|
238
|
+
"line_count": line_count,
|
|
239
|
+
"docstring_summary": module_summary,
|
|
240
|
+
"functions": functions,
|
|
241
|
+
"classes": classes,
|
|
242
|
+
"imports": sorted(set(imports)),
|
|
243
|
+
"has_main_guard": has_main_guard,
|
|
244
|
+
"has_argparse": has_argparse,
|
|
245
|
+
"is_entrypoint": has_main_guard or has_argparse,
|
|
246
|
+
"complexity": {
|
|
247
|
+
"functions": len(functions),
|
|
248
|
+
"classes": len(classes),
|
|
249
|
+
"methods": sum(len(c["methods"]) for c in classes),
|
|
250
|
+
"imports": len(set(imports)),
|
|
251
|
+
"lines": line_count,
|
|
252
|
+
},
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _extract_function(node):
|
|
257
|
+
"""Extract function signature and metadata."""
|
|
258
|
+
args = []
|
|
259
|
+
for arg in node.args.args:
|
|
260
|
+
args.append(arg.arg)
|
|
261
|
+
if node.args.vararg:
|
|
262
|
+
args.append(f"*{node.args.vararg.arg}")
|
|
263
|
+
if node.args.kwonlyargs:
|
|
264
|
+
for kw in node.args.kwonlyargs:
|
|
265
|
+
args.append(kw.arg)
|
|
266
|
+
if node.args.kwarg:
|
|
267
|
+
args.append(f"**{node.args.kwarg.arg}")
|
|
268
|
+
|
|
269
|
+
decorators = []
|
|
270
|
+
for dec in node.decorator_list:
|
|
271
|
+
if isinstance(dec, ast.Name):
|
|
272
|
+
decorators.append(dec.id)
|
|
273
|
+
elif isinstance(dec, ast.Attribute):
|
|
274
|
+
decorators.append(ast.dump(dec))
|
|
275
|
+
|
|
276
|
+
# Extract docstring
|
|
277
|
+
docstring = ast.get_docstring(node) or ""
|
|
278
|
+
first_line = docstring.split("\n")[0].strip() if docstring else ""
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
"name": node.name,
|
|
282
|
+
"args": args,
|
|
283
|
+
"signature": f"{node.name}({', '.join(args)})",
|
|
284
|
+
"decorators": decorators,
|
|
285
|
+
"line": node.lineno,
|
|
286
|
+
"docstring_summary": first_line,
|
|
287
|
+
"is_private": node.name.startswith("_"),
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _extract_class(node):
|
|
292
|
+
"""Extract class structure."""
|
|
293
|
+
methods = []
|
|
294
|
+
for item in node.body:
|
|
295
|
+
if isinstance(item, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
296
|
+
methods.append(_extract_function(item))
|
|
297
|
+
|
|
298
|
+
bases = []
|
|
299
|
+
for base in node.bases:
|
|
300
|
+
if isinstance(base, ast.Name):
|
|
301
|
+
bases.append(base.id)
|
|
302
|
+
elif isinstance(base, ast.Attribute):
|
|
303
|
+
bases.append(f"{ast.dump(base)}")
|
|
304
|
+
|
|
305
|
+
docstring = ast.get_docstring(node) or ""
|
|
306
|
+
first_line = docstring.split("\n")[0].strip() if docstring else ""
|
|
307
|
+
|
|
308
|
+
return {
|
|
309
|
+
"name": node.name,
|
|
310
|
+
"bases": bases,
|
|
311
|
+
"methods": methods,
|
|
312
|
+
"line": node.lineno,
|
|
313
|
+
"docstring_summary": first_line,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def _is_main_guard(node):
|
|
318
|
+
"""Check if an If node is `if __name__ == '__main__':`."""
|
|
319
|
+
test = node.test
|
|
320
|
+
if isinstance(test, ast.Compare):
|
|
321
|
+
if (isinstance(test.left, ast.Name) and test.left.id == "__name__"
|
|
322
|
+
and len(test.comparators) == 1):
|
|
323
|
+
comp = test.comparators[0]
|
|
324
|
+
if isinstance(comp, ast.Constant) and comp.value == "__main__":
|
|
325
|
+
return True
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
# ---------------------------------------------------------------------------
|
|
330
|
+
# JavaScript / TypeScript analyzer (regex-based)
|
|
331
|
+
# ---------------------------------------------------------------------------
|
|
332
|
+
|
|
333
|
+
# Patterns for JS/TS function extraction
|
|
334
|
+
_JS_FUNCTION = re.compile(
|
|
335
|
+
r'^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)',
|
|
336
|
+
re.MULTILINE,
|
|
337
|
+
)
|
|
338
|
+
_JS_ARROW_NAMED = re.compile(
|
|
339
|
+
r'^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(?:\([^)]*\)|[\w]+)\s*=>',
|
|
340
|
+
re.MULTILINE,
|
|
341
|
+
)
|
|
342
|
+
_JS_CLASS = re.compile(
|
|
343
|
+
r'^(?:export\s+)?class\s+(\w+)',
|
|
344
|
+
re.MULTILINE,
|
|
345
|
+
)
|
|
346
|
+
_JS_METHOD = re.compile(
|
|
347
|
+
r'^\s+(?:async\s+)?(\w+)\s*\([^)]*\)\s*\{',
|
|
348
|
+
re.MULTILINE,
|
|
349
|
+
)
|
|
350
|
+
_JS_IMPORT = re.compile(
|
|
351
|
+
r'(?:import\s+.*?from\s+["\']([^"\']+)["\']|require\s*\(\s*["\']([^"\']+)["\']\s*\))',
|
|
352
|
+
)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def analyze_javascript(file_path, rel_path):
|
|
356
|
+
"""Extract structural information from a JS/TS file using regex.
|
|
357
|
+
|
|
358
|
+
Less precise than ast-based Python analysis, but captures the main
|
|
359
|
+
structural elements: named functions, arrow functions, classes, imports.
|
|
360
|
+
|
|
361
|
+
@reads: single JS/TS source file
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
source = file_path.read_text(encoding="utf-8", errors="replace")
|
|
365
|
+
except OSError:
|
|
366
|
+
return None
|
|
367
|
+
|
|
368
|
+
lines = source.splitlines()
|
|
369
|
+
line_count = len(lines)
|
|
370
|
+
|
|
371
|
+
# Extract first comment block as module summary
|
|
372
|
+
module_summary = ""
|
|
373
|
+
if lines and lines[0].strip().startswith("/**"):
|
|
374
|
+
for line in lines[1:]:
|
|
375
|
+
stripped = line.strip().lstrip("* ").rstrip()
|
|
376
|
+
if stripped.startswith("*/"):
|
|
377
|
+
break
|
|
378
|
+
if stripped and not module_summary:
|
|
379
|
+
module_summary = stripped
|
|
380
|
+
break
|
|
381
|
+
|
|
382
|
+
functions = []
|
|
383
|
+
classes = []
|
|
384
|
+
imports = []
|
|
385
|
+
|
|
386
|
+
# Named functions
|
|
387
|
+
for m in _JS_FUNCTION.finditer(source):
|
|
388
|
+
name = m.group(1)
|
|
389
|
+
args_str = m.group(2).strip()
|
|
390
|
+
args = [a.strip().split(":")[0].strip() for a in args_str.split(",") if a.strip()] if args_str else []
|
|
391
|
+
functions.append({
|
|
392
|
+
"name": name,
|
|
393
|
+
"args": args,
|
|
394
|
+
"signature": f"{name}({', '.join(args)})",
|
|
395
|
+
"decorators": [],
|
|
396
|
+
"line": source[:m.start()].count("\n") + 1,
|
|
397
|
+
"docstring_summary": "",
|
|
398
|
+
"is_private": name.startswith("_"),
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
# Arrow functions assigned to const/let/var
|
|
402
|
+
for m in _JS_ARROW_NAMED.finditer(source):
|
|
403
|
+
name = m.group(1)
|
|
404
|
+
functions.append({
|
|
405
|
+
"name": name,
|
|
406
|
+
"args": [],
|
|
407
|
+
"signature": f"{name}()",
|
|
408
|
+
"decorators": [],
|
|
409
|
+
"line": source[:m.start()].count("\n") + 1,
|
|
410
|
+
"docstring_summary": "",
|
|
411
|
+
"is_private": name.startswith("_"),
|
|
412
|
+
})
|
|
413
|
+
|
|
414
|
+
# Classes
|
|
415
|
+
for m in _JS_CLASS.finditer(source):
|
|
416
|
+
name = m.group(1)
|
|
417
|
+
classes.append({
|
|
418
|
+
"name": name,
|
|
419
|
+
"bases": [],
|
|
420
|
+
"methods": [],
|
|
421
|
+
"line": source[:m.start()].count("\n") + 1,
|
|
422
|
+
"docstring_summary": "",
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
# Imports
|
|
426
|
+
for m in _JS_IMPORT.finditer(source):
|
|
427
|
+
imp = m.group(1) or m.group(2)
|
|
428
|
+
if imp:
|
|
429
|
+
imports.append(imp)
|
|
430
|
+
|
|
431
|
+
return {
|
|
432
|
+
"rel_path": rel_path,
|
|
433
|
+
"line_count": line_count,
|
|
434
|
+
"docstring_summary": module_summary,
|
|
435
|
+
"functions": functions,
|
|
436
|
+
"classes": classes,
|
|
437
|
+
"imports": sorted(set(imports)),
|
|
438
|
+
"has_main_guard": False,
|
|
439
|
+
"has_argparse": False,
|
|
440
|
+
"is_entrypoint": False,
|
|
441
|
+
"complexity": {
|
|
442
|
+
"functions": len(functions),
|
|
443
|
+
"classes": len(classes),
|
|
444
|
+
"methods": 0,
|
|
445
|
+
"imports": len(set(imports)),
|
|
446
|
+
"lines": line_count,
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
# ---------------------------------------------------------------------------
|
|
452
|
+
# Shell analyzer (regex-based)
|
|
453
|
+
# ---------------------------------------------------------------------------
|
|
454
|
+
|
|
455
|
+
_SH_FUNCTION = re.compile(
|
|
456
|
+
r'^(?:function\s+)?(\w+)\s*\(\s*\)\s*\{',
|
|
457
|
+
re.MULTILINE,
|
|
458
|
+
)
|
|
459
|
+
_SH_SOURCE = re.compile(
|
|
460
|
+
r'^\s*(?:source|\.)(?:\s+)([^\s;#]+)',
|
|
461
|
+
re.MULTILINE,
|
|
462
|
+
)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
def analyze_shell(file_path, rel_path):
|
|
466
|
+
"""Extract structural information from a shell script using regex.
|
|
467
|
+
|
|
468
|
+
Captures function definitions and source/. includes.
|
|
469
|
+
|
|
470
|
+
@reads: single shell source file
|
|
471
|
+
"""
|
|
472
|
+
try:
|
|
473
|
+
source = file_path.read_text(encoding="utf-8", errors="replace")
|
|
474
|
+
except OSError:
|
|
475
|
+
return None
|
|
476
|
+
|
|
477
|
+
lines = source.splitlines()
|
|
478
|
+
line_count = len(lines)
|
|
479
|
+
|
|
480
|
+
# Extract first comment block as module summary
|
|
481
|
+
module_summary = ""
|
|
482
|
+
for line in lines:
|
|
483
|
+
stripped = line.strip()
|
|
484
|
+
if stripped.startswith("#!"):
|
|
485
|
+
continue
|
|
486
|
+
if stripped.startswith("#"):
|
|
487
|
+
text = stripped.lstrip("# ").strip()
|
|
488
|
+
if text and not module_summary:
|
|
489
|
+
module_summary = text
|
|
490
|
+
break
|
|
491
|
+
elif stripped:
|
|
492
|
+
break
|
|
493
|
+
|
|
494
|
+
functions = []
|
|
495
|
+
imports = []
|
|
496
|
+
|
|
497
|
+
for m in _SH_FUNCTION.finditer(source):
|
|
498
|
+
name = m.group(1)
|
|
499
|
+
functions.append({
|
|
500
|
+
"name": name,
|
|
501
|
+
"args": [],
|
|
502
|
+
"signature": f"{name}()",
|
|
503
|
+
"decorators": [],
|
|
504
|
+
"line": source[:m.start()].count("\n") + 1,
|
|
505
|
+
"docstring_summary": "",
|
|
506
|
+
"is_private": name.startswith("_"),
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
for m in _SH_SOURCE.finditer(source):
|
|
510
|
+
imports.append(m.group(1))
|
|
511
|
+
|
|
512
|
+
return {
|
|
513
|
+
"rel_path": rel_path,
|
|
514
|
+
"line_count": line_count,
|
|
515
|
+
"docstring_summary": module_summary,
|
|
516
|
+
"functions": functions,
|
|
517
|
+
"classes": [],
|
|
518
|
+
"imports": sorted(set(imports)),
|
|
519
|
+
"has_main_guard": False,
|
|
520
|
+
"has_argparse": False,
|
|
521
|
+
"is_entrypoint": True, # Shell scripts are typically entrypoints
|
|
522
|
+
"complexity": {
|
|
523
|
+
"functions": len(functions),
|
|
524
|
+
"classes": 0,
|
|
525
|
+
"methods": 0,
|
|
526
|
+
"imports": len(set(imports)),
|
|
527
|
+
"lines": line_count,
|
|
528
|
+
},
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
|
|
532
|
+
# ---------------------------------------------------------------------------
|
|
533
|
+
# Minimal analyzer (coverage tracking only)
|
|
534
|
+
# ---------------------------------------------------------------------------
|
|
535
|
+
|
|
536
|
+
def analyze_minimal(file_path, rel_path):
|
|
537
|
+
"""Minimal analysis for file types without a structural analyzer.
|
|
538
|
+
|
|
539
|
+
Only counts lines — enough for coverage-gap detection and basic
|
|
540
|
+
scaffolding. No function/class extraction.
|
|
541
|
+
|
|
542
|
+
@reads: single source file
|
|
543
|
+
"""
|
|
544
|
+
try:
|
|
545
|
+
source = file_path.read_text(encoding="utf-8", errors="replace")
|
|
546
|
+
except OSError:
|
|
547
|
+
return None
|
|
548
|
+
|
|
549
|
+
line_count = len(source.splitlines())
|
|
550
|
+
|
|
551
|
+
return {
|
|
552
|
+
"rel_path": rel_path,
|
|
553
|
+
"line_count": line_count,
|
|
554
|
+
"docstring_summary": "",
|
|
555
|
+
"functions": [],
|
|
556
|
+
"classes": [],
|
|
557
|
+
"imports": [],
|
|
558
|
+
"has_main_guard": False,
|
|
559
|
+
"has_argparse": False,
|
|
560
|
+
"is_entrypoint": False,
|
|
561
|
+
"complexity": {
|
|
562
|
+
"functions": 0,
|
|
563
|
+
"classes": 0,
|
|
564
|
+
"methods": 0,
|
|
565
|
+
"imports": 0,
|
|
566
|
+
"lines": line_count,
|
|
567
|
+
},
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
|
|
571
|
+
# ---------------------------------------------------------------------------
|
|
572
|
+
# Charter name generation
|
|
573
|
+
# ---------------------------------------------------------------------------
|
|
574
|
+
|
|
575
|
+
def charter_name_from_path(rel_path):
|
|
576
|
+
"""Generate a charter name from a file path.
|
|
577
|
+
|
|
578
|
+
Examples:
|
|
579
|
+
src/auth/login.py -> auth-login
|
|
580
|
+
gator-command/scripts/gator-fleet-report.py -> gator-fleet-report
|
|
581
|
+
lib/utils.py -> utils
|
|
582
|
+
"""
|
|
583
|
+
p = Path(rel_path)
|
|
584
|
+
stem = p.stem
|
|
585
|
+
|
|
586
|
+
# For files in a meaningful directory, include the parent
|
|
587
|
+
parent = p.parent
|
|
588
|
+
if parent != Path(".") and parent.name not in (".", "src", "lib", "scripts"):
|
|
589
|
+
return f"{parent.name}-{stem}"
|
|
590
|
+
|
|
591
|
+
return stem
|
|
592
|
+
|
|
593
|
+
|
|
594
|
+
# ---------------------------------------------------------------------------
|
|
595
|
+
# Scaffold generation
|
|
596
|
+
# ---------------------------------------------------------------------------
|
|
597
|
+
|
|
598
|
+
def generate_scaffold(analysis):
|
|
599
|
+
"""Generate a markdown charter scaffold from analysis results.
|
|
600
|
+
|
|
601
|
+
@reads: analysis dict from analyze_python()
|
|
602
|
+
"""
|
|
603
|
+
rel_path = analysis["rel_path"]
|
|
604
|
+
name = charter_name_from_path(rel_path)
|
|
605
|
+
complexity = analysis["complexity"]
|
|
606
|
+
today = date.today().isoformat()
|
|
607
|
+
|
|
608
|
+
lines = []
|
|
609
|
+
lines.append(f"# Charter: {name}")
|
|
610
|
+
lines.append("")
|
|
611
|
+
lines.append(f"**Covers**: `{rel_path}`")
|
|
612
|
+
lines.append("")
|
|
613
|
+
|
|
614
|
+
# Owns section
|
|
615
|
+
lines.append("## Owns")
|
|
616
|
+
lines.append("")
|
|
617
|
+
if analysis.get("docstring_summary"):
|
|
618
|
+
lines.append(f"<!-- Mechanical hint: module docstring says: {analysis['docstring_summary']} -->")
|
|
619
|
+
lines.append("<!-- Agent enrichment needed: describe what this module owns and why -->")
|
|
620
|
+
lines.append("")
|
|
621
|
+
|
|
622
|
+
# Does Not Own section
|
|
623
|
+
lines.append("## Does Not Own")
|
|
624
|
+
lines.append("")
|
|
625
|
+
lines.append("<!-- Agent enrichment needed: define boundaries -->")
|
|
626
|
+
lines.append("")
|
|
627
|
+
lines.append("---")
|
|
628
|
+
lines.append("")
|
|
629
|
+
|
|
630
|
+
# Public functions
|
|
631
|
+
public_fns = [f for f in analysis["functions"] if not f["is_private"]]
|
|
632
|
+
private_fns = [f for f in analysis["functions"] if f["is_private"]]
|
|
633
|
+
|
|
634
|
+
for fn in public_fns:
|
|
635
|
+
lines.append(f"### {fn['signature']}")
|
|
636
|
+
lines.append(f"File: `{rel_path}`")
|
|
637
|
+
if fn["docstring_summary"]:
|
|
638
|
+
lines.append(f"<!-- Mechanical hint: {fn['docstring_summary']} -->")
|
|
639
|
+
lines.append("<!-- Agent enrichment needed: purpose, filesystem access, callers, tripwires -->")
|
|
640
|
+
lines.append("")
|
|
641
|
+
|
|
642
|
+
# Classes
|
|
643
|
+
for cls in analysis["classes"]:
|
|
644
|
+
lines.append(f"### class {cls['name']}")
|
|
645
|
+
lines.append(f"File: `{rel_path}`")
|
|
646
|
+
if cls["docstring_summary"]:
|
|
647
|
+
lines.append(f"<!-- Mechanical hint: {cls['docstring_summary']} -->")
|
|
648
|
+
lines.append("<!-- Agent enrichment needed: purpose, responsibilities -->")
|
|
649
|
+
lines.append("")
|
|
650
|
+
|
|
651
|
+
pub_methods = [m for m in cls["methods"]
|
|
652
|
+
if not m["name"].startswith("_") or m["name"] in ("__init__",)]
|
|
653
|
+
for m in pub_methods:
|
|
654
|
+
lines.append(f"#### {cls['name']}.{m['signature']}")
|
|
655
|
+
if m["docstring_summary"]:
|
|
656
|
+
lines.append(f"<!-- Mechanical hint: {m['docstring_summary']} -->")
|
|
657
|
+
lines.append("<!-- Agent enrichment needed -->")
|
|
658
|
+
lines.append("")
|
|
659
|
+
|
|
660
|
+
# Internal helpers (collapsed)
|
|
661
|
+
if private_fns:
|
|
662
|
+
lines.append("### Internal helpers")
|
|
663
|
+
lines.append("")
|
|
664
|
+
for fn in private_fns:
|
|
665
|
+
summary = f" — {fn['docstring_summary']}" if fn["docstring_summary"] else ""
|
|
666
|
+
lines.append(f"- `{fn['signature']}`{summary}")
|
|
667
|
+
lines.append("")
|
|
668
|
+
|
|
669
|
+
lines.append("---")
|
|
670
|
+
lines.append("")
|
|
671
|
+
|
|
672
|
+
# Mechanical scaffold notes
|
|
673
|
+
lines.append("## Scaffold Notes")
|
|
674
|
+
lines.append("")
|
|
675
|
+
if analysis["imports"]:
|
|
676
|
+
lines.append(f"- **Imports**: {', '.join(analysis['imports'])}")
|
|
677
|
+
lines.append(f"- **Complexity**: {complexity['functions']} functions, "
|
|
678
|
+
f"{complexity['classes']} classes, "
|
|
679
|
+
f"{complexity['methods']} methods, "
|
|
680
|
+
f"{complexity['lines']} lines")
|
|
681
|
+
lines.append(f"- **Entrypoint**: {'yes' if analysis['is_entrypoint'] else 'no'}")
|
|
682
|
+
lines.append(f"- Generated: {today} by gator-charter-draft {VERSION}")
|
|
683
|
+
lines.append("")
|
|
684
|
+
|
|
685
|
+
# Connections placeholder
|
|
686
|
+
lines.append("## Connections")
|
|
687
|
+
lines.append("")
|
|
688
|
+
lines.append("<!-- Agent enrichment needed: cross-references to other charters -->")
|
|
689
|
+
lines.append("-> [Index](INDEX.md)")
|
|
690
|
+
lines.append("")
|
|
691
|
+
|
|
692
|
+
return "\n".join(lines), name
|
|
693
|
+
|
|
694
|
+
|
|
695
|
+
# ---------------------------------------------------------------------------
|
|
696
|
+
# Output
|
|
697
|
+
# ---------------------------------------------------------------------------
|
|
698
|
+
|
|
699
|
+
def print_scaffolds(scaffolds):
|
|
700
|
+
"""Print all scaffolds to stdout, separated by dividers."""
|
|
701
|
+
for i, (text, name) in enumerate(scaffolds):
|
|
702
|
+
if i > 0:
|
|
703
|
+
print("\n" + "=" * 72 + "\n")
|
|
704
|
+
print(text)
|
|
705
|
+
|
|
706
|
+
|
|
707
|
+
def print_json(analyses, scaffolds):
|
|
708
|
+
"""Output structured JSON."""
|
|
709
|
+
items = []
|
|
710
|
+
for analysis, (text, name) in zip(analyses, scaffolds):
|
|
711
|
+
items.append({
|
|
712
|
+
"charter_name": name,
|
|
713
|
+
"source_file": analysis["rel_path"],
|
|
714
|
+
"complexity": analysis["complexity"],
|
|
715
|
+
"is_entrypoint": analysis["is_entrypoint"],
|
|
716
|
+
"function_count": len(analysis["functions"]),
|
|
717
|
+
"class_count": len(analysis["classes"]),
|
|
718
|
+
"scaffold_lines": len(text.splitlines()),
|
|
719
|
+
})
|
|
720
|
+
|
|
721
|
+
output = {
|
|
722
|
+
"version": VERSION,
|
|
723
|
+
"generated": date.today().isoformat(),
|
|
724
|
+
"file_count": len(items),
|
|
725
|
+
"charters": items,
|
|
726
|
+
}
|
|
727
|
+
print(json.dumps(output, indent=2))
|
|
728
|
+
|
|
729
|
+
|
|
730
|
+
def write_scaffolds(scaffolds, output_dir, dry_run=False):
|
|
731
|
+
"""Write scaffold files to the specified charter directory.
|
|
732
|
+
|
|
733
|
+
Never overwrites existing charter files.
|
|
734
|
+
|
|
735
|
+
@writes: <output_dir>/<name>.md (new files only)
|
|
736
|
+
"""
|
|
737
|
+
charters_dir = Path(output_dir)
|
|
738
|
+
charters_dir.mkdir(parents=True, exist_ok=True)
|
|
739
|
+
|
|
740
|
+
written = 0
|
|
741
|
+
skipped = 0
|
|
742
|
+
|
|
743
|
+
for text, name in scaffolds:
|
|
744
|
+
filename = f"{name}.md"
|
|
745
|
+
dest = charters_dir / filename
|
|
746
|
+
|
|
747
|
+
if dest.exists():
|
|
748
|
+
if dry_run:
|
|
749
|
+
print(f" skip (exists): {filename}")
|
|
750
|
+
skipped += 1
|
|
751
|
+
continue
|
|
752
|
+
|
|
753
|
+
if dry_run:
|
|
754
|
+
print(f" would write: {filename}")
|
|
755
|
+
else:
|
|
756
|
+
dest.write_text(text, encoding="utf-8")
|
|
757
|
+
print(f" + {filename}")
|
|
758
|
+
written += 1
|
|
759
|
+
|
|
760
|
+
return written, skipped
|
|
761
|
+
|
|
762
|
+
|
|
763
|
+
def print_index_suggestions(scaffolds, analyses):
|
|
764
|
+
"""Print INDEX.md routing suggestions."""
|
|
765
|
+
print()
|
|
766
|
+
print(" INDEX.md suggestions:")
|
|
767
|
+
print()
|
|
768
|
+
print(" | Charter | Covers | Complexity |")
|
|
769
|
+
print(" |---------|--------|------------|")
|
|
770
|
+
for analysis, (_, name) in zip(analyses, scaffolds):
|
|
771
|
+
c = analysis["complexity"]
|
|
772
|
+
score = f"{c['functions']}f/{c['classes']}c/{c['lines']}L"
|
|
773
|
+
print(f" | [{name}]({name}.md) | `{analysis['rel_path']}` | {score} |")
|
|
774
|
+
print()
|
|
775
|
+
|
|
776
|
+
|
|
777
|
+
# ---------------------------------------------------------------------------
|
|
778
|
+
# Main
|
|
779
|
+
# ---------------------------------------------------------------------------
|
|
780
|
+
|
|
781
|
+
def main():
|
|
782
|
+
ensure_utf8_stdout()
|
|
783
|
+
|
|
784
|
+
parser = argparse.ArgumentParser(
|
|
785
|
+
description="Gator charter-draft — mechanical charter scaffold generator.",
|
|
786
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
787
|
+
)
|
|
788
|
+
parser.add_argument(
|
|
789
|
+
"--path", "-p",
|
|
790
|
+
help="Path to repo (default: current directory, walks up to find .gator/)",
|
|
791
|
+
)
|
|
792
|
+
parser.add_argument(
|
|
793
|
+
"--dirs", "-d", nargs="+",
|
|
794
|
+
help="Directories to scope to (relative to repo root). "
|
|
795
|
+
"If omitted, scans all tracked files minus .charterignore exclusions.",
|
|
796
|
+
)
|
|
797
|
+
parser.add_argument(
|
|
798
|
+
"--output-dir", "-o",
|
|
799
|
+
help="Directory to write charters to (default: .gator/charters/). "
|
|
800
|
+
"Use this to target a different charter surface, e.g. gator-command/charters/.",
|
|
801
|
+
)
|
|
802
|
+
parser.add_argument(
|
|
803
|
+
"--write", "-w", action="store_true",
|
|
804
|
+
help="Write draft charters (never overwrites existing). "
|
|
805
|
+
"Default target: .gator/charters/ (override with --output-dir).",
|
|
806
|
+
)
|
|
807
|
+
parser.add_argument(
|
|
808
|
+
"--dry-run", "-n", action="store_true",
|
|
809
|
+
help="Show what would be written without writing",
|
|
810
|
+
)
|
|
811
|
+
parser.add_argument(
|
|
812
|
+
"--json", "-j", action="store_true",
|
|
813
|
+
help="Output structured JSON",
|
|
814
|
+
)
|
|
815
|
+
parser.add_argument(
|
|
816
|
+
"--index", "-i", action="store_true",
|
|
817
|
+
help="Include INDEX.md routing suggestions",
|
|
818
|
+
)
|
|
819
|
+
args = parser.parse_args()
|
|
820
|
+
|
|
821
|
+
# Find repo root
|
|
822
|
+
repo_root = find_gator_root(args.path)
|
|
823
|
+
if not repo_root:
|
|
824
|
+
print(" Error: no .gator/ found. Run from a gatorized repo.", file=sys.stderr)
|
|
825
|
+
sys.exit(1)
|
|
826
|
+
|
|
827
|
+
gator_dir = repo_root / ".gator"
|
|
828
|
+
|
|
829
|
+
# Discover files
|
|
830
|
+
files = discover_files(repo_root, gator_dir, args.dirs)
|
|
831
|
+
|
|
832
|
+
if not files:
|
|
833
|
+
if args.json:
|
|
834
|
+
print(json.dumps({"version": VERSION, "file_count": 0, "charters": []}))
|
|
835
|
+
else:
|
|
836
|
+
print()
|
|
837
|
+
print(" gator charter-draft")
|
|
838
|
+
print()
|
|
839
|
+
print(" No charterable source files found.")
|
|
840
|
+
if args.dirs:
|
|
841
|
+
print(f" Scoped to: {', '.join(args.dirs)}")
|
|
842
|
+
print(" Check .gator/.charterignore and --dirs scope.")
|
|
843
|
+
print()
|
|
844
|
+
return
|
|
845
|
+
|
|
846
|
+
# Analyze
|
|
847
|
+
analyses = []
|
|
848
|
+
for rel_path in files:
|
|
849
|
+
abs_path = repo_root / rel_path
|
|
850
|
+
ext = Path(rel_path).suffix.lower()
|
|
851
|
+
|
|
852
|
+
result = None
|
|
853
|
+
if ext == ".py":
|
|
854
|
+
result = analyze_python(abs_path, rel_path)
|
|
855
|
+
elif ext in (".js", ".jsx", ".ts", ".tsx"):
|
|
856
|
+
result = analyze_javascript(abs_path, rel_path)
|
|
857
|
+
elif ext in (".sh", ".bash"):
|
|
858
|
+
result = analyze_shell(abs_path, rel_path)
|
|
859
|
+
else:
|
|
860
|
+
# Minimal analysis for coverage tracking only
|
|
861
|
+
result = analyze_minimal(abs_path, rel_path)
|
|
862
|
+
|
|
863
|
+
if result:
|
|
864
|
+
analyses.append(result)
|
|
865
|
+
|
|
866
|
+
if not analyses:
|
|
867
|
+
if args.json:
|
|
868
|
+
print(json.dumps({"version": VERSION, "file_count": 0, "charters": []}))
|
|
869
|
+
else:
|
|
870
|
+
print()
|
|
871
|
+
print(" gator charter-draft")
|
|
872
|
+
print()
|
|
873
|
+
print(f" Found {len(files)} source files but none could be parsed.")
|
|
874
|
+
print()
|
|
875
|
+
return
|
|
876
|
+
|
|
877
|
+
# Generate scaffolds
|
|
878
|
+
scaffolds = [generate_scaffold(a) for a in analyses]
|
|
879
|
+
|
|
880
|
+
# Output
|
|
881
|
+
if args.json:
|
|
882
|
+
print_json(analyses, scaffolds)
|
|
883
|
+
return
|
|
884
|
+
|
|
885
|
+
if not args.write and not args.dry_run:
|
|
886
|
+
print()
|
|
887
|
+
print(f" gator charter-draft ({len(scaffolds)} files → {len(scaffolds)} charter drafts)")
|
|
888
|
+
print()
|
|
889
|
+
print_scaffolds(scaffolds)
|
|
890
|
+
|
|
891
|
+
if args.index:
|
|
892
|
+
print_index_suggestions(scaffolds, analyses)
|
|
893
|
+
return
|
|
894
|
+
|
|
895
|
+
# Resolve output directory
|
|
896
|
+
if args.output_dir:
|
|
897
|
+
output_dir = Path(args.output_dir)
|
|
898
|
+
if not output_dir.is_absolute():
|
|
899
|
+
output_dir = repo_root / output_dir
|
|
900
|
+
else:
|
|
901
|
+
output_dir = gator_dir / "charters"
|
|
902
|
+
|
|
903
|
+
# Write or dry-run
|
|
904
|
+
mode = "dry run" if args.dry_run else "write"
|
|
905
|
+
print()
|
|
906
|
+
print(f" gator charter-draft ({mode})")
|
|
907
|
+
print(f" target: {output_dir}")
|
|
908
|
+
print()
|
|
909
|
+
written, skipped = write_scaffolds(scaffolds, output_dir, dry_run=args.dry_run)
|
|
910
|
+
|
|
911
|
+
if args.index:
|
|
912
|
+
print_index_suggestions(scaffolds, analyses)
|
|
913
|
+
|
|
914
|
+
print(f" {written} {'would be written' if args.dry_run else 'written'}, {skipped} skipped (already exist)")
|
|
915
|
+
print()
|
|
916
|
+
|
|
917
|
+
|
|
918
|
+
if __name__ == "__main__":
|
|
919
|
+
main()
|