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,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()
|