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.
Files changed (110) hide show
  1. gator_command/__init__.py +2 -0
  2. gator_command/cli.py +137 -0
  3. gator_command/scripts/crawler.py +633 -0
  4. gator_command/scripts/dashboard/dashboard.css +982 -0
  5. gator_command/scripts/dashboard/dashboard.html +84 -0
  6. gator_command/scripts/dashboard/dashboard.js +419 -0
  7. gator_command/scripts/dashboard/views/audit.js +270 -0
  8. gator_command/scripts/dashboard/views/fleet.js +307 -0
  9. gator_command/scripts/dashboard/views/repo.js +599 -0
  10. gator_command/scripts/dashboard/views/settings.js +173 -0
  11. gator_command/scripts/dashboard/views/updates.js +308 -0
  12. gator_command/scripts/enforcer-prompt.md +22 -0
  13. gator_command/scripts/extract-claude-sessions.py +489 -0
  14. gator_command/scripts/extract-codex-sessions.py +477 -0
  15. gator_command/scripts/extract-gemini-sessions.py +410 -0
  16. gator_command/scripts/gator-audit.py +956 -0
  17. gator_command/scripts/gator-charter-draft.py +919 -0
  18. gator_command/scripts/gator-charter-lint.py +427 -0
  19. gator_command/scripts/gator-charter-verify.py +606 -0
  20. gator_command/scripts/gator-dashboard.py +1271 -0
  21. gator_command/scripts/gator-deploy.py +916 -0
  22. gator_command/scripts/gator-drift.py +569 -0
  23. gator_command/scripts/gator-enforce.py +82 -0
  24. gator_command/scripts/gator-fleet-intel.py +460 -0
  25. gator_command/scripts/gator-fleet-report.py +615 -0
  26. gator_command/scripts/gator-init-command-post.py +315 -0
  27. gator_command/scripts/gator-init.py +434 -0
  28. gator_command/scripts/gator-machine-id.py +153 -0
  29. gator_command/scripts/gator-policy-status.py +631 -0
  30. gator_command/scripts/gator-pulse.py +459 -0
  31. gator_command/scripts/gator-repo-status.py +649 -0
  32. gator_command/scripts/gator-session-common.py +372 -0
  33. gator_command/scripts/gator-session-sink.py +831 -0
  34. gator_command/scripts/gator-sessions.py +1244 -0
  35. gator_command/scripts/gator-update.py +615 -0
  36. gator_command/scripts/gator-version.py +38 -0
  37. gator_command/scripts/gator_core.py +489 -0
  38. gator_command/scripts/gator_remote.py +381 -0
  39. gator_command/scripts/gator_runtime.py +142 -0
  40. gator_command/scripts/gatorize-actions.sh +989 -0
  41. gator_command/scripts/gatorize-lib.sh +166 -0
  42. gator_command/scripts/gatorize-post.sh +394 -0
  43. gator_command/scripts/gatorize.py +1163 -0
  44. gator_command/scripts/gatorize.sh +185 -0
  45. gator_command/scripts/generate_markdown.py +212 -0
  46. gator_command/scripts/generate_wiki.py +424 -0
  47. gator_command/scripts/graph_health.py +780 -0
  48. gator_command/scripts/memex-lint.py +286 -0
  49. gator_command/scripts/memex-lint.sh +205 -0
  50. gator_command/scripts/memex.py +1472 -0
  51. gator_command/scripts/memex_formatters.py +191 -0
  52. gator_command/scripts/memex_state.py +236 -0
  53. gator_command/scripts/spawn.py +650 -0
  54. gator_command/templates/gator-starter/blueprints/README.md +32 -0
  55. gator_command/templates/gator-starter/charterignore +53 -0
  56. gator_command/templates/gator-starter/charters/README.md +178 -0
  57. gator_command/templates/gator-starter/charters/_template.md +31 -0
  58. gator_command/templates/gator-starter/commands/commit.md +33 -0
  59. gator_command/templates/gator-starter/commands/init.md +11 -0
  60. gator_command/templates/gator-starter/commands/update.md +5 -0
  61. gator_command/templates/gator-starter/constitution.md +165 -0
  62. gator_command/templates/gator-starter/field-guides/README.md +25 -0
  63. gator_command/templates/gator-starter/gator-start-up.md +119 -0
  64. gator_command/templates/gator-starter/procedures/charter-alignment.md +83 -0
  65. gator_command/templates/gator-starter/procedures/enforcer-review.md +317 -0
  66. gator_command/templates/gator-starter/procedures/field-guide-generation.md +176 -0
  67. gator_command/templates/gator-starter/procedures/knowledge-capture.md +57 -0
  68. gator_command/templates/gator-starter/procedures/significance-check.md +69 -0
  69. gator_command/templates/gator-starter/reference-notes/concierge-responses.md +535 -0
  70. gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +91 -0
  71. gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +22 -0
  72. gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +232 -0
  73. gator_command/templates/gator-starter/reference-notes/example-project.md +289 -0
  74. gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +72 -0
  75. gator_command/templates/gator-starter/reference-notes/git-workflow.md +60 -0
  76. gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +37 -0
  77. gator_command/templates/gator-starter/reference-notes/refactor-approach.md +155 -0
  78. gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +108 -0
  79. gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +99 -0
  80. gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +155 -0
  81. gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  82. gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  83. gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  84. gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  85. gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  86. gator_command/templates/gator-starter/scripts/enforcer-prompt.md +55 -0
  87. gator_command/templates/gator-starter/scripts/enforcer-review.py +1551 -0
  88. gator_command/templates/gator-starter/scripts/gator-approve.py +139 -0
  89. gator_command/templates/gator-starter/scripts/gator-enforce.py +82 -0
  90. gator_command/templates/gator-starter/scripts/gator-init.py +434 -0
  91. gator_command/templates/gator-starter/scripts/gator-pre-commit.py +2670 -0
  92. gator_command/templates/gator-starter/scripts/gator-pulse.py +459 -0
  93. gator_command/templates/gator-starter/scripts/gator-update.py +615 -0
  94. gator_command/templates/gator-starter/scripts/gator-version.py +38 -0
  95. gator_command/templates/gator-starter/scripts/gator_core.py +487 -0
  96. gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  97. gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  98. gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  99. gator_command/templates/gator-starter/scripts/hooks/commit-msg +5 -0
  100. gator_command/templates/gator-starter/scripts/hooks/post-commit +7 -0
  101. gator_command/templates/gator-starter/scripts/hooks/pre-commit +5 -0
  102. gator_command/templates/gator-starter/sessions/.gitignore +7 -0
  103. gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  104. gator_command/templates/gator-starter/whiteboard.md +5 -0
  105. gator_command-1.0.0.dist-info/METADATA +122 -0
  106. gator_command-1.0.0.dist-info/RECORD +110 -0
  107. gator_command-1.0.0.dist-info/WHEEL +5 -0
  108. gator_command-1.0.0.dist-info/entry_points.txt +2 -0
  109. gator_command-1.0.0.dist-info/licenses/LICENSE +21 -0
  110. 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()