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,286 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
memex-lint.py — Mechanical invariant checker for the .gator/ knowledge layer.
|
|
4
|
+
|
|
5
|
+
Read-only. Reports findings, changes nothing. Cross-platform (no bash).
|
|
6
|
+
|
|
7
|
+
Checks:
|
|
8
|
+
1. Budget — always-loaded files under 400 lines total
|
|
9
|
+
2. Thread size — active threads over 60 lines flagged for split
|
|
10
|
+
3. Frontmatter — required fields (last-touched, category, hits)
|
|
11
|
+
4. Summary — every thread has ## Summary
|
|
12
|
+
5. Cross-references — markdown links resolve to existing files
|
|
13
|
+
6. Orphans — threads with no inbound references
|
|
14
|
+
7. Active thread count — target 5-8
|
|
15
|
+
|
|
16
|
+
Usage:
|
|
17
|
+
python memex-lint.py [--path /repo] [--json]
|
|
18
|
+
|
|
19
|
+
@reads: .gator/ knowledge layer (or gator-command/ for source repo)
|
|
20
|
+
@writes: stdout only
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json as json_mod
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from datetime import datetime
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
31
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
32
|
+
from gator_core import find_gator_root, ensure_utf8_stdout # noqa: E402
|
|
33
|
+
|
|
34
|
+
BUDGET = 400
|
|
35
|
+
SPLIT_THRESHOLD = 60
|
|
36
|
+
ACTIVE_TARGET = (3, 8)
|
|
37
|
+
SKIP_FILES = {"_TEMPLATE.md", "_template.md", "README.md"}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# ── output helpers ─────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
errors = 0
|
|
43
|
+
warnings = 0
|
|
44
|
+
findings = []
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def error(msg):
|
|
48
|
+
global errors
|
|
49
|
+
errors += 1
|
|
50
|
+
findings.append({"level": "error", "message": msg})
|
|
51
|
+
print(f" ERROR: {msg}")
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def warn(msg):
|
|
55
|
+
global warnings
|
|
56
|
+
warnings += 1
|
|
57
|
+
findings.append({"level": "warn", "message": msg})
|
|
58
|
+
print(f" WARN: {msg}")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def ok(msg):
|
|
62
|
+
findings.append({"level": "ok", "message": msg})
|
|
63
|
+
print(f" OK: {msg}")
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# ── checks ─────────────────────────────────────────────────────────────────
|
|
67
|
+
|
|
68
|
+
def count_lines(path):
|
|
69
|
+
try:
|
|
70
|
+
return len(path.read_text(encoding="utf-8", errors="replace").splitlines())
|
|
71
|
+
except OSError:
|
|
72
|
+
return 0
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def get_threads(memex_dir):
|
|
76
|
+
"""Collect all thread files from active-threads/ and threads/."""
|
|
77
|
+
threads = []
|
|
78
|
+
for subdir in ("active-threads", "threads"):
|
|
79
|
+
d = memex_dir / subdir
|
|
80
|
+
if d.is_dir():
|
|
81
|
+
for f in sorted(d.glob("*.md")):
|
|
82
|
+
if f.name not in SKIP_FILES:
|
|
83
|
+
threads.append(f)
|
|
84
|
+
return threads
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def check_budget(memex_dir):
|
|
88
|
+
"""Check 1: always-loaded files under budget."""
|
|
89
|
+
print("-- Budget Check --")
|
|
90
|
+
budget_files = []
|
|
91
|
+
|
|
92
|
+
for name in ("identity.md", "inbox.md"):
|
|
93
|
+
f = memex_dir / name
|
|
94
|
+
if f.exists():
|
|
95
|
+
budget_files.append(f)
|
|
96
|
+
|
|
97
|
+
at_dir = memex_dir / "active-threads"
|
|
98
|
+
if at_dir.is_dir():
|
|
99
|
+
for f in sorted(at_dir.glob("*.md")):
|
|
100
|
+
if f.name not in SKIP_FILES:
|
|
101
|
+
budget_files.append(f)
|
|
102
|
+
|
|
103
|
+
patterns_dir = memex_dir / "patterns"
|
|
104
|
+
if patterns_dir.is_dir():
|
|
105
|
+
for f in sorted(patterns_dir.glob("*.md")):
|
|
106
|
+
if f.name not in SKIP_FILES:
|
|
107
|
+
budget_files.append(f)
|
|
108
|
+
elif (memex_dir / "patterns.md").exists():
|
|
109
|
+
budget_files.append(memex_dir / "patterns.md")
|
|
110
|
+
|
|
111
|
+
total = sum(count_lines(f) for f in budget_files)
|
|
112
|
+
if total > BUDGET:
|
|
113
|
+
error(f"Always-loaded budget: {total} / {BUDGET} lines (OVER by {total - BUDGET})")
|
|
114
|
+
else:
|
|
115
|
+
ok(f"Always-loaded budget: {total} / {BUDGET} lines ({BUDGET - total} headroom)")
|
|
116
|
+
print()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def check_thread_size(memex_dir):
|
|
120
|
+
"""Check 2: active threads over 60 lines."""
|
|
121
|
+
print("-- Thread Size Check (active threads > 60 lines) --")
|
|
122
|
+
at_dir = memex_dir / "active-threads"
|
|
123
|
+
if not at_dir.is_dir():
|
|
124
|
+
ok("No active-threads/ directory")
|
|
125
|
+
print()
|
|
126
|
+
return
|
|
127
|
+
|
|
128
|
+
for f in sorted(at_dir.glob("*.md")):
|
|
129
|
+
if f.name in SKIP_FILES:
|
|
130
|
+
continue
|
|
131
|
+
lines = count_lines(f)
|
|
132
|
+
if lines > SPLIT_THRESHOLD:
|
|
133
|
+
warn(f"{f.name}: {lines} lines -- evaluate for split")
|
|
134
|
+
else:
|
|
135
|
+
ok(f"{f.name}: {lines} lines")
|
|
136
|
+
print()
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def check_frontmatter(threads):
|
|
140
|
+
"""Check 3: required frontmatter fields."""
|
|
141
|
+
print("-- Frontmatter Check --")
|
|
142
|
+
required = ["last-touched"] # category, hits, tags are optional in modern Gator
|
|
143
|
+
|
|
144
|
+
for f in threads:
|
|
145
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
146
|
+
for field in required:
|
|
147
|
+
if f"\n{field}:" not in text and not text.startswith(f"{field}:"):
|
|
148
|
+
error(f"{f.name}: missing {field}")
|
|
149
|
+
|
|
150
|
+
ok("Frontmatter check complete")
|
|
151
|
+
print()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def check_summaries(threads):
|
|
155
|
+
"""Check 4: every thread has ## Summary."""
|
|
156
|
+
print("-- Summary Check --")
|
|
157
|
+
for f in threads:
|
|
158
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
159
|
+
if "## Summary" not in text:
|
|
160
|
+
error(f"{f.name}: missing ## Summary section")
|
|
161
|
+
|
|
162
|
+
ok("Summary check complete")
|
|
163
|
+
print()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_crossrefs(threads, memex_dir):
|
|
167
|
+
"""Check 5: markdown links resolve."""
|
|
168
|
+
print("-- Cross-Reference Integrity --")
|
|
169
|
+
link_pattern = re.compile(r'\[([^\]]+)\]\(([^)]+\.md)\)')
|
|
170
|
+
|
|
171
|
+
for f in threads:
|
|
172
|
+
text = f.read_text(encoding="utf-8", errors="replace")
|
|
173
|
+
for match in link_pattern.finditer(text):
|
|
174
|
+
link_path = match.group(2)
|
|
175
|
+
if link_path.startswith("http"):
|
|
176
|
+
continue
|
|
177
|
+
resolved = (f.parent / link_path).resolve()
|
|
178
|
+
if not resolved.is_file():
|
|
179
|
+
error(f"{f.name}: broken link -> {link_path}")
|
|
180
|
+
|
|
181
|
+
ok("Cross-reference check complete")
|
|
182
|
+
print()
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def check_orphans(threads, memex_dir):
|
|
186
|
+
"""Check 6: threads with no inbound references."""
|
|
187
|
+
print("-- Orphan Check (threads with no inbound references) --")
|
|
188
|
+
|
|
189
|
+
# Build reference map
|
|
190
|
+
all_text = {}
|
|
191
|
+
for f in threads:
|
|
192
|
+
all_text[f] = f.read_text(encoding="utf-8", errors="replace")
|
|
193
|
+
|
|
194
|
+
for f in threads:
|
|
195
|
+
name = f.name
|
|
196
|
+
inbound = sum(1 for other, text in all_text.items()
|
|
197
|
+
if other != f and name in text)
|
|
198
|
+
if inbound == 0:
|
|
199
|
+
warn(f"{name}: no inbound references (potential orphan)")
|
|
200
|
+
|
|
201
|
+
print()
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def check_active_count(memex_dir):
|
|
205
|
+
"""Check 7: active thread count in target range."""
|
|
206
|
+
print("-- Active Thread Count --")
|
|
207
|
+
at_dir = memex_dir / "active-threads"
|
|
208
|
+
if not at_dir.is_dir():
|
|
209
|
+
ok("No active-threads/ directory")
|
|
210
|
+
print()
|
|
211
|
+
return
|
|
212
|
+
|
|
213
|
+
count = sum(1 for f in at_dir.glob("*.md") if f.name not in SKIP_FILES)
|
|
214
|
+
lo, hi = ACTIVE_TARGET
|
|
215
|
+
if count > hi:
|
|
216
|
+
error(f"Active threads: {count} (max {hi})")
|
|
217
|
+
elif count < lo:
|
|
218
|
+
warn(f"Active threads: {count} (unusually low)")
|
|
219
|
+
else:
|
|
220
|
+
ok(f"Active threads: {count} (target: {lo}-{hi})")
|
|
221
|
+
print()
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
# ── main ───────────────────────────────────────────────────────────────────
|
|
225
|
+
|
|
226
|
+
def main():
|
|
227
|
+
ensure_utf8_stdout()
|
|
228
|
+
|
|
229
|
+
parser = argparse.ArgumentParser(description="Gator knowledge layer lint")
|
|
230
|
+
parser.add_argument("--path", "-p", help="Path to repo (default: cwd)")
|
|
231
|
+
parser.add_argument("--json", "-j", action="store_true", help="JSON output")
|
|
232
|
+
args = parser.parse_args()
|
|
233
|
+
|
|
234
|
+
repo_root = find_gator_root(args.path)
|
|
235
|
+
if not repo_root:
|
|
236
|
+
print("Error: no .gator/ found.", file=sys.stderr)
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
# Detect knowledge layer location
|
|
240
|
+
# gator-command source repo uses gator-command/, fleet repos use .gator/
|
|
241
|
+
gc_dir = repo_root / "gator-command"
|
|
242
|
+
gator_dir = repo_root / ".gator"
|
|
243
|
+
if gc_dir.is_dir() and (gc_dir / "active-threads").is_dir():
|
|
244
|
+
memex_dir = gc_dir
|
|
245
|
+
else:
|
|
246
|
+
memex_dir = gator_dir
|
|
247
|
+
|
|
248
|
+
today = datetime.now().strftime("%Y-%m-%d")
|
|
249
|
+
print(f"==============================")
|
|
250
|
+
print(f" Gator Lint -- {today}")
|
|
251
|
+
print(f" {memex_dir}")
|
|
252
|
+
print(f"==============================")
|
|
253
|
+
print()
|
|
254
|
+
|
|
255
|
+
threads = get_threads(memex_dir)
|
|
256
|
+
|
|
257
|
+
check_budget(memex_dir)
|
|
258
|
+
check_thread_size(memex_dir)
|
|
259
|
+
check_frontmatter(threads)
|
|
260
|
+
check_summaries(threads)
|
|
261
|
+
check_crossrefs(threads, memex_dir)
|
|
262
|
+
check_orphans(threads, memex_dir)
|
|
263
|
+
check_active_count(memex_dir)
|
|
264
|
+
|
|
265
|
+
print("==============================")
|
|
266
|
+
if errors == 0 and warnings == 0:
|
|
267
|
+
print("All checks passed.")
|
|
268
|
+
elif errors == 0:
|
|
269
|
+
print(f"{warnings} warning(s), 0 errors.")
|
|
270
|
+
else:
|
|
271
|
+
print(f"{errors} error(s), {warnings} warning(s).")
|
|
272
|
+
print("==============================")
|
|
273
|
+
|
|
274
|
+
if args.json:
|
|
275
|
+
print()
|
|
276
|
+
print(json_mod.dumps({
|
|
277
|
+
"errors": errors,
|
|
278
|
+
"warnings": warnings,
|
|
279
|
+
"findings": findings,
|
|
280
|
+
}, indent=2))
|
|
281
|
+
|
|
282
|
+
sys.exit(errors)
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
if __name__ == "__main__":
|
|
286
|
+
main()
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# memex-lint.sh — Mechanical invariant checker for the Memex
|
|
3
|
+
# Read-only. Reports findings, changes nothing.
|
|
4
|
+
# Usage: bash scripts/memex-lint.sh
|
|
5
|
+
|
|
6
|
+
MEMEX_DIR="$(cd "$(dirname "$0")/.." && pwd)"
|
|
7
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
8
|
+
ERRORS=0
|
|
9
|
+
WARNINGS=0
|
|
10
|
+
|
|
11
|
+
RED='\033[0;31m'
|
|
12
|
+
YELLOW='\033[0;33m'
|
|
13
|
+
GREEN='\033[0;32m'
|
|
14
|
+
NC='\033[0m'
|
|
15
|
+
|
|
16
|
+
error() { echo -e "${RED}ERROR:${NC} $1"; ((ERRORS++)); }
|
|
17
|
+
warn() { echo -e "${YELLOW}WARN:${NC} $1"; ((WARNINGS++)); }
|
|
18
|
+
ok() { echo -e "${GREEN}OK:${NC} $1"; }
|
|
19
|
+
|
|
20
|
+
echo "=============================="
|
|
21
|
+
echo " Memex Lint — $(date +%Y-%m-%d)"
|
|
22
|
+
echo "=============================="
|
|
23
|
+
echo ""
|
|
24
|
+
|
|
25
|
+
# ── 1. Budget Check ──────────────────────────────────────────────
|
|
26
|
+
echo "── Budget Check ──"
|
|
27
|
+
|
|
28
|
+
# Budget includes: identity.md + active-threads (excluding _TEMPLATE.md) + patterns/*.md (excluding README.md) + inbox.md
|
|
29
|
+
BUDGET_FILES=()
|
|
30
|
+
BUDGET_FILES+=("$MEMEX_DIR/identity.md")
|
|
31
|
+
BUDGET_FILES+=("$MEMEX_DIR/inbox.md")
|
|
32
|
+
|
|
33
|
+
for f in "$MEMEX_DIR"/active-threads/*.md; do
|
|
34
|
+
[[ "$(basename "$f")" == "_TEMPLATE.md" ]] && continue
|
|
35
|
+
BUDGET_FILES+=("$f")
|
|
36
|
+
done
|
|
37
|
+
|
|
38
|
+
for f in "$MEMEX_DIR"/patterns/*.md; do
|
|
39
|
+
[[ "$(basename "$f")" == "README.md" ]] && continue
|
|
40
|
+
BUDGET_FILES+=("$f")
|
|
41
|
+
done
|
|
42
|
+
|
|
43
|
+
TOTAL_LINES=0
|
|
44
|
+
for f in "${BUDGET_FILES[@]}"; do
|
|
45
|
+
lines=$(wc -l < "$f" 2>/dev/null || echo 0)
|
|
46
|
+
TOTAL_LINES=$((TOTAL_LINES + lines))
|
|
47
|
+
done
|
|
48
|
+
|
|
49
|
+
BUDGET=400
|
|
50
|
+
if [ "$TOTAL_LINES" -gt "$BUDGET" ]; then
|
|
51
|
+
error "Always-loaded budget: $TOTAL_LINES / $BUDGET lines (OVER by $((TOTAL_LINES - BUDGET)))"
|
|
52
|
+
else
|
|
53
|
+
ok "Always-loaded budget: $TOTAL_LINES / $BUDGET lines ($((BUDGET - TOTAL_LINES)) headroom)"
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# Constitution line count (informational, not budgeted)
|
|
57
|
+
CONST_LINES=$(wc -l < "$REPO_ROOT/constitution.md" 2>/dev/null || echo 0)
|
|
58
|
+
echo " Constitution: $CONST_LINES lines (budget-exempt)"
|
|
59
|
+
echo ""
|
|
60
|
+
|
|
61
|
+
# ── 2. Thread Size Check ─────────────────────────────────────────
|
|
62
|
+
echo "── Thread Size Check (active threads > 60 lines) ──"
|
|
63
|
+
|
|
64
|
+
SPLIT_THRESHOLD=60
|
|
65
|
+
for f in "$MEMEX_DIR"/active-threads/*.md; do
|
|
66
|
+
[[ "$(basename "$f")" == "_TEMPLATE.md" ]] && continue
|
|
67
|
+
lines=$(wc -l < "$f")
|
|
68
|
+
name=$(basename "$f")
|
|
69
|
+
if [ "$lines" -gt "$SPLIT_THRESHOLD" ]; then
|
|
70
|
+
warn "$name: $lines lines — evaluate for split or compression"
|
|
71
|
+
else
|
|
72
|
+
ok "$name: $lines lines"
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
echo ""
|
|
76
|
+
|
|
77
|
+
# ── 3. Frontmatter Check ─────────────────────────────────────────
|
|
78
|
+
echo "── Frontmatter Check ──"
|
|
79
|
+
|
|
80
|
+
ALL_THREADS=()
|
|
81
|
+
for f in "$MEMEX_DIR"/active-threads/*.md "$MEMEX_DIR"/threads/*.md; do
|
|
82
|
+
[[ "$(basename "$f")" == "_TEMPLATE.md" ]] && continue
|
|
83
|
+
[ -f "$f" ] && ALL_THREADS+=("$f")
|
|
84
|
+
done
|
|
85
|
+
|
|
86
|
+
for f in "${ALL_THREADS[@]}"; do
|
|
87
|
+
name=$(basename "$f")
|
|
88
|
+
|
|
89
|
+
if ! grep -q "^last-touched:" "$f"; then
|
|
90
|
+
error "$name: missing last-touched"
|
|
91
|
+
fi
|
|
92
|
+
if ! grep -q "^category:" "$f"; then
|
|
93
|
+
error "$name: missing category"
|
|
94
|
+
fi
|
|
95
|
+
if ! grep -q "^hits:" "$f"; then
|
|
96
|
+
error "$name: missing hits"
|
|
97
|
+
fi
|
|
98
|
+
if ! grep -q "^tags:" "$f"; then
|
|
99
|
+
error "$name: missing tags"
|
|
100
|
+
fi
|
|
101
|
+
done
|
|
102
|
+
|
|
103
|
+
# Check for valid categories
|
|
104
|
+
VALID_CATEGORIES="mathematics|cognition|systems|ventures|economics|civic"
|
|
105
|
+
for f in "${ALL_THREADS[@]}"; do
|
|
106
|
+
name=$(basename "$f")
|
|
107
|
+
cat_line=$(grep "^category:" "$f" 2>/dev/null || echo "")
|
|
108
|
+
if [ -n "$cat_line" ]; then
|
|
109
|
+
cat_value=$(echo "$cat_line" | sed 's/^category: *//')
|
|
110
|
+
if ! echo "$cat_value" | grep -qE "^($VALID_CATEGORIES)$"; then
|
|
111
|
+
error "$name: invalid category '$cat_value'"
|
|
112
|
+
fi
|
|
113
|
+
fi
|
|
114
|
+
done
|
|
115
|
+
|
|
116
|
+
ok "Frontmatter check complete"
|
|
117
|
+
echo ""
|
|
118
|
+
|
|
119
|
+
# ── 4. Summary Check ─────────────────────────────────────────────
|
|
120
|
+
echo "── Summary Check ──"
|
|
121
|
+
|
|
122
|
+
for f in "${ALL_THREADS[@]}"; do
|
|
123
|
+
name=$(basename "$f")
|
|
124
|
+
if ! grep -q "^## Summary" "$f"; then
|
|
125
|
+
error "$name: missing ## Summary section"
|
|
126
|
+
fi
|
|
127
|
+
done
|
|
128
|
+
|
|
129
|
+
ok "Summary check complete"
|
|
130
|
+
echo ""
|
|
131
|
+
|
|
132
|
+
# ── 5. Cross-Reference Integrity ─────────────────────────────────
|
|
133
|
+
echo "── Cross-Reference Integrity ──"
|
|
134
|
+
|
|
135
|
+
for f in "${ALL_THREADS[@]}"; do
|
|
136
|
+
name=$(basename "$f")
|
|
137
|
+
dir=$(dirname "$f")
|
|
138
|
+
|
|
139
|
+
# Extract markdown links: [text](path.md)
|
|
140
|
+
grep -oE '\[([^]]+)\]\(([^)]+\.md)\)' "$f" 2>/dev/null | while read -r match; do
|
|
141
|
+
link_path=$(echo "$match" | grep -oE '\(([^)]+)\)' | tr -d '()')
|
|
142
|
+
|
|
143
|
+
# Skip external URLs
|
|
144
|
+
[[ "$link_path" == http* ]] && continue
|
|
145
|
+
|
|
146
|
+
# Resolve relative path
|
|
147
|
+
resolved="$dir/$link_path"
|
|
148
|
+
resolved=$(cd "$dir" 2>/dev/null && realpath -m "$link_path" 2>/dev/null || echo "$resolved")
|
|
149
|
+
|
|
150
|
+
if [ ! -f "$resolved" ]; then
|
|
151
|
+
error "$name: broken link → $link_path"
|
|
152
|
+
fi
|
|
153
|
+
done
|
|
154
|
+
done
|
|
155
|
+
|
|
156
|
+
ok "Cross-reference check complete"
|
|
157
|
+
echo ""
|
|
158
|
+
|
|
159
|
+
# ── 6. Orphan Check ──────────────────────────────────────────────
|
|
160
|
+
echo "── Orphan Check (threads with no inbound references) ──"
|
|
161
|
+
|
|
162
|
+
for f in "${ALL_THREADS[@]}"; do
|
|
163
|
+
name=$(basename "$f")
|
|
164
|
+
name_no_ext="${name%.md}"
|
|
165
|
+
|
|
166
|
+
# Search all threads for references to this file
|
|
167
|
+
inbound=$(grep -rl "$name" "$MEMEX_DIR"/active-threads/ "$MEMEX_DIR"/threads/ 2>/dev/null | grep -v "$f" | grep -v "_TEMPLATE.md" | wc -l)
|
|
168
|
+
|
|
169
|
+
if [ "$inbound" -eq 0 ]; then
|
|
170
|
+
warn "$name: no inbound references (potential orphan)"
|
|
171
|
+
fi
|
|
172
|
+
done
|
|
173
|
+
|
|
174
|
+
echo ""
|
|
175
|
+
|
|
176
|
+
# ── 7. Active Thread Count ───────────────────────────────────────
|
|
177
|
+
echo "── Active Thread Count ──"
|
|
178
|
+
|
|
179
|
+
ACTIVE_COUNT=0
|
|
180
|
+
for f in "$MEMEX_DIR"/active-threads/*.md; do
|
|
181
|
+
[[ "$(basename "$f")" == "_TEMPLATE.md" ]] && continue
|
|
182
|
+
((ACTIVE_COUNT++))
|
|
183
|
+
done
|
|
184
|
+
|
|
185
|
+
if [ "$ACTIVE_COUNT" -gt 8 ]; then
|
|
186
|
+
error "Active threads: $ACTIVE_COUNT (max 8)"
|
|
187
|
+
elif [ "$ACTIVE_COUNT" -lt 3 ]; then
|
|
188
|
+
warn "Active threads: $ACTIVE_COUNT (unusually low)"
|
|
189
|
+
else
|
|
190
|
+
ok "Active threads: $ACTIVE_COUNT (target: 5-8)"
|
|
191
|
+
fi
|
|
192
|
+
echo ""
|
|
193
|
+
|
|
194
|
+
# ── Summary ──────────────────────────────────────────────────────
|
|
195
|
+
echo "=============================="
|
|
196
|
+
if [ "$ERRORS" -eq 0 ] && [ "$WARNINGS" -eq 0 ]; then
|
|
197
|
+
echo -e "${GREEN}All checks passed.${NC}"
|
|
198
|
+
elif [ "$ERRORS" -eq 0 ]; then
|
|
199
|
+
echo -e "${YELLOW}$WARNINGS warning(s), 0 errors.${NC}"
|
|
200
|
+
else
|
|
201
|
+
echo -e "${RED}$ERRORS error(s), $WARNINGS warning(s).${NC}"
|
|
202
|
+
fi
|
|
203
|
+
echo "=============================="
|
|
204
|
+
|
|
205
|
+
exit $ERRORS
|