tribunal-kit 4.2.0 → 4.3.1

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 (186) hide show
  1. package/.agent/ARCHITECTURE.md +21 -14
  2. package/.agent/agents/swarm-worker-contracts.md +5 -5
  3. package/.agent/agents/ui-ux-auditor.md +292 -0
  4. package/.agent/rules/GEMINI.md +8 -8
  5. package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
  6. package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
  7. package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
  8. package/.agent/scripts/_colors.js +18 -0
  9. package/.agent/scripts/_utils.js +42 -0
  10. package/.agent/scripts/auto_preview.js +197 -0
  11. package/.agent/scripts/bundle_analyzer.js +290 -0
  12. package/.agent/scripts/case_law_manager.js +684 -0
  13. package/.agent/scripts/checklist.js +266 -0
  14. package/.agent/scripts/colors.js +17 -0
  15. package/.agent/scripts/compress_skills.js +141 -0
  16. package/.agent/scripts/consolidate_skills.js +149 -0
  17. package/.agent/scripts/context_broker.js +609 -0
  18. package/.agent/scripts/deep_compress.js +150 -0
  19. package/.agent/scripts/dependency_analyzer.js +272 -0
  20. package/.agent/scripts/graph_builder.js +199 -0
  21. package/.agent/scripts/graph_zoom.js +154 -0
  22. package/.agent/scripts/inner_loop_validator.js +465 -0
  23. package/.agent/scripts/lint_runner.js +187 -0
  24. package/.agent/scripts/minify_context.js +100 -0
  25. package/.agent/scripts/patch_skills_meta.js +156 -0
  26. package/.agent/scripts/patch_skills_output.js +244 -0
  27. package/.agent/scripts/schema_validator.js +297 -0
  28. package/.agent/scripts/security_scan.js +303 -0
  29. package/.agent/scripts/session_manager.js +276 -0
  30. package/.agent/scripts/skill_evolution.js +644 -0
  31. package/.agent/scripts/skill_integrator.js +313 -0
  32. package/.agent/scripts/strengthen_skills.js +193 -0
  33. package/.agent/scripts/strip_tribunal.js +47 -0
  34. package/.agent/scripts/swarm_dispatcher.js +360 -0
  35. package/.agent/scripts/test_runner.js +193 -0
  36. package/.agent/scripts/utils.js +32 -0
  37. package/.agent/scripts/verify_all.js +256 -0
  38. package/.agent/skills/agent-organizer/SKILL.md +12 -4
  39. package/.agent/skills/agentic-patterns/SKILL.md +12 -4
  40. package/.agent/skills/ai-prompt-injection-defense/SKILL.md +12 -4
  41. package/.agent/skills/api-patterns/SKILL.md +209 -201
  42. package/.agent/skills/api-security-auditor/SKILL.md +12 -4
  43. package/.agent/skills/app-builder/SKILL.md +12 -4
  44. package/.agent/skills/app-builder/templates/SKILL.md +76 -68
  45. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
  46. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
  47. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
  48. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
  49. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
  50. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
  51. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
  52. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
  53. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
  54. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
  55. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
  56. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
  57. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
  58. package/.agent/skills/appflow-wireframe/SKILL.md +12 -4
  59. package/.agent/skills/architecture/SKILL.md +12 -4
  60. package/.agent/skills/authentication-best-practices/SKILL.md +12 -4
  61. package/.agent/skills/bash-linux/SKILL.md +12 -4
  62. package/.agent/skills/behavioral-modes/SKILL.md +12 -4
  63. package/.agent/skills/brainstorming/SKILL.md +12 -4
  64. package/.agent/skills/building-native-ui/SKILL.md +12 -4
  65. package/.agent/skills/clean-code/SKILL.md +12 -4
  66. package/.agent/skills/code-review-checklist/SKILL.md +12 -4
  67. package/.agent/skills/config-validator/SKILL.md +12 -4
  68. package/.agent/skills/csharp-developer/SKILL.md +12 -4
  69. package/.agent/skills/data-validation-schemas/SKILL.md +290 -282
  70. package/.agent/skills/database-design/SKILL.md +202 -194
  71. package/.agent/skills/deployment-procedures/SKILL.md +12 -4
  72. package/.agent/skills/devops-engineer/SKILL.md +12 -4
  73. package/.agent/skills/devops-incident-responder/SKILL.md +12 -4
  74. package/.agent/skills/doc.md +1 -1
  75. package/.agent/skills/documentation-templates/SKILL.md +12 -4
  76. package/.agent/skills/edge-computing/SKILL.md +12 -4
  77. package/.agent/skills/error-resilience/SKILL.md +390 -382
  78. package/.agent/skills/extract-design-system/SKILL.md +12 -4
  79. package/.agent/skills/framer-motion-expert/SKILL.md +206 -199
  80. package/.agent/skills/frontend-design/SKILL.md +163 -155
  81. package/.agent/skills/game-design-expert/SKILL.md +12 -4
  82. package/.agent/skills/game-engineering-expert/SKILL.md +12 -4
  83. package/.agent/skills/geo-fundamentals/SKILL.md +12 -4
  84. package/.agent/skills/github-operations/SKILL.md +12 -4
  85. package/.agent/skills/gsap-core/SKILL.md +54 -48
  86. package/.agent/skills/gsap-frameworks/SKILL.md +54 -48
  87. package/.agent/skills/gsap-performance/SKILL.md +54 -48
  88. package/.agent/skills/gsap-plugins/SKILL.md +54 -48
  89. package/.agent/skills/gsap-react/SKILL.md +54 -48
  90. package/.agent/skills/gsap-scrolltrigger/SKILL.md +54 -48
  91. package/.agent/skills/gsap-timeline/SKILL.md +54 -48
  92. package/.agent/skills/gsap-utils/SKILL.md +54 -48
  93. package/.agent/skills/i18n-localization/SKILL.md +12 -4
  94. package/.agent/skills/intelligent-routing/SKILL.md +41 -33
  95. package/.agent/skills/knowledge-graph/SKILL.md +36 -0
  96. package/.agent/skills/lint-and-validate/SKILL.md +12 -4
  97. package/.agent/skills/llm-engineering/SKILL.md +12 -4
  98. package/.agent/skills/local-first/SKILL.md +12 -4
  99. package/.agent/skills/mcp-builder/SKILL.md +12 -4
  100. package/.agent/skills/mobile-design/SKILL.md +225 -217
  101. package/.agent/skills/monorepo-management/SKILL.md +296 -288
  102. package/.agent/skills/motion-engineering/SKILL.md +195 -187
  103. package/.agent/skills/nextjs-react-expert/SKILL.md +196 -188
  104. package/.agent/skills/nodejs-best-practices/SKILL.md +12 -4
  105. package/.agent/skills/observability/SKILL.md +12 -4
  106. package/.agent/skills/parallel-agents/SKILL.md +12 -4
  107. package/.agent/skills/performance-profiling/SKILL.md +12 -4
  108. package/.agent/skills/plan-writing/SKILL.md +12 -4
  109. package/.agent/skills/platform-engineer/SKILL.md +12 -4
  110. package/.agent/skills/playwright-best-practices/SKILL.md +12 -4
  111. package/.agent/skills/powershell-windows/SKILL.md +12 -4
  112. package/.agent/skills/project-idioms/SKILL.md +12 -4
  113. package/.agent/skills/python-patterns/SKILL.md +12 -4
  114. package/.agent/skills/python-pro/SKILL.md +285 -277
  115. package/.agent/skills/react-specialist/SKILL.md +239 -231
  116. package/.agent/skills/readme-builder/SKILL.md +12 -4
  117. package/.agent/skills/realtime-patterns/SKILL.md +12 -4
  118. package/.agent/skills/red-team-tactics/SKILL.md +12 -4
  119. package/.agent/skills/rust-pro/SKILL.md +12 -4
  120. package/.agent/skills/seo-fundamentals/SKILL.md +12 -4
  121. package/.agent/skills/server-management/SKILL.md +12 -4
  122. package/.agent/skills/shadcn-ui-expert/SKILL.md +12 -4
  123. package/.agent/skills/skill-creator/SKILL.md +12 -4
  124. package/.agent/skills/sql-pro/SKILL.md +12 -4
  125. package/.agent/skills/supabase-postgres-best-practices/SKILL.md +12 -4
  126. package/.agent/skills/swiftui-expert/SKILL.md +12 -4
  127. package/.agent/skills/systematic-debugging/SKILL.md +12 -4
  128. package/.agent/skills/tailwind-patterns/SKILL.md +12 -4
  129. package/.agent/skills/tdd-workflow/SKILL.md +12 -4
  130. package/.agent/skills/test-result-analyzer/SKILL.md +12 -4
  131. package/.agent/skills/testing-patterns/SKILL.md +12 -4
  132. package/.agent/skills/trend-researcher/SKILL.md +12 -4
  133. package/.agent/skills/typescript-advanced/SKILL.md +297 -289
  134. package/.agent/skills/ui-ux-pro-max/SKILL.md +12 -4
  135. package/.agent/skills/ui-ux-researcher/SKILL.md +12 -4
  136. package/.agent/skills/vue-expert/SKILL.md +237 -229
  137. package/.agent/skills/vulnerability-scanner/SKILL.md +12 -4
  138. package/.agent/skills/web-accessibility-auditor/SKILL.md +12 -4
  139. package/.agent/skills/web-design-guidelines/SKILL.md +12 -4
  140. package/.agent/skills/webapp-testing/SKILL.md +12 -4
  141. package/.agent/skills/whimsy-injector/SKILL.md +12 -4
  142. package/.agent/skills/workflow-optimizer/SKILL.md +12 -4
  143. package/.agent/workflows/audit.md +6 -6
  144. package/.agent/workflows/deploy.md +1 -1
  145. package/.agent/workflows/generate.md +23 -6
  146. package/.agent/workflows/session.md +5 -5
  147. package/.agent/workflows/swarm.md +2 -2
  148. package/README.md +242 -186
  149. package/bin/tribunal-kit.js +297 -57
  150. package/package.json +81 -77
  151. package/scripts/changelog.js +167 -0
  152. package/scripts/sync-version.js +81 -0
  153. package/scripts/validate-payload.js +73 -0
  154. package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
  155. package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
  156. package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
  157. package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
  158. package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
  159. package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
  160. package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
  161. package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
  162. package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
  163. package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
  164. package/.agent/scripts/auto_preview.py +0 -180
  165. package/.agent/scripts/bundle_analyzer.py +0 -259
  166. package/.agent/scripts/case_law_manager.py +0 -755
  167. package/.agent/scripts/checklist.py +0 -209
  168. package/.agent/scripts/compress_skills.py +0 -167
  169. package/.agent/scripts/consolidate_skills.py +0 -173
  170. package/.agent/scripts/deep_compress.py +0 -202
  171. package/.agent/scripts/dependency_analyzer.py +0 -247
  172. package/.agent/scripts/lint_runner.py +0 -188
  173. package/.agent/scripts/minify_context.py +0 -80
  174. package/.agent/scripts/patch_skills_meta.py +0 -177
  175. package/.agent/scripts/patch_skills_output.py +0 -285
  176. package/.agent/scripts/schema_validator.py +0 -279
  177. package/.agent/scripts/security_scan.py +0 -224
  178. package/.agent/scripts/session_manager.py +0 -261
  179. package/.agent/scripts/skill_evolution.py +0 -563
  180. package/.agent/scripts/skill_integrator.py +0 -234
  181. package/.agent/scripts/strengthen_skills.py +0 -220
  182. package/.agent/scripts/strip_tribunal.py +0 -41
  183. package/.agent/scripts/swarm_dispatcher.py +0 -350
  184. package/.agent/scripts/test_runner.py +0 -192
  185. package/.agent/scripts/test_swarm_dispatcher.py +0 -163
  186. package/.agent/scripts/verify_all.py +0 -195
