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,427 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
gator-charter-lint — validate charter files against Charter Schema v1.
|
|
4
|
+
|
|
5
|
+
Usage:
|
|
6
|
+
python gator-charter-lint.py [--json] [--charter PATH] [--charters-dir DIR]
|
|
7
|
+
|
|
8
|
+
Structural validation only — checks that required sections exist and function
|
|
9
|
+
entries are well-formed. Does not check content quality, file path resolution,
|
|
10
|
+
or function name existence.
|
|
11
|
+
|
|
12
|
+
@reads: .gator/charters/*.md, gator-command/charters/*.md
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import argparse
|
|
16
|
+
import json
|
|
17
|
+
import re
|
|
18
|
+
import sys
|
|
19
|
+
from dataclasses import dataclass, field, asdict
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
23
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
24
|
+
|
|
25
|
+
try:
|
|
26
|
+
from gator_core import ensure_utf8_stdout
|
|
27
|
+
except ImportError:
|
|
28
|
+
def ensure_utf8_stdout():
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
SKIP_FILES = {"_template.md", "README.md"}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Data structures
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Finding:
|
|
40
|
+
check: str
|
|
41
|
+
severity: str # error, warn, info
|
|
42
|
+
line: int
|
|
43
|
+
message: str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass
|
|
47
|
+
class FunctionEntry:
|
|
48
|
+
name: str
|
|
49
|
+
line: int
|
|
50
|
+
has_file_line: bool = False
|
|
51
|
+
has_description: bool = False
|
|
52
|
+
has_annotations: bool = False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@dataclass
|
|
56
|
+
class CharterDoc:
|
|
57
|
+
path: str = ""
|
|
58
|
+
lines: list = field(default_factory=list)
|
|
59
|
+
title: str = ""
|
|
60
|
+
title_line: int = 0
|
|
61
|
+
has_covers: bool = False
|
|
62
|
+
covers_line: int = 0
|
|
63
|
+
sections: dict = field(default_factory=dict) # heading -> line number
|
|
64
|
+
separators: list = field(default_factory=list) # line numbers of ---
|
|
65
|
+
functions: list = field(default_factory=list) # list of FunctionEntry
|
|
66
|
+
is_index: bool = False
|
|
67
|
+
is_cross_cutting: bool = False
|
|
68
|
+
has_dispatch_table: bool = False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Parser
|
|
73
|
+
# ---------------------------------------------------------------------------
|
|
74
|
+
|
|
75
|
+
def parse_charter(path):
|
|
76
|
+
"""Parse a charter markdown file into a CharterDoc structure."""
|
|
77
|
+
text = Path(path).read_text(encoding="utf-8", errors="replace")
|
|
78
|
+
lines = text.splitlines()
|
|
79
|
+
doc = CharterDoc(path=str(path), lines=lines)
|
|
80
|
+
|
|
81
|
+
# Detect file type from first heading
|
|
82
|
+
for i, line in enumerate(lines):
|
|
83
|
+
stripped = line.strip()
|
|
84
|
+
if not stripped:
|
|
85
|
+
continue
|
|
86
|
+
if stripped.startswith("# Charter Index"):
|
|
87
|
+
doc.is_index = True
|
|
88
|
+
doc.title = stripped
|
|
89
|
+
doc.title_line = i + 1
|
|
90
|
+
break
|
|
91
|
+
if stripped.startswith("# Charter:"):
|
|
92
|
+
doc.title = stripped
|
|
93
|
+
doc.title_line = i + 1
|
|
94
|
+
break
|
|
95
|
+
if stripped.startswith("# "):
|
|
96
|
+
doc.title = stripped
|
|
97
|
+
doc.title_line = i + 1
|
|
98
|
+
break
|
|
99
|
+
|
|
100
|
+
# Scan for structural elements
|
|
101
|
+
non_blank_count = 0
|
|
102
|
+
current_section = None
|
|
103
|
+
current_func = None
|
|
104
|
+
in_table = False
|
|
105
|
+
|
|
106
|
+
ANNOTATION_PREFIXES = (
|
|
107
|
+
"@reads:", "@writes:", "Models:", "Filesystem:",
|
|
108
|
+
"Session R:", "Session W:",
|
|
109
|
+
"←", "<-", "→", "->", "!",
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
for i, line in enumerate(lines):
|
|
113
|
+
stripped = line.strip()
|
|
114
|
+
lineno = i + 1
|
|
115
|
+
|
|
116
|
+
# Track non-blank lines for covers detection
|
|
117
|
+
if stripped:
|
|
118
|
+
non_blank_count += 1
|
|
119
|
+
|
|
120
|
+
# Covers line
|
|
121
|
+
if "**Covers**:" in stripped or "**Covers**" in stripped:
|
|
122
|
+
doc.has_covers = True
|
|
123
|
+
doc.covers_line = lineno
|
|
124
|
+
|
|
125
|
+
# Separators
|
|
126
|
+
if stripped == "---":
|
|
127
|
+
doc.separators.append(lineno)
|
|
128
|
+
|
|
129
|
+
# Tables (for dispatch table detection)
|
|
130
|
+
if stripped.startswith("|") and "|" in stripped[1:]:
|
|
131
|
+
in_table = True
|
|
132
|
+
if "changing" in stripped.lower() or "charter" in stripped.lower():
|
|
133
|
+
doc.has_dispatch_table = True
|
|
134
|
+
elif in_table and not stripped.startswith("|"):
|
|
135
|
+
in_table = False
|
|
136
|
+
|
|
137
|
+
# Section headings (## level)
|
|
138
|
+
if stripped.startswith("## ") and not stripped.startswith("### "):
|
|
139
|
+
section_name = stripped[3:].strip()
|
|
140
|
+
doc.sections[section_name] = lineno
|
|
141
|
+
current_section = section_name
|
|
142
|
+
current_func = None
|
|
143
|
+
|
|
144
|
+
# Detect cross-cutting patterns
|
|
145
|
+
if "TRIPWIRE" in section_name or "Pattern" in section_name:
|
|
146
|
+
doc.is_cross_cutting = True
|
|
147
|
+
continue
|
|
148
|
+
|
|
149
|
+
# Function entries (### level)
|
|
150
|
+
if stripped.startswith("### "):
|
|
151
|
+
func_name = stripped[4:].strip()
|
|
152
|
+
current_func = FunctionEntry(name=func_name, line=lineno)
|
|
153
|
+
doc.functions.append(current_func)
|
|
154
|
+
continue
|
|
155
|
+
|
|
156
|
+
# Parse function entry content
|
|
157
|
+
if current_func and stripped:
|
|
158
|
+
if stripped.startswith("File:") or stripped.startswith("File :"):
|
|
159
|
+
current_func.has_file_line = True
|
|
160
|
+
elif any(stripped.startswith(p) for p in ANNOTATION_PREFIXES):
|
|
161
|
+
current_func.has_annotations = True
|
|
162
|
+
elif not current_func.has_description:
|
|
163
|
+
# First non-annotation, non-File line is description
|
|
164
|
+
# (File: line may be absent for bash/single-file charters)
|
|
165
|
+
current_func.has_description = True
|
|
166
|
+
|
|
167
|
+
return doc
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
# ---------------------------------------------------------------------------
|
|
171
|
+
# Validation
|
|
172
|
+
# ---------------------------------------------------------------------------
|
|
173
|
+
|
|
174
|
+
def _get_covers_text(doc):
|
|
175
|
+
"""Extract the covers line text from the doc."""
|
|
176
|
+
if not doc.has_covers:
|
|
177
|
+
return ""
|
|
178
|
+
idx = doc.covers_line - 1
|
|
179
|
+
if 0 <= idx < len(doc.lines):
|
|
180
|
+
return doc.lines[idx]
|
|
181
|
+
return ""
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def validate_charter(doc):
|
|
185
|
+
"""Validate a parsed CharterDoc. Returns list of Finding."""
|
|
186
|
+
findings = []
|
|
187
|
+
|
|
188
|
+
if doc.is_index:
|
|
189
|
+
return _validate_index(doc)
|
|
190
|
+
|
|
191
|
+
# Title
|
|
192
|
+
if not doc.title.startswith("# Charter:"):
|
|
193
|
+
findings.append(Finding(
|
|
194
|
+
"title-format", "error", doc.title_line or 1,
|
|
195
|
+
f"First heading should be '# Charter: [Name]', got: '{doc.title}'"
|
|
196
|
+
))
|
|
197
|
+
|
|
198
|
+
# Covers — not required for cross-cutting charters (they cover patterns, not files)
|
|
199
|
+
if not doc.has_covers and not doc.is_cross_cutting:
|
|
200
|
+
findings.append(Finding(
|
|
201
|
+
"covers-present", "error", 1,
|
|
202
|
+
"**Covers**: line missing (should appear within first 5 non-blank lines)"
|
|
203
|
+
))
|
|
204
|
+
|
|
205
|
+
# Required sections
|
|
206
|
+
owns_found = False
|
|
207
|
+
does_not_own_found = False
|
|
208
|
+
|
|
209
|
+
for section, lineno in doc.sections.items():
|
|
210
|
+
if section == "Owns":
|
|
211
|
+
owns_found = True
|
|
212
|
+
if section == "Does Not Own":
|
|
213
|
+
does_not_own_found = True
|
|
214
|
+
|
|
215
|
+
if not owns_found:
|
|
216
|
+
findings.append(Finding(
|
|
217
|
+
"owns-section", "error", 1,
|
|
218
|
+
"Missing required section: ## Owns"
|
|
219
|
+
))
|
|
220
|
+
|
|
221
|
+
if not does_not_own_found:
|
|
222
|
+
findings.append(Finding(
|
|
223
|
+
"does-not-own-section", "error", 1,
|
|
224
|
+
"Missing required section: ## Does Not Own"
|
|
225
|
+
))
|
|
226
|
+
|
|
227
|
+
# Separator before functions
|
|
228
|
+
if doc.functions and not doc.separators:
|
|
229
|
+
findings.append(Finding(
|
|
230
|
+
"separator-before-functions", "error", doc.functions[0].line,
|
|
231
|
+
"Function entries found but no --- separator before them"
|
|
232
|
+
))
|
|
233
|
+
|
|
234
|
+
# Conditional sections (required if functions exist)
|
|
235
|
+
if doc.functions:
|
|
236
|
+
has_before_changing = any(
|
|
237
|
+
s.startswith("Before Changing") for s in doc.sections
|
|
238
|
+
)
|
|
239
|
+
has_connections = "Connections" in doc.sections
|
|
240
|
+
|
|
241
|
+
if not has_before_changing:
|
|
242
|
+
findings.append(Finding(
|
|
243
|
+
"before-changing-section", "warn", 1,
|
|
244
|
+
"Missing '## Before Changing This Module' — recommended when function entries exist"
|
|
245
|
+
))
|
|
246
|
+
|
|
247
|
+
if not has_connections:
|
|
248
|
+
findings.append(Finding(
|
|
249
|
+
"connections-section", "warn", 1,
|
|
250
|
+
"Missing '## Connections' — recommended when function entries exist"
|
|
251
|
+
))
|
|
252
|
+
|
|
253
|
+
# Function entry validation
|
|
254
|
+
# Count how many covered files there are (multi-file charters need File: lines more)
|
|
255
|
+
multi_file = doc.has_covers and ("," in _get_covers_text(doc) or "`.gator" not in _get_covers_text(doc) and "`gator" not in _get_covers_text(doc))
|
|
256
|
+
|
|
257
|
+
for func in doc.functions:
|
|
258
|
+
if not func.has_file_line:
|
|
259
|
+
# Error for multi-file charters (ambiguous which file), warn for single-file
|
|
260
|
+
severity = "warn"
|
|
261
|
+
findings.append(Finding(
|
|
262
|
+
"file-line", severity, func.line,
|
|
263
|
+
f"Function entry '{func.name}' missing 'File:' line"
|
|
264
|
+
))
|
|
265
|
+
|
|
266
|
+
if func.has_file_line and not func.has_description:
|
|
267
|
+
findings.append(Finding(
|
|
268
|
+
"description-line", "warn", func.line,
|
|
269
|
+
f"Function entry '{func.name}' has no description after File: line"
|
|
270
|
+
))
|
|
271
|
+
|
|
272
|
+
if not func.has_annotations:
|
|
273
|
+
findings.append(Finding(
|
|
274
|
+
"no-annotations", "info", func.line,
|
|
275
|
+
f"Function entry '{func.name}' has no caller/callee/tripwire annotations"
|
|
276
|
+
))
|
|
277
|
+
|
|
278
|
+
return findings
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _validate_index(doc):
|
|
282
|
+
"""Validate an INDEX.md file."""
|
|
283
|
+
findings = []
|
|
284
|
+
|
|
285
|
+
if not doc.title.startswith("# Charter Index"):
|
|
286
|
+
findings.append(Finding(
|
|
287
|
+
"index-title", "error", doc.title_line or 1,
|
|
288
|
+
f"INDEX.md should start with '# Charter Index', got: '{doc.title}'"
|
|
289
|
+
))
|
|
290
|
+
|
|
291
|
+
if not doc.has_dispatch_table:
|
|
292
|
+
findings.append(Finding(
|
|
293
|
+
"dispatch-table", "warn", 1,
|
|
294
|
+
"INDEX.md should contain a dispatch table mapping code paths to charters"
|
|
295
|
+
))
|
|
296
|
+
|
|
297
|
+
return findings
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
# ---------------------------------------------------------------------------
|
|
301
|
+
# Report
|
|
302
|
+
# ---------------------------------------------------------------------------
|
|
303
|
+
|
|
304
|
+
def print_report(results):
|
|
305
|
+
"""Print human-readable report."""
|
|
306
|
+
total_errors = 0
|
|
307
|
+
total_warnings = 0
|
|
308
|
+
total_info = 0
|
|
309
|
+
|
|
310
|
+
for path, findings in results.items():
|
|
311
|
+
errors = [f for f in findings if f.severity == "error"]
|
|
312
|
+
warnings = [f for f in findings if f.severity == "warn"]
|
|
313
|
+
infos = [f for f in findings if f.severity == "info"]
|
|
314
|
+
|
|
315
|
+
total_errors += len(errors)
|
|
316
|
+
total_warnings += len(warnings)
|
|
317
|
+
total_info += len(infos)
|
|
318
|
+
|
|
319
|
+
if not findings:
|
|
320
|
+
print(f" ✓ {Path(path).name}")
|
|
321
|
+
continue
|
|
322
|
+
|
|
323
|
+
status = "✗" if errors else "⚠" if warnings else "·"
|
|
324
|
+
print(f" {status} {Path(path).name}")
|
|
325
|
+
|
|
326
|
+
for f in findings:
|
|
327
|
+
icon = {"error": "✗", "warn": "⚠", "info": "·"}.get(f.severity, " ")
|
|
328
|
+
print(f" {icon} L{f.line}: [{f.check}] {f.message}")
|
|
329
|
+
|
|
330
|
+
print()
|
|
331
|
+
print(f" {len(results)} files checked: "
|
|
332
|
+
f"{total_errors} errors, {total_warnings} warnings, {total_info} info")
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
def print_json_report(results):
|
|
336
|
+
"""Print JSON report."""
|
|
337
|
+
output = {}
|
|
338
|
+
for path, findings in results.items():
|
|
339
|
+
output[path] = [asdict(f) for f in findings]
|
|
340
|
+
print(json.dumps(output, indent=2))
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
# ---------------------------------------------------------------------------
|
|
344
|
+
# CLI
|
|
345
|
+
# ---------------------------------------------------------------------------
|
|
346
|
+
|
|
347
|
+
def find_charter_dirs():
|
|
348
|
+
"""Find charter directories from the current working directory."""
|
|
349
|
+
dirs = []
|
|
350
|
+
cwd = Path.cwd()
|
|
351
|
+
|
|
352
|
+
# Check .gator/charters/ (per-repo governance)
|
|
353
|
+
gator_charters = cwd / ".gator" / "charters"
|
|
354
|
+
if gator_charters.is_dir():
|
|
355
|
+
dirs.append(gator_charters)
|
|
356
|
+
|
|
357
|
+
# Check gator-command/charters/ (command-post product charters)
|
|
358
|
+
gc_charters = cwd / "gator-command" / "charters"
|
|
359
|
+
if gc_charters.is_dir():
|
|
360
|
+
dirs.append(gc_charters)
|
|
361
|
+
|
|
362
|
+
return dirs
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def collect_files(charter_path=None, charters_dir=None):
|
|
366
|
+
"""Collect charter files to validate."""
|
|
367
|
+
files = []
|
|
368
|
+
|
|
369
|
+
if charter_path:
|
|
370
|
+
files.append(Path(charter_path))
|
|
371
|
+
elif charters_dir:
|
|
372
|
+
d = Path(charters_dir)
|
|
373
|
+
if d.is_dir():
|
|
374
|
+
files.extend(sorted(f for f in d.glob("*.md") if f.name not in SKIP_FILES))
|
|
375
|
+
else:
|
|
376
|
+
for d in find_charter_dirs():
|
|
377
|
+
files.extend(sorted(f for f in d.glob("*.md") if f.name not in SKIP_FILES))
|
|
378
|
+
|
|
379
|
+
return files
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def main():
|
|
383
|
+
ensure_utf8_stdout()
|
|
384
|
+
|
|
385
|
+
parser = argparse.ArgumentParser(
|
|
386
|
+
description="Validate charter files against Charter Schema v1."
|
|
387
|
+
)
|
|
388
|
+
parser.add_argument("--json", "-j", action="store_true", help="JSON output")
|
|
389
|
+
parser.add_argument("--charter", metavar="PATH", help="Validate a single charter file")
|
|
390
|
+
parser.add_argument("--charters-dir", metavar="DIR", help="Validate all charters in directory")
|
|
391
|
+
|
|
392
|
+
args = parser.parse_args()
|
|
393
|
+
|
|
394
|
+
files = collect_files(charter_path=args.charter, charters_dir=args.charters_dir)
|
|
395
|
+
|
|
396
|
+
if not files:
|
|
397
|
+
print(" No charter files found.")
|
|
398
|
+
return
|
|
399
|
+
|
|
400
|
+
print()
|
|
401
|
+
print(f" charter-lint: checking {len(files)} files")
|
|
402
|
+
print()
|
|
403
|
+
|
|
404
|
+
results = {}
|
|
405
|
+
has_errors = False
|
|
406
|
+
|
|
407
|
+
for f in files:
|
|
408
|
+
try:
|
|
409
|
+
doc = parse_charter(f)
|
|
410
|
+
findings = validate_charter(doc)
|
|
411
|
+
results[str(f)] = findings
|
|
412
|
+
if any(fd.severity == "error" for fd in findings):
|
|
413
|
+
has_errors = True
|
|
414
|
+
except Exception as e:
|
|
415
|
+
results[str(f)] = [Finding("parse-error", "error", 0, str(e))]
|
|
416
|
+
has_errors = True
|
|
417
|
+
|
|
418
|
+
if args.json:
|
|
419
|
+
print_json_report(results)
|
|
420
|
+
else:
|
|
421
|
+
print_report(results)
|
|
422
|
+
|
|
423
|
+
sys.exit(1 if has_errors else 0)
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
if __name__ == "__main__":
|
|
427
|
+
main()
|