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,606 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator charter-verify — Structural charter quality checker.
4
+
5
+ Compares existing charters against actual code structure and emits
6
+ findings. Machine checks plausibility; enforcer judges materiality.
7
+
8
+ Finding classes:
9
+ coverage-gap — tracked source file not covered by any charter
10
+ function-gap — public function not represented in any charter
11
+ complexity-mismatch — complex file with unusually thin charter
12
+ stale-structure — charter references functions that no longer exist
13
+ cross-cutting-suspect — high import fan-out without cross-cutting treatment
14
+
15
+ Usage:
16
+ python gator-command/scripts/gator-charter-verify.py --path <repo>
17
+ python gator-command/scripts/gator-charter-verify.py --path <repo> --json
18
+ python gator-command/scripts/gator-charter-verify.py --path <repo> --changed-only
19
+
20
+ @reads: charter files, source files (via ast), .gator/.charterignore, git
21
+ @writes: stdout (text or json)
22
+ """
23
+
24
+ import argparse
25
+ import json
26
+ import re
27
+ import sys
28
+ from pathlib import Path
29
+
30
+ from gator_core import (
31
+ get_version, find_gator_root, ensure_utf8_stdout, git, import_sibling,
32
+ )
33
+
34
+ VERSION = get_version()
35
+
36
+ # Import shared analysis functions from charter-draft
37
+ _draft = import_sibling("gator-charter-draft")
38
+ if not _draft:
39
+ print(" Error: gator-charter-draft.py not found.", file=sys.stderr)
40
+ sys.exit(1)
41
+
42
+ discover_files = _draft.discover_files
43
+ analyze_python = _draft.analyze_python
44
+ load_charterignore = _draft.load_charterignore
45
+ SUPPORTED_EXTENSIONS = _draft.SUPPORTED_EXTENSIONS
46
+
47
+
48
+ # ---------------------------------------------------------------------------
49
+ # Charter parsing
50
+ # ---------------------------------------------------------------------------
51
+
52
+ def find_charter_dirs(repo_root):
53
+ """Find charter directories in the repo.
54
+
55
+ Returns a list of (dir_path, label) tuples. Checks both:
56
+ - .gator/charters/ (per-repo governance)
57
+ - gator-command/charters/ (command-post source charters)
58
+
59
+ @reads: repo directory structure
60
+ """
61
+ dirs = []
62
+ gator_charters = repo_root / ".gator" / "charters"
63
+ if gator_charters.is_dir():
64
+ dirs.append((gator_charters, ".gator/charters"))
65
+
66
+ gc_charters = repo_root / "gator-command" / "charters"
67
+ if gc_charters.is_dir():
68
+ dirs.append((gc_charters, "gator-command/charters"))
69
+
70
+ return dirs
71
+
72
+
73
+ def parse_charters(charter_dirs):
74
+ """Parse all charter files and extract structural information.
75
+
76
+ Returns a dict mapping source file paths (from Covers: lines) to
77
+ charter info, and a list of all charter entries with their functions.
78
+
79
+ @reads: charter .md files
80
+ """
81
+ # Maps: source_file_path -> list of charter names that cover it
82
+ coverage_map = {}
83
+ # Maps: charter_name -> set of documented function names
84
+ charter_functions = {}
85
+ # Maps: charter_name -> dict of function_name -> file_path
86
+ charter_function_files = {}
87
+ # All charter entries
88
+ charters = []
89
+
90
+ skip_files = {"_template.md", "README.md", "INDEX.md"}
91
+
92
+ for charter_dir, label in charter_dirs:
93
+ for md_file in sorted(charter_dir.glob("*.md")):
94
+ if md_file.name in skip_files:
95
+ continue
96
+
97
+ try:
98
+ text = md_file.read_text(encoding="utf-8", errors="replace")
99
+ except OSError:
100
+ continue
101
+
102
+ charter_name = md_file.stem
103
+ covers = _parse_covers(text)
104
+ functions, function_files = _parse_function_entries(text)
105
+
106
+ charter_functions[charter_name] = functions
107
+ charter_function_files[charter_name] = function_files
108
+
109
+ for src_path in covers:
110
+ if src_path not in coverage_map:
111
+ coverage_map[src_path] = []
112
+ coverage_map[src_path].append(charter_name)
113
+
114
+ charters.append({
115
+ "name": charter_name,
116
+ "file": str(md_file),
117
+ "label": label,
118
+ "covers": covers,
119
+ "functions": functions,
120
+ "line_count": len(text.splitlines()),
121
+ })
122
+
123
+ return coverage_map, charter_functions, charter_function_files, charters
124
+
125
+
126
+ def _parse_covers(text):
127
+ """Extract file paths from the **Covers**: line."""
128
+ paths = []
129
+ for line in text.splitlines():
130
+ if line.startswith("**Covers**:"):
131
+ # Extract backtick-quoted paths
132
+ paths.extend(re.findall(r'`([^`]+)`', line))
133
+ break
134
+ return paths
135
+
136
+
137
+ def _parse_function_entries(text):
138
+ """Extract function names from ### entries in a charter.
139
+
140
+ Recognizes patterns like:
141
+ - ### function_name(args)
142
+ - ### ClassName.method_name(args)
143
+ - ### class ClassName
144
+
145
+ Returns a set of function names (for coverage checks) and a dict
146
+ mapping function names to their associated File: path (for stale checks).
147
+ """
148
+ functions = set()
149
+ function_files = {} # name -> file path from File: line
150
+ last_file_ref = None # Track File: context for #### method entries
151
+
152
+ lines = text.splitlines()
153
+ for i, line in enumerate(lines):
154
+ # Match both ### and #### entries (methods use ####)
155
+ m = re.match(r'^#{3,4}\s+(?:class\s+)?(\w[\w.]*?)(?:\(|$)', line)
156
+ if m:
157
+ name = m.group(1)
158
+ # For ClassName.method, extract both class and method
159
+ if "." in name:
160
+ parts = name.split(".")
161
+ functions.add(parts[-1])
162
+ functions.add(name)
163
+ else:
164
+ functions.add(name)
165
+
166
+ # Look for File: line in the next few lines
167
+ found_file = False
168
+ for j in range(i + 1, min(i + 4, len(lines))):
169
+ fm = re.match(r'^File:\s*`([^`]+)`', lines[j])
170
+ if fm:
171
+ file_ref = fm.group(1)
172
+ function_files[name] = file_ref
173
+ if "." in name:
174
+ function_files[name.split(".")[-1]] = file_ref
175
+ last_file_ref = file_ref
176
+ found_file = True
177
+ break
178
+
179
+ # #### entries (methods) inherit the File: from their parent ### entry
180
+ if not found_file and line.startswith("####") and last_file_ref:
181
+ function_files[name] = last_file_ref
182
+ if "." in name:
183
+ function_files[name.split(".")[-1]] = last_file_ref
184
+
185
+ # Track File: lines for context inheritance
186
+ elif line.startswith("File:"):
187
+ fm = re.match(r'^File:\s*`([^`]+)`', line)
188
+ if fm:
189
+ last_file_ref = fm.group(1)
190
+
191
+ # Reset context at section boundaries
192
+ elif line.startswith("---") or line.startswith("## "):
193
+ last_file_ref = None
194
+
195
+ return functions, function_files
196
+
197
+
198
+ # ---------------------------------------------------------------------------
199
+ # Finding generation
200
+ # ---------------------------------------------------------------------------
201
+
202
+ def verify(repo_root, gator_dir, dirs=None, changed_only=False):
203
+ """Run all verification checks. Returns a list of findings.
204
+
205
+ @reads: charters, source files, git
206
+ """
207
+ findings = []
208
+
209
+ # Discover charterable files
210
+ all_files = discover_files(repo_root, gator_dir, dirs)
211
+
212
+ # If --changed-only, filter to files changed since last commit
213
+ if changed_only:
214
+ diff_output, ok = git("diff", "--name-only", "HEAD~1", "HEAD", cwd=repo_root)
215
+ if ok and diff_output:
216
+ changed = set(diff_output.splitlines())
217
+ all_files = [f for f in all_files if f in changed]
218
+
219
+ # Parse existing charters
220
+ charter_dirs = find_charter_dirs(repo_root)
221
+ coverage_map, charter_functions, charter_function_files, charters = parse_charters(charter_dirs)
222
+
223
+ # Analyze source files (all supported languages)
224
+ analyses = {}
225
+ for rel_path in all_files:
226
+ abs_path = repo_root / rel_path
227
+ ext = Path(rel_path).suffix.lower()
228
+
229
+ result = None
230
+ if ext == ".py":
231
+ result = analyze_python(abs_path, rel_path)
232
+ elif ext in (".js", ".jsx", ".ts", ".tsx"):
233
+ result = _draft.analyze_javascript(abs_path, rel_path)
234
+ elif ext in (".sh", ".bash"):
235
+ result = _draft.analyze_shell(abs_path, rel_path)
236
+ else:
237
+ result = _draft.analyze_minimal(abs_path, rel_path)
238
+
239
+ if result:
240
+ analyses[rel_path] = result
241
+
242
+ # --- Check: coverage-gap ---
243
+ findings.extend(_check_coverage_gaps(all_files, coverage_map))
244
+
245
+ # --- Check: function-gap ---
246
+ findings.extend(_check_function_gaps(analyses, coverage_map, charter_functions))
247
+
248
+ # --- Check: complexity-mismatch ---
249
+ findings.extend(_check_complexity_mismatch(analyses, coverage_map, charters))
250
+
251
+ # --- Check: stale-structure ---
252
+ findings.extend(_check_stale_structure(analyses, coverage_map, charter_functions, charter_function_files))
253
+
254
+ # --- Check: cross-cutting-suspect ---
255
+ findings.extend(_check_cross_cutting(analyses, coverage_map))
256
+
257
+ return findings
258
+
259
+
260
+ def _check_coverage_gaps(all_files, coverage_map):
261
+ """Find tracked source files not covered by any charter."""
262
+ findings = []
263
+ for rel_path in all_files:
264
+ if rel_path not in coverage_map:
265
+ findings.append({
266
+ "class": "coverage-gap",
267
+ "severity": "warn",
268
+ "file": rel_path,
269
+ "message": f"Not covered by any charter",
270
+ })
271
+ return findings
272
+
273
+
274
+ # Functions that are conventionally not charter-worthy
275
+ _SKIP_FUNCTION_NAMES = {
276
+ "main", "setup", "teardown",
277
+ # Common output helpers — rarely need charter entries
278
+ "print_report", "print_json", "print_json_report", "print_result",
279
+ "print_plan", "print_json_plan", "print_findings", "print_json_findings",
280
+ "print_scaffolds", "print_index_suggestions",
281
+ # Common rendering helpers
282
+ "escHtml", "fmtTs", "fmtDate",
283
+ }
284
+
285
+
286
+ def _is_charter_worthy(fn, analysis):
287
+ """Heuristic: is this function significant enough to warrant a charter entry?
288
+
289
+ Filters out conventional helpers and small utility functions to reduce noise.
290
+ Only flags functions that look like public API, complex logic, or entrypoints.
291
+ """
292
+ name = fn["name"]
293
+
294
+ # Skip conventional names
295
+ if name in _SKIP_FUNCTION_NAMES:
296
+ return False
297
+
298
+ # Skip private functions
299
+ if fn["is_private"]:
300
+ return False
301
+
302
+ # Skip very short names (likely helpers: e.g., "go", "run", "ok")
303
+ if len(name) <= 2:
304
+ return False
305
+
306
+ # Always flag if the function has a docstring (author intended it to be public API)
307
+ if fn.get("docstring_summary"):
308
+ return True
309
+
310
+ # Flag functions with many arguments (likely significant interface)
311
+ if len(fn.get("args", [])) >= 4:
312
+ return True
313
+
314
+ # In files with few functions, each one is more likely significant
315
+ total_public = sum(1 for f in analysis["functions"] if not f["is_private"])
316
+ if total_public <= 3:
317
+ return True
318
+
319
+ # Flag functions whose name suggests significance (verbs that imply action)
320
+ significant_prefixes = (
321
+ "scan_", "collect_", "build_", "generate_", "execute_",
322
+ "install_", "deploy_", "validate_", "verify_", "resolve_",
323
+ "discover_", "analyze_", "parse_", "load_", "save_",
324
+ "check_", "create_", "update_", "delete_", "run_",
325
+ "render_", "handle_", "process_", "fetch_", "pull_",
326
+ )
327
+ if any(name.startswith(p) for p in significant_prefixes):
328
+ return True
329
+
330
+ return False
331
+
332
+
333
+ def _check_function_gaps(analyses, coverage_map, charter_functions):
334
+ """Find significant public functions not documented in their covering charter.
335
+
336
+ Uses heuristics to filter out conventional helpers and small utility
337
+ functions. Only flags functions that look charter-worthy.
338
+ """
339
+ findings = []
340
+ for rel_path, analysis in analyses.items():
341
+ # Only check files that have a charter
342
+ if rel_path not in coverage_map:
343
+ continue
344
+
345
+ charter_names = coverage_map[rel_path]
346
+ # Collect all documented functions across covering charters
347
+ all_documented = set()
348
+ for cn in charter_names:
349
+ all_documented.update(charter_functions.get(cn, set()))
350
+
351
+ # Check charter-worthy functions only
352
+ for fn in analysis["functions"]:
353
+ if fn["name"] in all_documented:
354
+ continue
355
+ if not _is_charter_worthy(fn, analysis):
356
+ continue
357
+ findings.append({
358
+ "class": "function-gap",
359
+ "severity": "info",
360
+ "file": rel_path,
361
+ "function": fn["name"],
362
+ "message": f"Public function `{fn['name']}()` not documented in charter",
363
+ "charter": charter_names[0],
364
+ })
365
+ return findings
366
+
367
+
368
+ def _check_complexity_mismatch(analyses, coverage_map, charters):
369
+ """Find complex files with unusually thin charters."""
370
+ findings = []
371
+
372
+ # Build charter line count map
373
+ charter_lines = {}
374
+ for c in charters:
375
+ for src_path in c["covers"]:
376
+ # Use the charter with the most lines if multiple cover this file
377
+ if src_path not in charter_lines or c["line_count"] > charter_lines[src_path]:
378
+ charter_lines[src_path] = c["line_count"]
379
+
380
+ for rel_path, analysis in analyses.items():
381
+ if rel_path not in coverage_map:
382
+ continue
383
+
384
+ c = analysis["complexity"]
385
+ # Heuristic: file has significant complexity
386
+ is_complex = (c["functions"] >= 8 or c["classes"] >= 2 or c["lines"] >= 300)
387
+ # Charter is thin (less than 30 lines suggests minimal documentation)
388
+ charter_lc = charter_lines.get(rel_path, 0)
389
+ is_thin = charter_lc < 30
390
+
391
+ if is_complex and is_thin:
392
+ findings.append({
393
+ "class": "complexity-mismatch",
394
+ "severity": "warn",
395
+ "file": rel_path,
396
+ "message": (
397
+ f"Complex file ({c['functions']}f/{c['classes']}c/{c['lines']}L) "
398
+ f"but charter is only {charter_lc} lines"
399
+ ),
400
+ })
401
+ return findings
402
+
403
+
404
+ def _check_stale_structure(analyses, coverage_map, charter_functions, charter_function_files):
405
+ """Find charter function entries that no longer exist in code.
406
+
407
+ Only flags functions whose File: line in the charter matches the
408
+ specific source file being checked. This avoids false positives when
409
+ a charter covers multiple files.
410
+ """
411
+ findings = []
412
+
413
+ for rel_path, analysis in analyses.items():
414
+ if rel_path not in coverage_map:
415
+ continue
416
+
417
+ charter_names = coverage_map[rel_path]
418
+ # Get all actual function/class names in the file
419
+ actual_names = set()
420
+ for fn in analysis["functions"]:
421
+ actual_names.add(fn["name"])
422
+ for cls in analysis["classes"]:
423
+ actual_names.add(cls["name"])
424
+ for m in cls["methods"]:
425
+ actual_names.add(m["name"])
426
+ actual_names.add(f"{cls['name']}.{m['name']}")
427
+
428
+ for cn in charter_names:
429
+ documented = charter_functions.get(cn, set())
430
+ fn_files = charter_function_files.get(cn, {})
431
+
432
+ for doc_fn in documented:
433
+ # Skip generic section headers
434
+ if doc_fn in ("Internal_helpers", "Internal"):
435
+ continue
436
+
437
+ # Only check if the File: line points to this specific file
438
+ declared_file = fn_files.get(doc_fn, "")
439
+ if declared_file and declared_file != rel_path:
440
+ continue # This function belongs to a different file in the same charter
441
+
442
+ # If no File: line, skip — can't determine which file it belongs to
443
+ if not declared_file:
444
+ continue
445
+
446
+ base_name = doc_fn.split(".")[-1] if "." in doc_fn else doc_fn
447
+ if base_name not in actual_names and doc_fn not in actual_names:
448
+ findings.append({
449
+ "class": "stale-structure",
450
+ "severity": "info",
451
+ "file": rel_path,
452
+ "function": doc_fn,
453
+ "message": f"Charter `{cn}` documents `{doc_fn}()` but it was not found in `{rel_path}`",
454
+ "charter": cn,
455
+ })
456
+ return findings
457
+
458
+
459
+ def _check_cross_cutting(analyses, coverage_map):
460
+ """Find modules with high import fan-out that may need cross-cutting treatment."""
461
+ findings = []
462
+
463
+ for rel_path, analysis in analyses.items():
464
+ imports = analysis["imports"]
465
+ # Heuristic: importing from many sibling modules suggests orchestration
466
+ sibling_imports = [i for i in imports if not i.startswith(("os", "sys", "json",
467
+ "re", "pathlib", "datetime", "argparse", "subprocess", "shutil",
468
+ "io", "ast", "fnmatch", "collections", "typing", "dataclasses",
469
+ "threading", "socket", "http", "importlib", "filecmp", "webbrowser"))]
470
+
471
+ if len(sibling_imports) >= 5:
472
+ findings.append({
473
+ "class": "cross-cutting-suspect",
474
+ "severity": "info",
475
+ "file": rel_path,
476
+ "message": (
477
+ f"High sibling-import fan-out ({len(sibling_imports)} non-stdlib imports: "
478
+ f"{', '.join(sibling_imports[:5])}{'...' if len(sibling_imports) > 5 else ''})"
479
+ ),
480
+ })
481
+ return findings
482
+
483
+
484
+ # ---------------------------------------------------------------------------
485
+ # Output
486
+ # ---------------------------------------------------------------------------
487
+
488
+ def print_findings(findings):
489
+ """Print findings to terminal."""
490
+ if not findings:
491
+ print()
492
+ print(" gator charter-verify")
493
+ print()
494
+ print(" No findings. Charters look structurally sound.")
495
+ print()
496
+ return
497
+
498
+ # Group by severity
499
+ by_severity = {"warn": [], "info": []}
500
+ for f in findings:
501
+ by_severity.setdefault(f["severity"], []).append(f)
502
+
503
+ # Group by class
504
+ by_class = {}
505
+ for f in findings:
506
+ by_class.setdefault(f["class"], []).append(f)
507
+
508
+ print()
509
+ print(f" gator charter-verify ({len(findings)} findings)")
510
+ print()
511
+
512
+ # Summary counts
513
+ for cls, items in sorted(by_class.items()):
514
+ severity_counts = {}
515
+ for item in items:
516
+ severity_counts[item["severity"]] = severity_counts.get(item["severity"], 0) + 1
517
+ counts = ", ".join(f"{v} {k}" for k, v in sorted(severity_counts.items()))
518
+ print(f" {cls}: {len(items)} ({counts})")
519
+ print()
520
+
521
+ # Warnings first
522
+ warns = by_severity.get("warn", [])
523
+ if warns:
524
+ print(" ── Warnings ──")
525
+ print()
526
+ for f in warns:
527
+ print(f" [{f['class']}] {f['file']}")
528
+ print(f" {f['message']}")
529
+ print()
530
+
531
+ # Info
532
+ infos = by_severity.get("info", [])
533
+ if infos:
534
+ print(" ── Info ──")
535
+ print()
536
+ for f in infos:
537
+ print(f" [{f['class']}] {f['file']}")
538
+ print(f" {f['message']}")
539
+ print()
540
+
541
+
542
+ def print_json_findings(findings):
543
+ """Output findings as JSON."""
544
+ output = {
545
+ "version": VERSION,
546
+ "finding_count": len(findings),
547
+ "summary": {},
548
+ "findings": findings,
549
+ }
550
+ # Summary by class
551
+ for f in findings:
552
+ cls = f["class"]
553
+ output["summary"][cls] = output["summary"].get(cls, 0) + 1
554
+
555
+ print(json.dumps(output, indent=2))
556
+
557
+
558
+ # ---------------------------------------------------------------------------
559
+ # Main
560
+ # ---------------------------------------------------------------------------
561
+
562
+ def main():
563
+ ensure_utf8_stdout()
564
+
565
+ parser = argparse.ArgumentParser(
566
+ description="Gator charter-verify — structural charter quality checker.",
567
+ formatter_class=argparse.RawDescriptionHelpFormatter,
568
+ )
569
+ parser.add_argument(
570
+ "--path", "-p",
571
+ help="Path to repo (default: current directory, walks up to find .gator/)",
572
+ )
573
+ parser.add_argument(
574
+ "--dirs", "-d", nargs="+",
575
+ help="Directories to scope to (relative to repo root)",
576
+ )
577
+ parser.add_argument(
578
+ "--json", "-j", action="store_true",
579
+ help="Output findings as JSON",
580
+ )
581
+ parser.add_argument(
582
+ "--changed-only", "-c", action="store_true",
583
+ help="Only check files changed in the last commit",
584
+ )
585
+ args = parser.parse_args()
586
+
587
+ # Find repo root
588
+ repo_root = find_gator_root(args.path)
589
+ if not repo_root:
590
+ print(" Error: no .gator/ found. Run from a gatorized repo.", file=sys.stderr)
591
+ sys.exit(1)
592
+
593
+ gator_dir = repo_root / ".gator"
594
+
595
+ # Run verification
596
+ findings = verify(repo_root, gator_dir, args.dirs, args.changed_only)
597
+
598
+ # Output
599
+ if args.json:
600
+ print_json_findings(findings)
601
+ else:
602
+ print_findings(findings)
603
+
604
+
605
+ if __name__ == "__main__":
606
+ main()