@@ -1,755 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- case_law_manager.py — Tribunal Kit Case Law Engine
4
- =====================================================
5
- Records rejected code patterns as "Cases" and surfaces them as
6
- binding Legal Precedence during future Tribunal reviews.
7
-
8
- Usage:
9
- python .agent/scripts/case_law_manager.py add-case
10
- python .agent/scripts/case_law_manager.py search-cases --query "forEach side effects"
11
- python .agent/scripts/case_law_manager.py list
12
- python .agent/scripts/case_law_manager.py show --id 7
13
- python .agent/scripts/case_law_manager.py export
14
- python .agent/scripts/case_law_manager.py stats
15
-
16
- Storage:
17
- .agent/history/case-law/index.json ← master index of all cases
18
- .agent/history/case-law/cases/ ← one JSON file per case
19
- """
20
-
21
- import os
22
- import sys
23
- import json
24
- import hashlib
25
- import math
26
- import re
27
- from pathlib import Path
28
- from datetime import datetime
29
- from collections import Counter
30
-
31
- # ── Colours ──────────────────────────────────────────────────────────────────
32
- GREEN = "\033[92m"
33
- YELLOW = "\033[93m"
34
- CYAN = "\033[96m"
35
- RED = "\033[91m"
36
- BLUE = "\033[94m"
37
- BOLD = "\033[1m"
38
- DIM = "\033[2m"
39
- RESET = "\033[0m"
40
-
41
- # ── Paths ─────────────────────────────────────────────────────────────────────
42
- def find_agent_dir() -> Path:
43
- """Walk up until we find .agent/"""
44
- current = Path.cwd()
45
- while current != current.parent:
46
- candidate = current / ".agent"
47
- if candidate.is_dir():
48
- return candidate
49
- current = current.parent
50
-
51
- print("\033[91m✖ Error: '.agent' directory not found. Please run 'npx tribunal-kit init' first.\033[0m")
52
- sys.exit(1)
53
-
54
- AGENT_DIR = find_agent_dir()
55
- HISTORY_DIR = AGENT_DIR / "history" / "case-law"
56
- CASES_DIR = HISTORY_DIR / "cases"
57
- INDEX_FILE = HISTORY_DIR / "index.json"
58
-
59
- VALID_DOMAINS = {
60
- "backend", "frontend", "database", "security",
61
- "performance", "mobile", "testing", "devops", "general"
62
- }
63
-
64
- VALID_VERDICTS = {"REJECTED", "APPROVED_WITH_CONDITIONS", "PRECEDENT_SET", "OVERRULED"}
65
-
66
- # ── Noise filter (skip trivial rejections during auto-record) ────────────────
67
- NOISE_PATTERNS = [
68
- r"\bformatting\b",
69
- r"\bwhitespace\b",
70
- r"\bindent(ation)?\b",
71
- r"\bimport\s+order\b",
72
- r"\btrailing\s+(comma|space|whitespace)\b",
73
- r"\bsemicolon\b",
74
- r"\bprettier\b",
75
- r"\beslint.*fix\b",
76
- r"\blint.*only\b",
77
- ]
78
-
79
- def is_noise_rejection(reason: str) -> bool:
80
- """Return True if the rejection reason is trivial (formatting/lint-only)."""
81
- lower = reason.lower()
82
- for pattern in NOISE_PATTERNS:
83
- if re.search(pattern, lower):
84
- return True
85
- return False
86
-
87
- # ── Trivial-change filter (Semantic Delta) ────────────────────────────────────
88
- TRIVIAL_PATTERNS = [
89
- r"^\s*$", # blank lines
90
- r"^\s*//.*$", # comment-only lines
91
- r"^\s*#.*$", # python comments
92
- r"^\s*\*.*$", # JSDoc lines
93
- r"^[\+\-]\s*(import\s+\{[^}]+\}|from\s+['\"])", # import reorders
94
- ]
95
-
96
- def is_trivial_line(line: str) -> bool:
97
- for pattern in TRIVIAL_PATTERNS:
98
- if re.match(pattern, line):
99
- return True
100
- return False
101
-
102
- def semantic_delta(diff_text: str) -> str:
103
- """
104
- Strip trivial changes from a diff and return only the meaningful
105
- architectural delta. This is the core token-saving mechanism:
106
- 80% of whitespace/comment/import-order noise is removed before
107
- any LLM sees the content.
108
- """
109
- lines = diff_text.splitlines()
110
- meaningful = []
111
- for line in lines:
112
- if line.startswith(("+++", "---", "@@")):
113
- meaningful.append(line)
114
- continue
115
- if line.startswith(("+", "-")):
116
- code_part = line[1:]
117
- if not is_trivial_line(code_part):
118
- meaningful.append(line)
119
- else:
120
- meaningful.append(line)
121
-
122
- filtered = "\n".join(meaningful)
123
- # Collapse runs of 3+ blank context lines into separator
124
- filtered = re.sub(r"(\n[ ]{0,1}\n){3,}", "\n\n", filtered)
125
- return filtered.strip()
126
-
127
- def content_hash(text: str) -> str:
128
- """Stable 8-char fingerprint of meaningful content."""
129
- cleaned = semantic_delta(text)
130
- return hashlib.sha256(cleaned.encode()).hexdigest()[:8]
131
-
132
- # ── Index helpers ─────────────────────────────────────────────────────────────
133
- def load_index() -> dict:
134
- HISTORY_DIR.mkdir(parents=True, exist_ok=True)
135
- CASES_DIR.mkdir(parents=True, exist_ok=True)
136
- if INDEX_FILE.exists():
137
- try:
138
- return json.loads(INDEX_FILE.read_text(encoding="utf-8"))
139
- except (json.JSONDecodeError, IOError):
140
- pass
141
- return {"version": "1.0", "cases": [], "next_id": 1}
142
-
143
- def save_index(index: dict) -> None:
144
- HISTORY_DIR.mkdir(parents=True, exist_ok=True)
145
- INDEX_FILE.write_text(json.dumps(index, indent=2), encoding="utf-8")
146
-
147
- def load_case(case_id: int) -> dict | None:
148
- path = CASES_DIR / f"case-{case_id:04d}.json"
149
- if not path.exists():
150
- return None
151
- try:
152
- return json.loads(path.read_text(encoding="utf-8"))
153
- except Exception:
154
- return None
155
-
156
- def save_case(case: dict) -> None:
157
- path = CASES_DIR / f"case-{case['id']:04d}.json"
158
- path.write_text(json.dumps(case, indent=2), encoding="utf-8")
159
-
160
- # ── Keyword/tag extraction ─────────────────────────────────────────────────────
161
- def extract_tags(text: str) -> list[str]:
162
- """Extract meaningful code-level keywords from diff/reason text."""
163
- # Pull identifiers: camelCase, snake_case, method calls
164
- tokens = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b", text)
165
- stop_words = {
166
- "the", "and", "for", "was", "this", "with", "that",
167
- "from", "are", "not", "use", "but", "also", "code",
168
- "have", "will", "should", "must", "can", "may", "any",
169
- "all", "new", "old", "add", "get", "set", "var", "let",
170
- "const", "function", "return", "import", "export", "class",
171
- "async", "await", "true", "false", "null", "undefined"
172
- }
173
- seen = set()
174
- tags = []
175
- for token in tokens:
176
- lower = token.lower()
177
- if lower not in stop_words and lower not in seen:
178
- seen.add(lower)
179
- tags.append(lower)
180
- if len(tags) >= 20:
181
- break
182
- return tags
183
-
184
- # ── Similarity scoring (TF-IDF Cosine — token-free) ──────────────────────────
185
- def _build_idf(corpus: list[list[str]]) -> dict[str, float]:
186
- """Compute Inverse Document Frequency across all case tag-lists."""
187
- n = len(corpus)
188
- if n == 0:
189
- return {}
190
- doc_freq: dict[str, int] = Counter()
191
- for tags in corpus:
192
- for unique_tag in set(tags):
193
- doc_freq[unique_tag] += 1
194
- return {term: math.log((n + 1) / (df + 1)) + 1.0 for term, df in doc_freq.items()}
195
-
196
-
197
- def tfidf_cosine_similarity(query_tags: list[str], case_tags: list[str],
198
- idf: dict[str, float]) -> float:
199
- """
200
- TF-IDF weighted cosine similarity. No LLM required.
201
- Significantly more accurate than Jaccard for code pattern matching.
202
- """
203
- if not query_tags or not case_tags:
204
- return 0.0
205
-
206
- # Term frequency vectors
207
- tf_q = Counter(query_tags)
208
- tf_c = Counter(case_tags)
209
-
210
- # All unique terms
211
- all_terms = set(tf_q) | set(tf_c)
212
-
213
- # Weighted vectors
214
- dot = 0.0
215
- mag_q = 0.0
216
- mag_c = 0.0
217
- for term in all_terms:
218
- w_q = tf_q.get(term, 0) * idf.get(term, 1.0)
219
- w_c = tf_c.get(term, 0) * idf.get(term, 1.0)
220
- dot += w_q * w_c
221
- mag_q += w_q ** 2
222
- mag_c += w_c ** 2
223
-
224
- if mag_q == 0 or mag_c == 0:
225
- return 0.0
226
- return dot / (math.sqrt(mag_q) * math.sqrt(mag_c))
227
-
228
-
229
- # Backward-compatibility alias
230
- def jaccard_similarity(tags_a: list[str], tags_b: list[str]) -> float:
231
- """Legacy fallback — kept for compatibility but no longer primary."""
232
- if not tags_a or not tags_b:
233
- return 0.0
234
- set_a, set_b = set(tags_a), set(tags_b)
235
- return len(set_a & set_b) / len(set_a | set_b)
236
-
237
- # ── Commands ──────────────────────────────────────────────────────────────────
238
- def cmd_add_case(args: list[str]) -> None:
239
- """
240
- Interactive case recording. All fields collected via prompts so the
241
- agent can call this without any arguments — the script does the rest.
242
- """
243
- print(f"\n{BOLD}{CYAN}━━━ Recording New Case ━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
244
-
245
- # ── Gather fields ─────
246
- diff_text = prompt_multiline("Paste the REJECTED diff (code snippet):", "END_DIFF")
247
- if not diff_text.strip():
248
- print(f"{RED}✖ Diff cannot be empty. Aborting.{RESET}")
249
- sys.exit(1)
250
-
251
- reason = prompt_line("Rejection reason (1-2 sentences):")
252
- if not reason.strip():
253
- print(f"{RED}✖ Reason cannot be empty. Aborting.{RESET}")
254
- sys.exit(1)
255
-
256
- domain = prompt_choice(
257
- "Domain",
258
- sorted(VALID_DOMAINS),
259
- default="general"
260
- )
261
-
262
- verdict = prompt_choice(
263
- "Verdict",
264
- sorted(VALID_VERDICTS),
265
- default="REJECTED"
266
- )
267
-
268
- pr_ref = prompt_line("PR / commit reference (optional, e.g. PR-404):").strip() or None
269
- reviewer = prompt_line("Reviewer agent (optional, e.g. security-auditor):").strip() or None
270
-
271
- # ── Build delta ──────
272
- delta = semantic_delta(diff_text)
273
- fingerprint = content_hash(diff_text)
274
- tags = extract_tags(diff_text + " " + reason)
275
-
276
- # ── Persist ──────────
277
- index = load_index()
278
- case_id = index["next_id"]
279
-
280
- case = {
281
- "id": case_id,
282
- "fingerprint": fingerprint,
283
- "timestamp": datetime.now().isoformat(timespec="seconds"),
284
- "domain": domain,
285
- "verdict": verdict,
286
- "reason": reason.strip(),
287
- "pr_ref": pr_ref,
288
- "reviewer": reviewer,
289
- "tags": tags,
290
- "diff_raw": diff_text.strip(),
291
- "diff_delta": delta
292
- }
293
-
294
- save_case(case)
295
-
296
- # Update index
297
- index["cases"].append({
298
- "id": case_id,
299
- "fingerprint": fingerprint,
300
- "domain": domain,
301
- "verdict": verdict,
302
- "tags": tags,
303
- "timestamp": case["timestamp"],
304
- "reason_summary": reason.strip()[:120]
305
- })
306
- index["next_id"] = case_id + 1
307
- save_index(index)
308
-
309
- print(f"\n{GREEN}✔ Case #{case_id:04d} recorded{RESET}")
310
- print(f" {DIM}Fingerprint : {fingerprint}{RESET}")
311
- print(f" {DIM}Domain : {domain}{RESET}")
312
- print(f" {DIM}Tags : {', '.join(tags[:8])}{RESET}")
313
- print(f" {DIM}Stored at : {CASES_DIR / f'case-{case_id:04d}.json'}{RESET}")
314
- print()
315
-
316
-
317
- def cmd_search_cases(args: list[str]) -> None:
318
- """
319
- Search past cases by query text using Jaccard tag similarity.
320
- Token-free — no LLM call required.
321
- """
322
- query = " ".join(args)
323
- if not query:
324
- # Try reading from --query flag
325
- try:
326
- qi = sys.argv.index("--query")
327
- query = " ".join(sys.argv[qi + 1:])
328
- except (ValueError, IndexError):
329
- pass
330
-
331
- if not query:
332
- print(f"{RED}✖ Provide a search query: search-cases --query \"forEach side effects\"{RESET}")
333
- sys.exit(1)
334
-
335
- query_tags = extract_tags(query)
336
- index = load_index()
337
-
338
- if not index["cases"]:
339
- print(f"{YELLOW}No cases recorded yet. Use 'add-case' to record your first rejection.{RESET}")
340
- return
341
-
342
- # Build corpus IDF from all stored cases
343
- corpus = [entry.get("tags", []) for entry in index["cases"]]
344
- idf = _build_idf(corpus)
345
-
346
- # Score every case with TF-IDF cosine
347
- scored = []
348
- for entry in index["cases"]:
349
- score = tfidf_cosine_similarity(query_tags, entry.get("tags", []), idf)
350
- if score > 0.0:
351
- scored.append((score, entry))
352
-
353
- scored.sort(key=lambda x: x[0], reverse=True)
354
- top = scored[:5]
355
-
356
- if not top:
357
- print(f"{YELLOW}No matching cases found for: \"{query}\"{RESET}")
358
- print(f" {DIM}Try broader terms or check 'list' for available cases.{RESET}")
359
- return
360
-
361
- print(f"\n{BOLD}{CYAN}━━━ Case Law Search Results ━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
362
- print(f" Query : {BOLD}{query}{RESET}")
363
- print(f" Matches: {len(top)} of {len(index['cases'])} cases\n")
364
-
365
- for score, entry in top:
366
- verdict_color = RED if entry["verdict"] == "REJECTED" else YELLOW
367
- print(f" {BOLD}Case #{entry['id']:04d}{RESET} {verdict_color}[{entry['verdict']}]{RESET} "
368
- f"{DIM}{entry['timestamp'][:10]}{RESET} score={score:.2f}")
369
- print(f" {DIM}Domain: {entry['domain']}{RESET}")
370
- print(f" {entry['reason_summary']}")
371
- print(f" {DIM}Tags: {', '.join(entry['tags'][:8])}{RESET}")
372
- print()
373
-
374
- print(f" {DIM}Run 'show --id <N>' to see the full diff for any case.{RESET}")
375
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
376
-
377
-
378
- def cmd_list(args: list[str]) -> None:
379
- index = load_index()
380
- cases = index.get("cases", [])
381
- if not cases:
382
- print(f"{YELLOW}No cases recorded yet.{RESET}")
383
- return
384
-
385
- domain_filter = None
386
- if "--domain" in args:
387
- try:
388
- domain_filter = args[args.index("--domain") + 1].lower()
389
- except IndexError:
390
- pass
391
-
392
- filtered = [c for c in cases if not domain_filter or c["domain"] == domain_filter]
393
- total = len(filtered)
394
-
395
- print(f"\n{BOLD}{CYAN}━━━ Case Law Index ({total} cases) ━━━━━━━━━━━━━━━━━━━━{RESET}")
396
- if domain_filter:
397
- print(f" {DIM}Filtered by domain: {domain_filter}{RESET}\n")
398
-
399
- for entry in reversed(filtered[-20:]):
400
- verdict_color = RED if entry["verdict"] == "REJECTED" else YELLOW
401
- print(f" {BOLD}#{entry['id']:04d}{RESET} "
402
- f"{verdict_color}[{entry['verdict']}]{RESET} "
403
- f"{DIM}{entry['domain'].upper()}{RESET} "
404
- f"{entry['timestamp'][:10]}")
405
- print(f" {entry['reason_summary'][:80]}")
406
-
407
- if total > 20:
408
- print(f"\n {YELLOW}... showing last 20 of {total}. Use 'export' for full history.{RESET}")
409
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
410
-
411
-
412
- def cmd_show(args: list[str]) -> None:
413
- case_id = None
414
- if "--id" in args:
415
- try:
416
- case_id = int(args[args.index("--id") + 1])
417
- except (IndexError, ValueError):
418
- pass
419
-
420
- if case_id is None:
421
- print(f"{RED}✖ Provide a case ID: show --id 7{RESET}")
422
- sys.exit(1)
423
-
424
- case = load_case(case_id)
425
- if not case:
426
- print(f"{RED}✖ Case #{case_id:04d} not found.{RESET}")
427
- sys.exit(1)
428
-
429
- verdict_color = RED if case["verdict"] == "REJECTED" else YELLOW
430
- print(f"\n{BOLD}{CYAN}━━━ Case #{case['id']:04d} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
431
- print(f" Verdict : {verdict_color}{BOLD}{case['verdict']}{RESET}")
432
- print(f" Domain : {case['domain']}")
433
- print(f" Recorded : {case['timestamp']}")
434
- if case.get("pr_ref"):
435
- print(f" PR / Ref : {case['pr_ref']}")
436
- if case.get("reviewer"):
437
- print(f" Reviewer : {case['reviewer']}")
438
- print(f"\n {BOLD}Reason:{RESET}")
439
- print(f" {case['reason']}")
440
- print(f"\n {BOLD}Semantic Delta (meaningful changes only):{RESET}")
441
- print(f" {DIM}─────────────────────────────────────────{RESET}")
442
- for line in case.get("diff_delta", case["diff_raw"]).splitlines()[:40]:
443
- if line.startswith("+"):
444
- print(f" {GREEN}{line}{RESET}")
445
- elif line.startswith("-"):
446
- print(f" {RED}{line}{RESET}")
447
- else:
448
- print(f" {DIM}{line}{RESET}")
449
- print(f"\n {BOLD}Tags:{RESET} {', '.join(case.get('tags', []))}")
450
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
451
-
452
-
453
- def cmd_export(args: list[str]) -> None:
454
- to_stdout = "--stdout" in args
455
- index = load_index()
456
- cases = index.get("cases", [])
457
-
458
- if not cases:
459
- print(f"{YELLOW}No cases to export.{RESET}")
460
- return
461
-
462
- lines = [
463
- "# Tribunal Case Law — Full Export\n",
464
- f"Generated: {datetime.now().isoformat(timespec='seconds')}",
465
- f"Total Cases: {len(cases)}\n",
466
- "---\n"
467
- ]
468
-
469
- for entry in cases:
470
- case = load_case(entry["id"]) or entry
471
- verdict_badge = f"[{case.get('verdict', 'REJECTED')}]"
472
- lines.append(f"## Case #{entry['id']:04d} {verdict_badge}")
473
- lines.append(f"**Domain:** {entry['domain']} ")
474
- lines.append(f"**Recorded:** {entry['timestamp'][:10]} ")
475
- if case.get("pr_ref"):
476
- lines.append(f"**PR/Ref:** {case['pr_ref']} ")
477
- lines.append(f"\n**Reason:** {entry['reason_summary']}\n")
478
- lines.append(f"**Tags:** `{', '.join(entry['tags'][:8])}`\n")
479
- lines.append("---\n")
480
-
481
- content = "\n".join(lines)
482
-
483
- if to_stdout:
484
- print(content)
485
- else:
486
- out_path = HISTORY_DIR / "case-law-export.md"
487
- out_path.write_text(content, encoding="utf-8")
488
- print(f"{GREEN}✔ Exported {len(cases)} cases to {out_path}{RESET}")
489
-
490
-
491
- def cmd_stats(args: list[str]) -> None:
492
- index = load_index()
493
- cases = index.get("cases", [])
494
-
495
- domain_counts: dict[str, int] = {}
496
- verdict_counts: dict[str, int] = {}
497
- for c in cases:
498
- domain_counts[c["domain"]] = domain_counts.get(c["domain"], 0) + 1
499
- verdict_counts[c["verdict"]] = verdict_counts.get(c["verdict"], 0) + 1
500
-
501
- print(f"\n{BOLD}{CYAN}━━━ Case Law Statistics ━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
502
- print(f" Total cases: {BOLD}{len(cases)}{RESET}")
503
- print(f"\n {BOLD}By Verdict:{RESET}")
504
- for verdict, count in sorted(verdict_counts.items()):
505
- color = RED if verdict == "REJECTED" else YELLOW
506
- print(f" {color}{verdict:<30}{RESET} {count}")
507
- print(f"\n {BOLD}By Domain:{RESET}")
508
- for domain, count in sorted(domain_counts.items(), key=lambda x: -x[1]):
509
- print(f" {CYAN}{domain:<20}{RESET} {count}")
510
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
511
-
512
-
513
- def cmd_auto_record(args: list[str]) -> None:
514
- """
515
- Non-interactive auto-recording for AI-driven case creation.
516
- Called by the precedence-reviewer after a Tribunal rejection.
517
-
518
- Usage:
519
- python case_law_manager.py auto-record \\
520
- --diff "code snippet" \\
521
- --reason "why rejected" \\
522
- --domain security \\
523
- --verdict REJECTED \\
524
- --reviewer security-auditor
525
- """
526
- # Parse flags
527
- def get_flag(name: str) -> str:
528
- flag = f"--{name}"
529
- all_args = sys.argv[1:]
530
- if flag in all_args:
531
- idx = all_args.index(flag)
532
- if idx + 1 < len(all_args):
533
- return all_args[idx + 1]
534
- return ""
535
-
536
- diff_text = get_flag("diff")
537
- reason = get_flag("reason")
538
- domain = get_flag("domain") or "general"
539
- verdict = get_flag("verdict") or "REJECTED"
540
- reviewer = get_flag("reviewer") or None
541
- pr_ref = get_flag("pr-ref") or None
542
-
543
- if not diff_text or not reason:
544
- print(f"{RED}✖ auto-record requires --diff and --reason flags.{RESET}")
545
- print(f" Usage: auto-record --diff \"code\" --reason \"why\" --domain security --reviewer agent-name")
546
- sys.exit(1)
547
-
548
- # Noise filter — skip trivial rejections
549
- if is_noise_rejection(reason):
550
- print(f"{DIM}⊘ Skipped: trivial rejection (noise filter matched).{RESET}")
551
- return
552
-
553
- if domain not in VALID_DOMAINS:
554
- domain = "general"
555
- if verdict not in VALID_VERDICTS:
556
- verdict = "REJECTED"
557
-
558
- # Duplicate check: fingerprint match
559
- fingerprint = content_hash(diff_text)
560
- index = load_index()
561
- for existing in index["cases"]:
562
- if existing.get("fingerprint") == fingerprint:
563
- print(f"{YELLOW}⊘ Duplicate: Case #{existing['id']:04d} already records this pattern.{RESET}")
564
- return
565
-
566
- # Build and persist
567
- delta = semantic_delta(diff_text)
568
- tags = extract_tags(diff_text + " " + reason)
569
- case_id = index["next_id"]
570
-
571
- case_record = {
572
- "id": case_id,
573
- "fingerprint": fingerprint,
574
- "timestamp": datetime.now().isoformat(timespec="seconds"),
575
- "domain": domain,
576
- "verdict": verdict,
577
- "reason": reason.strip(),
578
- "pr_ref": pr_ref,
579
- "reviewer": reviewer,
580
- "tags": tags,
581
- "diff_raw": diff_text.strip(),
582
- "diff_delta": delta,
583
- "auto_recorded": True
584
- }
585
-
586
- save_case(case_record)
587
-
588
- index["cases"].append({
589
- "id": case_id,
590
- "fingerprint": fingerprint,
591
- "domain": domain,
592
- "verdict": verdict,
593
- "tags": tags,
594
- "timestamp": case_record["timestamp"],
595
- "reason_summary": reason.strip()[:120]
596
- })
597
- index["next_id"] = case_id + 1
598
- save_index(index)
599
-
600
- print(f"{GREEN}✔ Auto-recorded Case #{case_id:04d}{RESET} [{verdict}] domain={domain}")
601
- print(f" {DIM}Reason: {reason[:80]}{RESET}")
602
-
603
-
604
- def cmd_overrule(args: list[str]) -> None:
605
- """
606
- Formally overrule a past precedent. Does NOT delete the case —
607
- marks it as OVERRULED with a reason, preserving legal history.
608
- """
609
- case_id = None
610
- if "--id" in args:
611
- try:
612
- case_id = int(args[args.index("--id") + 1])
613
- except (IndexError, ValueError):
614
- pass
615
-
616
- if case_id is None:
617
- print(f"{RED}✖ Provide a case ID: overrule --id 7{RESET}")
618
- sys.exit(1)
619
-
620
- case_record = load_case(case_id)
621
- if not case_record:
622
- print(f"{RED}✖ Case #{case_id:04d} not found.{RESET}")
623
- sys.exit(1)
624
-
625
- if case_record["verdict"] == "OVERRULED":
626
- print(f"{YELLOW}Case #{case_id:04d} is already OVERRULED.{RESET}")
627
- return
628
-
629
- # Get reason for overruling
630
- reason = None
631
- if "--reason" in args:
632
- try:
633
- reason = args[args.index("--reason") + 1]
634
- except (IndexError, ValueError):
635
- pass
636
-
637
- if not reason:
638
- reason = prompt_line("Reason for overruling this precedent:")
639
-
640
- if not reason or not reason.strip():
641
- print(f"{RED}✖ An overrule reason is required.{RESET}")
642
- sys.exit(1)
643
-
644
- # Preserve history
645
- old_verdict = case_record["verdict"]
646
- case_record["verdict"] = "OVERRULED"
647
- case_record["overruled_at"] = datetime.now().isoformat(timespec="seconds")
648
- case_record["overrule_reason"] = reason.strip()
649
- case_record["previous_verdict"] = old_verdict
650
- save_case(case_record)
651
-
652
- # Update index entry
653
- index = load_index()
654
- for entry in index["cases"]:
655
- if entry["id"] == case_id:
656
- entry["verdict"] = "OVERRULED"
657
- break
658
- save_index(index)
659
-
660
- print(f"\n{GREEN}✔ Case #{case_id:04d} OVERRULED{RESET}")
661
- print(f" {DIM}Previous verdict : {old_verdict}{RESET}")
662
- print(f" {DIM}Overrule reason : {reason.strip()}{RESET}")
663
- print(f" {DIM}The case is preserved in history but no longer blocks reviews.{RESET}")
664
- print()
665
-
666
-
667
- # ── Input helpers ─────────────────────────────────────────────────────────────
668
- def prompt_multiline(prompt: str, sentinel: str) -> str:
669
- print(f" {BOLD}{prompt}{RESET}")
670
- print(f" {DIM}(Type or paste content. Type '{sentinel}' on its own line when done.){RESET}")
671
- lines = []
672
- while True:
673
- try:
674
- line = input()
675
- except EOFError:
676
- break
677
- if line.strip() == sentinel:
678
- break
679
- lines.append(line)
680
- return "\n".join(lines)
681
-
682
-
683
- def prompt_line(prompt: str) -> str:
684
- print(f" {BOLD}{prompt}{RESET}", end=" ")
685
- try:
686
- return input()
687
- except EOFError:
688
- return ""
689
-
690
-
691
- def prompt_choice(label: str, choices: list[str], default: str) -> str:
692
- opts = " / ".join(
693
- f"{BOLD}{c}{RESET}" if c == default else c
694
- for c in choices
695
- )
696
- print(f" {BOLD}{label}{RESET} [{opts}] (default: {default}): ", end="")
697
- try:
698
- value = input().strip().lower()
699
- except EOFError:
700
- return default
701
- if not value or value not in choices:
702
- return default
703
- return value
704
-
705
-
706
- # ── Main ──────────────────────────────────────────────────────────────────────
707
- COMMANDS = {
708
- "add-case": cmd_add_case,
709
- "auto-record": cmd_auto_record,
710
- "search-cases": cmd_search_cases,
711
- "list": cmd_list,
712
- "show": cmd_show,
713
- "overrule": cmd_overrule,
714
- "export": cmd_export,
715
- "stats": cmd_stats,
716
- }
717
-
718
- def main() -> None:
719
- # Ensure Unicode output works on Windows terminals
720
- if hasattr(sys.stdout, "reconfigure"):
721
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
722
-
723
- argv = sys.argv[1:]
724
- if not argv or argv[0] in ("-h", "--help", "help"):
725
- print(f"""
726
- {BOLD}case_law_manager.py{RESET} — Tribunal Case Law Engine
727
-
728
- {BOLD}Commands:{RESET}
729
- add-case Record a new rejected pattern (interactive)
730
- auto-record --diff --reason Record a rejection (non-interactive, for AI agents)
731
- search-cases --query <text> Find relevant precedents (TF-IDF cosine, token-free)
732
- list [--domain <domain>] List all recorded cases
733
- show --id <N> Show full diff for a case
734
- overrule --id <N> Formally overrule a past precedent
735
- export [--stdout] Export all cases to Markdown
736
- stats Show breakdown by domain/verdict
737
-
738
- {BOLD}Domains:{RESET} {', '.join(sorted(VALID_DOMAINS))}
739
- {BOLD}Verdicts:{RESET} {', '.join(sorted(VALID_VERDICTS))}
740
- """)
741
- return
742
-
743
- cmd = argv[0]
744
- rest = argv[1:]
745
-
746
- if cmd not in COMMANDS:
747
- print(f"{RED}✖ Unknown command: '{cmd}'{RESET}")
748
- print(f" Valid: {', '.join(COMMANDS)}")
749
- sys.exit(1)
750
-
751
- COMMANDS[cmd](rest)
752
-
753
-
754
- if __name__ == "__main__":
755
- main()