gator-command 1.2.0__tar.gz → 1.2.1__tar.gz

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 (136) hide show
  1. {gator_command-1.2.0/src/gator_command.egg-info → gator_command-1.2.1}/PKG-INFO +1 -1
  2. {gator_command-1.2.0 → gator_command-1.2.1}/pyproject.toml +1 -1
  3. gator_command-1.2.1/src/gator_command/scripts/gator-audit-renderers.py +361 -0
  4. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-audit.py +28 -348
  5. {gator_command-1.2.0 → gator_command-1.2.1/src/gator_command.egg-info}/PKG-INFO +1 -1
  6. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/SOURCES.txt +1 -0
  7. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_audit.py +180 -0
  8. {gator_command-1.2.0 → gator_command-1.2.1}/LICENSE +0 -0
  9. {gator_command-1.2.0 → gator_command-1.2.1}/PYPI_README.md +0 -0
  10. {gator_command-1.2.0 → gator_command-1.2.1}/README.md +0 -0
  11. {gator_command-1.2.0 → gator_command-1.2.1}/setup.cfg +0 -0
  12. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/__init__.py +0 -0
  13. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/cli.py +0 -0
  14. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/crawler.py +0 -0
  15. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.css +0 -0
  16. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.html +0 -0
  17. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/dashboard.js +0 -0
  18. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/audit.js +0 -0
  19. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/fleet.js +0 -0
  20. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/repo.js +0 -0
  21. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/settings.js +0 -0
  22. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/dashboard/views/updates.js +0 -0
  23. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/enforcer-prompt.md +0 -0
  24. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-claude-sessions.py +0 -0
  25. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-codex-sessions.py +0 -0
  26. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/extract-gemini-sessions.py +0 -0
  27. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-draft.py +0 -0
  28. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-lint.py +0 -0
  29. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-charter-verify.py +0 -0
  30. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-dashboard.py +0 -0
  31. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-deploy.py +0 -0
  32. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-drift.py +0 -0
  33. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-enforce.py +0 -0
  34. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-fleet-intel.py +0 -0
  35. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-fleet-report.py +0 -0
  36. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-init-command-post.py +0 -0
  37. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-init.py +0 -0
  38. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-machine-id.py +0 -0
  39. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-policy-status.py +0 -0
  40. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-pulse.py +0 -0
  41. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-repo-status.py +0 -0
  42. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-aggregator.py +0 -0
  43. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-common.py +0 -0
  44. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-session-sink.py +0 -0
  45. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-sessions.py +0 -0
  46. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-update.py +0 -0
  47. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator-version.py +0 -0
  48. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_core.py +0 -0
  49. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_remote.py +0 -0
  50. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gator_runtime.py +0 -0
  51. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-actions.sh +0 -0
  52. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-lib.sh +0 -0
  53. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize-post.sh +0 -0
  54. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize.py +0 -0
  55. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/gatorize.sh +0 -0
  56. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/generate_markdown.py +0 -0
  57. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/generate_wiki.py +0 -0
  58. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/graph_health.py +0 -0
  59. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex-lint.py +0 -0
  60. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex-lint.sh +0 -0
  61. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex.py +0 -0
  62. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex_formatters.py +0 -0
  63. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/memex_state.py +0 -0
  64. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/scripts/spawn.py +0 -0
  65. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/blueprints/README.md +0 -0
  66. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charterignore +0 -0
  67. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charters/README.md +0 -0
  68. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/charters/_template.md +0 -0
  69. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/commit.md +0 -0
  70. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/init.md +0 -0
  71. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/commands/update.md +0 -0
  72. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/constitution.md +0 -0
  73. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/field-guides/README.md +0 -0
  74. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/gator-start-up.md +0 -0
  75. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/charter-alignment.md +0 -0
  76. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/enforcer-review.md +0 -0
  77. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/field-guide-generation.md +0 -0
  78. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/knowledge-capture.md +0 -0
  79. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/procedures/significance-check.md +0 -0
  80. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/concierge-responses.md +0 -0
  81. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/dangerous-patterns.md +0 -0
  82. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/dashboard-operations.md +0 -0
  83. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/enforcer-configuration.md +0 -0
  84. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/example-project.md +0 -0
  85. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/failure-modes-and-self-correction.md +0 -0
  86. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/git-workflow.md +0 -0
  87. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/identity-and-ownership.md +0 -0
  88. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/refactor-approach.md +0 -0
  89. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/what-gator-requires-from-a-model.md +0 -0
  90. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/why-navigation-coding-feels-different.md +0 -0
  91. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/reference-notes/workflow-profiles.md +0 -0
  92. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/enforcer-review.cpython-313.pyc +0 -0
  93. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-approve.cpython-313.pyc +0 -0
  94. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-init.cpython-313.pyc +0 -0
  95. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-pre-commit.cpython-313.pyc +0 -0
  96. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/__pycache__/gator-update.cpython-313.pyc +0 -0
  97. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/enforcer-prompt.md +0 -0
  98. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/enforcer-review.py +0 -0
  99. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-approve.py +0 -0
  100. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-enforce.py +0 -0
  101. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-init.py +0 -0
  102. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-pre-commit.py +0 -0
  103. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-pulse.py +0 -0
  104. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-update.py +0 -0
  105. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator-version.py +0 -0
  106. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/gator_core.py +0 -0
  107. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/commit-msgcpython-313.pyc +0 -0
  108. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/post-commitcpython-313.pyc +0 -0
  109. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/__pycache__/pre-commitcpython-313.pyc +0 -0
  110. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/commit-msg +0 -0
  111. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/post-commit +0 -0
  112. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/scripts/hooks/pre-commit +0 -0
  113. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/sessions/.gitignore +0 -0
  114. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/vault/.gitkeep +0 -0
  115. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command/templates/gator-starter/whiteboard.md +0 -0
  116. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/dependency_links.txt +0 -0
  117. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/entry_points.txt +0 -0
  118. {gator_command-1.2.0 → gator_command-1.2.1}/src/gator_command.egg-info/top_level.txt +0 -0
  119. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_audit_integration.py +0 -0
  120. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_charter_lint.py +0 -0
  121. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_dashboard_sessions.py +0 -0
  122. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_deploy.py +0 -0
  123. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_drift.py +0 -0
  124. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_fleet_intel.py +0 -0
  125. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_fleet_report.py +0 -0
  126. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_gator_core.py +0 -0
  127. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_gatorize.py +0 -0
  128. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_hooks.py +0 -0
  129. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_init.py +0 -0
  130. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_memex_state.py +0 -0
  131. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_packaging.py +0 -0
  132. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_pre_commit.py +0 -0
  133. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_remote.py +0 -0
  134. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_session_aggregator.py +0 -0
  135. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_session_sink.py +0 -0
  136. {gator_command-1.2.0 → gator_command-1.2.1}/tests/test_sessions.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: gator-command
3
- Version: 1.2.0
3
+ Version: 1.2.1
4
4
  Summary: Git-native governance for AI-assisted engineering
5
5
  Author-email: Cumberland Laboratories <support@cumberlandlaboratories.com>
6
6
  License: MIT
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gator-command"
7
- version = "1.2.0"
7
+ version = "1.2.1"
8
8
  description = "Git-native governance for AI-assisted engineering"
9
9
  readme = "PYPI_README.md"
10
10
  license = {text = "MIT"}
@@ -0,0 +1,361 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ gator-audit-renderers.py — Output renderers for gator audit.
4
+
5
+ Pure rendering functions: take an assembled audit data dict, return a string.
6
+ No data collection, no side effects, no shared state.
7
+
8
+ Consumers:
9
+ from gator_audit_renderers import render_text, render_html
10
+
11
+ @reads: nothing (pure functions)
12
+ @writes: nothing (returns strings)
13
+ """
14
+
15
+
16
+ # ---------------------------------------------------------------------------
17
+ # Text Output
18
+ # ---------------------------------------------------------------------------
19
+
20
+ def render_text(data):
21
+ """Render audit data as terminal text."""
22
+ lines = []
23
+
24
+ lines.append("")
25
+ lines.append(f" gator audit")
26
+ lines.append(f" {data['generated_local']} | {data['version']} | {data.get('machine', {}).get('label', '?')}")
27
+ lines.append("")
28
+
29
+ # Build drift lookup for fleet indicator
30
+ drift_by_name = {}
31
+ for d in data.get("drift", []):
32
+ drift_by_name[d.get("name", "")] = d.get("severity", "ok")
33
+
34
+ # Fleet Status
35
+ fleet = data.get("fleet_status", [])
36
+ if fleet:
37
+ lines.append(f" FLEET STATUS ({len(fleet)} repos)")
38
+ lines.append("")
39
+ for r in fleet:
40
+ if not r.get("accessible"):
41
+ lines.append(f" ✗ {r.get('name', '?')} — NOT ACCESSIBLE")
42
+ continue
43
+
44
+ hooks = "✓" if r.get("has_hooks") else "✗"
45
+ trailers = "✓" if r.get("trailers") else "·"
46
+
47
+ # Indicator reflects governance health, not just issues
48
+ repo_drift = drift_by_name.get(r.get("name", ""), "ok")
49
+ if repo_drift == "drift":
50
+ indicator = "✗"
51
+ elif repo_drift == "warn" or not r.get("has_hooks"):
52
+ indicator = "⚠"
53
+ else:
54
+ indicator = "✓"
55
+
56
+ charters = r.get("charters", 0)
57
+ functions = r.get("functions", 0)
58
+
59
+ commit = r.get("last_commit")
60
+ commit_str = ""
61
+ if commit and not commit.get("error"):
62
+ commit_str = f" last: {commit['age']}"
63
+
64
+ lines.append(
65
+ f" {indicator} {r.get('name', '?'):<20}"
66
+ f" charters: {charters:>2} ({functions:>2} fn)"
67
+ f" hooks: {hooks} trailers: {trailers}"
68
+ f"{commit_str}"
69
+ )
70
+ lines.append("")
71
+
72
+ # Drift
73
+ drift_list = data.get("drift", [])
74
+ drift_sum = data.get("drift_summary", {})
75
+ if drift_list:
76
+ ok = drift_sum.get("ok", 0)
77
+ warn = drift_sum.get("warn", 0)
78
+ drifted = drift_sum.get("drift", 0)
79
+
80
+ # Surface command-post git failure once, not per-repo
81
+ cp_state = drift_sum.get("command_post", {})
82
+ if cp_state.get("git_failed"):
83
+ lines.append(f" DRIFT (command post git access failed — policy comparison skipped)")
84
+ else:
85
+ lines.append(f" DRIFT ({ok} current, {warn} warnings, {drifted} drifted)")
86
+ lines.append("")
87
+
88
+ seen_cp_warn = False
89
+ for r in drift_list:
90
+ if r.get("severity") == "ok":
91
+ continue
92
+ name = r.get("name", "?")
93
+ sev = "✗" if r["severity"] == "drift" else "⚠"
94
+ lines.append(f" {sev} {name}")
95
+ for f in r.get("findings", []):
96
+ # Deduplicate command-post git failure across repos
97
+ if f["check"] == "policy-version" and "git failed" in f.get("message", ""):
98
+ if not seen_cp_warn:
99
+ seen_cp_warn = True
100
+ marker = "⚠"
101
+ lines.append(f" {marker} {f['check']}: {f['message']}")
102
+ continue
103
+ marker = "✗" if f["severity"] == "drift" else "⚠"
104
+ lines.append(f" {marker} {f['check']}: {f['message']}")
105
+ if drifted == 0 and warn == 0:
106
+ lines.append(f" All repos current.")
107
+ lines.append("")
108
+
109
+ # Sessions
110
+ sessions = data.get("sessions", {})
111
+ if sessions and not sessions.get("error"):
112
+ since = sessions.get("since_days", 7)
113
+ recent = sessions.get("recent", 0)
114
+ total = sessions.get("total", 0)
115
+ lines.append(f" SESSIONS (last {since} days: {recent} of {total} total)")
116
+ lines.append("")
117
+
118
+ by_vendor = sessions.get("by_vendor", {})
119
+ if by_vendor:
120
+ vendor_parts = [f"{v}: {c}" for v, c in sorted(by_vendor.items())]
121
+ lines.append(f" Vendors: {' | '.join(vendor_parts)}")
122
+
123
+ by_repo = sessions.get("by_repo", {})
124
+ if by_repo:
125
+ repo_parts = [f"{r}({c})" for r, c in list(by_repo.items())[:6]]
126
+ lines.append(f" Repos: {', '.join(repo_parts)}")
127
+
128
+ pending = sessions.get("pending_export", 0)
129
+ exported = sessions.get("exported", 0)
130
+ lines.append(f" Export: {exported} exported, {pending} pending")
131
+ lines.append("")
132
+
133
+ # Governance Coverage
134
+ gov = data.get("governance", {})
135
+ if gov:
136
+ lines.append(f" GOVERNANCE COVERAGE")
137
+ lines.append("")
138
+ lines.append(f" Charters: {gov.get('charters', 0)} across {gov.get('repos', 0)} repos ({gov.get('functions', 0)} functions)")
139
+ lines.append(f" Hooks: {gov.get('hooks_installed', 0)}/{gov.get('repos', 0)} repos")
140
+ lines.append(f" Trailers: {gov.get('trailers_flowing', 0)}/{gov.get('repos', 0)} repos")
141
+ lines.append(f" Issues: {gov.get('issues', 0)} open")
142
+ lines.append("")
143
+
144
+ # Recent Decisions
145
+ decisions = data.get("decisions", [])
146
+ if decisions:
147
+ lines.append(f" RECENT DECISIONS ({len(decisions)})")
148
+ lines.append("")
149
+ for d in decisions[:10]:
150
+ ts = d.get("timestamp", "")[:10]
151
+ repo = d.get("repo", "")
152
+ repo_str = f" [{repo}]" if repo else ""
153
+ text = d.get("text", "")[:80]
154
+ lines.append(f" [{ts}]{repo_str} {text}")
155
+ lines.append("")
156
+
157
+ lines.append(f" Generated: {data['generated_local']} | Machine: {data.get('machine', {}).get('label', '?')}")
158
+ lines.append("")
159
+
160
+ return "\n".join(lines)
161
+
162
+
163
+ # ---------------------------------------------------------------------------
164
+ # HTML Output
165
+ # ---------------------------------------------------------------------------
166
+
167
+ def render_html(data):
168
+ """Render audit data as a self-contained HTML file.
169
+
170
+ Single file, inline CSS, no JavaScript dependencies, no external
171
+ resources. Opens in any browser. Can be saved as PDF, emailed,
172
+ attached to compliance reports. Air-gapped compatible.
173
+ """
174
+ fleet = data.get("fleet_status", [])
175
+ drift = data.get("drift", [])
176
+ drift_sum = data.get("drift_summary", {})
177
+ sessions = data.get("sessions", {})
178
+ gov = data.get("governance", {})
179
+ decisions = data.get("decisions", [])
180
+ machine = data.get("machine", {})
181
+
182
+ # Build drift lookup for fleet indicator
183
+ drift_by_name = {}
184
+ for d in drift:
185
+ drift_by_name[d.get("name", "")] = d.get("severity", "ok")
186
+
187
+ # Fleet rows
188
+ fleet_rows = ""
189
+ for r in fleet:
190
+ if not r.get("accessible"):
191
+ fleet_rows += f'<tr class="drift"><td>{r.get("name","?")}</td><td colspan="5">NOT ACCESSIBLE</td></tr>\n'
192
+ continue
193
+
194
+ repo_drift = drift_by_name.get(r.get("name", ""), "ok")
195
+ if repo_drift == "drift":
196
+ status_class = "drift"
197
+ elif repo_drift == "warn" or not r.get("has_hooks"):
198
+ status_class = "warn"
199
+ else:
200
+ status_class = "ok"
201
+ hooks = "&#10003;" if r.get("has_hooks") else "&#10007;"
202
+ hooks_class = "ok" if r.get("has_hooks") else "drift"
203
+ trailers = "&#10003;" if r.get("trailers") else "&middot;"
204
+ trailers_class = "ok" if r.get("trailers") else "muted"
205
+
206
+ commit = r.get("last_commit")
207
+ age = commit.get("age", "?") if commit and not commit.get("error") else "?"
208
+
209
+ fleet_rows += f'''<tr class="{status_class}">
210
+ <td><strong>{r.get("name","?")}</strong></td>
211
+ <td>{r.get("charters",0)} ({r.get("functions",0)} fn)</td>
212
+ <td class="{hooks_class}">{hooks}</td>
213
+ <td class="{trailers_class}">{trailers}</td>
214
+ <td>{r.get("issues",0)}</td>
215
+ <td>{age}</td>
216
+ </tr>\n'''
217
+
218
+ # Drift rows — deduplicate command-post git failure (same as text view)
219
+ drift_rows = ""
220
+ cp_state = drift_sum.get("command_post", {})
221
+ cp_git_failed = cp_state.get("git_failed", False)
222
+ seen_cp_warn = False
223
+
224
+ if cp_git_failed:
225
+ drift_rows += '<tr class="warn"><td colspan="3">Command post git access failed — policy comparison skipped for all repos</td></tr>\n'
226
+
227
+ for r in drift:
228
+ if r.get("severity") == "ok":
229
+ continue
230
+ for f in r.get("findings", []):
231
+ # Deduplicate command-post git failure
232
+ if f["check"] == "policy-version" and "git failed" in f.get("message", ""):
233
+ continue
234
+ sev_class = "drift" if f["severity"] == "drift" else "warn"
235
+ drift_rows += f'<tr class="{sev_class}"><td>{r.get("name","?")}</td><td>{f["check"]}</td><td>{f["message"]}</td></tr>\n'
236
+
237
+ if not drift_rows:
238
+ drift_rows = '<tr class="ok"><td colspan="3">All repos current. No drift detected.</td></tr>'
239
+
240
+ # Session stats
241
+ by_vendor_html = ""
242
+ for v, c in sorted(sessions.get("by_vendor", {}).items()):
243
+ by_vendor_html += f'<span class="tag">{v}: {c}</span> '
244
+
245
+ # Decision rows
246
+ decision_rows = ""
247
+ for d in decisions[:10]:
248
+ ts = d.get("timestamp", "")[:10]
249
+ repo = d.get("repo", "")
250
+ text = d.get("text", "")[:100]
251
+ decision_rows += f'<tr><td>{ts}</td><td>{repo}</td><td>{text}</td></tr>\n'
252
+
253
+ if not decision_rows:
254
+ decision_rows = '<tr><td colspan="3">No decisions extracted from recent sessions.</td></tr>'
255
+
256
+ ok_count = drift_sum.get("ok", 0)
257
+ warn_count = drift_sum.get("warn", 0)
258
+ drift_count = drift_sum.get("drift", 0)
259
+ since_days = sessions.get("since_days", 7)
260
+ recent_sessions = sessions.get("recent", 0)
261
+ total_sessions = sessions.get("total", 0)
262
+ pending = sessions.get("pending_export", 0)
263
+ exported = sessions.get("exported", 0)
264
+
265
+ html = f'''<!DOCTYPE html>
266
+ <html lang="en">
267
+ <head>
268
+ <meta charset="UTF-8">
269
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
270
+ <title>Gator Audit — {data["generated_local"]}</title>
271
+ <style>
272
+ * {{ margin: 0; padding: 0; box-sizing: border-box; }}
273
+ body {{ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, monospace;
274
+ background: #0d1117; color: #c9d1d9; padding: 24px; line-height: 1.5; }}
275
+ .container {{ max-width: 960px; margin: 0 auto; }}
276
+ h1 {{ color: #58a6ff; font-size: 1.4em; margin-bottom: 4px; }}
277
+ h2 {{ color: #8b949e; font-size: 1.1em; margin: 24px 0 12px 0;
278
+ border-bottom: 1px solid #21262d; padding-bottom: 6px; }}
279
+ .subtitle {{ color: #8b949e; font-size: 0.85em; margin-bottom: 20px; }}
280
+ table {{ width: 100%; border-collapse: collapse; margin: 8px 0 16px 0; font-size: 0.85em; }}
281
+ th {{ text-align: left; color: #8b949e; padding: 6px 10px; border-bottom: 1px solid #21262d; }}
282
+ td {{ padding: 6px 10px; border-bottom: 1px solid #161b22; }}
283
+ tr:hover {{ background: #161b22; }}
284
+ .ok {{ color: #3fb950; }}
285
+ .warn {{ color: #d29922; }}
286
+ .drift {{ color: #f85149; }}
287
+ .muted {{ color: #484f58; }}
288
+ .stats {{ display: flex; gap: 24px; flex-wrap: wrap; margin: 12px 0; }}
289
+ .stat {{ background: #161b22; border: 1px solid #21262d; border-radius: 6px;
290
+ padding: 12px 16px; min-width: 140px; }}
291
+ .stat-value {{ font-size: 1.6em; font-weight: bold; color: #58a6ff; }}
292
+ .stat-label {{ font-size: 0.8em; color: #8b949e; }}
293
+ .tag {{ background: #21262d; padding: 2px 8px; border-radius: 3px;
294
+ font-size: 0.8em; margin-right: 4px; }}
295
+ .footer {{ margin-top: 32px; padding-top: 12px; border-top: 1px solid #21262d;
296
+ color: #484f58; font-size: 0.75em; }}
297
+ @media print {{
298
+ body {{ background: white; color: #1f2328; }}
299
+ .stat {{ border-color: #d0d7de; }}
300
+ tr:hover {{ background: transparent; }}
301
+ }}
302
+ </style>
303
+ </head>
304
+ <body>
305
+ <div class="container">
306
+
307
+ <h1>Gator Audit Dashboard</h1>
308
+ <div class="subtitle">{data["generated_local"]} &middot; {data["version"]} &middot; {machine.get("label", "?")}</div>
309
+
310
+ <div class="stats">
311
+ <div class="stat"><div class="stat-value">{gov.get("repos", 0)}</div><div class="stat-label">Repos</div></div>
312
+ <div class="stat"><div class="stat-value">{gov.get("charters", 0)}</div><div class="stat-label">Charters</div></div>
313
+ <div class="stat"><div class="stat-value">{gov.get("functions", 0)}</div><div class="stat-label">Functions</div></div>
314
+ <div class="stat"><div class="stat-value {("ok" if drift_count == 0 else "drift")}">{drift_count}</div><div class="stat-label">Drifted</div></div>
315
+ <div class="stat"><div class="stat-value">{recent_sessions}</div><div class="stat-label">Sessions ({since_days}d)</div></div>
316
+ <div class="stat"><div class="stat-value">{gov.get("issues", 0)}</div><div class="stat-label">Issues</div></div>
317
+ </div>
318
+
319
+ <h2>Fleet Status</h2>
320
+ <table>
321
+ <tr><th>Repo</th><th>Charters</th><th>Hooks</th><th>Trailers</th><th>Issues</th><th>Last Commit</th></tr>
322
+ {fleet_rows}
323
+ </table>
324
+
325
+ <h2>Drift Findings</h2>
326
+ <div class="subtitle">{ok_count} current, {warn_count} warnings, {drift_count} drifted</div>
327
+ <table>
328
+ <tr><th>Repo</th><th>Check</th><th>Finding</th></tr>
329
+ {drift_rows}
330
+ </table>
331
+
332
+ <h2>Sessions (last {since_days} days)</h2>
333
+ <div class="subtitle">{recent_sessions} of {total_sessions} total &middot; {exported} exported, {pending} pending</div>
334
+ <div style="margin: 8px 0;">{by_vendor_html}</div>
335
+
336
+ <h2>Recent Decisions</h2>
337
+ <table>
338
+ <tr><th>Date</th><th>Repo</th><th>Decision</th></tr>
339
+ {decision_rows}
340
+ </table>
341
+
342
+ <h2>Governance Coverage</h2>
343
+ <table>
344
+ <tr><th>Metric</th><th>Value</th></tr>
345
+ <tr><td>Charters</td><td>{gov.get("charters",0)} across {gov.get("repos",0)} repos ({gov.get("functions",0)} functions documented)</td></tr>
346
+ <tr><td>Hooks Installed</td><td class="{"ok" if gov.get("hooks_installed",0) == gov.get("repos",0) else "warn"}">{gov.get("hooks_installed",0)} / {gov.get("repos",0)} repos</td></tr>
347
+ <tr><td>Trailers Flowing</td><td class="{"ok" if gov.get("trailers_flowing",0) == gov.get("repos",0) else "warn"}">{gov.get("trailers_flowing",0)} / {gov.get("repos",0)} repos</td></tr>
348
+ <tr><td>Open Issues</td><td>{gov.get("issues",0)}</td></tr>
349
+ </table>
350
+
351
+ <div class="footer">
352
+ Generated by Gator {data["version"]} &middot; {data["generated_at"]} &middot;
353
+ Machine: {machine.get("label", "?")} ({machine.get("id", "?")[:8]}) &middot;
354
+ <a href="https://github.com/cumberland-laboratories/gator" style="color: #484f58;">cumberland-laboratories/gator</a>
355
+ </div>
356
+
357
+ </div>
358
+ </body>
359
+ </html>'''
360
+
361
+ return html