tribunal-kit 4.0.1 → 4.3.0

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 (196) hide show
  1. package/.agent/ARCHITECTURE.md +21 -14
  2. package/.agent/GEMINI.md +4 -2
  3. package/.agent/agents/api-architect.md +66 -0
  4. package/.agent/agents/db-latency-auditor.md +216 -0
  5. package/.agent/agents/precedence-reviewer.md +41 -4
  6. package/.agent/agents/resilience-reviewer.md +88 -0
  7. package/.agent/agents/schema-reviewer.md +67 -0
  8. package/.agent/agents/swarm-worker-contracts.md +5 -5
  9. package/.agent/agents/throughput-optimizer.md +299 -0
  10. package/.agent/agents/ui-ux-auditor.md +292 -0
  11. package/.agent/agents/vitals-reviewer.md +223 -0
  12. package/.agent/history/case-law/cases/case-0001.json +33 -0
  13. package/.agent/history/case-law/index.json +35 -0
  14. package/.agent/rules/GEMINI.md +28 -11
  15. package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
  16. package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
  17. package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
  18. package/.agent/scripts/_colors.js +18 -0
  19. package/.agent/scripts/_utils.js +42 -0
  20. package/.agent/scripts/auto_preview.js +197 -0
  21. package/.agent/scripts/bundle_analyzer.js +290 -0
  22. package/.agent/scripts/case_law_manager.js +684 -0
  23. package/.agent/scripts/checklist.js +266 -0
  24. package/.agent/scripts/colors.js +17 -0
  25. package/.agent/scripts/compress_skills.js +141 -0
  26. package/.agent/scripts/consolidate_skills.js +149 -0
  27. package/.agent/scripts/context_broker.js +609 -0
  28. package/.agent/scripts/deep_compress.js +150 -0
  29. package/.agent/scripts/dependency_analyzer.js +272 -0
  30. package/.agent/scripts/inner_loop_validator.js +465 -0
  31. package/.agent/scripts/lint_runner.js +187 -0
  32. package/.agent/scripts/minify_context.js +100 -0
  33. package/.agent/scripts/patch_skills_meta.js +156 -0
  34. package/.agent/scripts/patch_skills_output.js +244 -0
  35. package/.agent/scripts/schema_validator.js +297 -0
  36. package/.agent/scripts/security_scan.js +303 -0
  37. package/.agent/scripts/session_manager.js +276 -0
  38. package/.agent/scripts/skill_evolution.js +644 -0
  39. package/.agent/scripts/skill_integrator.js +313 -0
  40. package/.agent/scripts/strengthen_skills.js +193 -0
  41. package/.agent/scripts/strip_tribunal.js +47 -0
  42. package/.agent/scripts/swarm_dispatcher.js +360 -0
  43. package/.agent/scripts/test_runner.js +193 -0
  44. package/.agent/scripts/utils.js +32 -0
  45. package/.agent/scripts/verify_all.js +256 -0
  46. package/.agent/skills/agent-organizer/SKILL.md +42 -0
  47. package/.agent/skills/agentic-patterns/SKILL.md +42 -0
  48. package/.agent/skills/ai-prompt-injection-defense/SKILL.md +42 -0
  49. package/.agent/skills/api-patterns/SKILL.md +42 -0
  50. package/.agent/skills/api-security-auditor/SKILL.md +42 -0
  51. package/.agent/skills/app-builder/SKILL.md +42 -0
  52. package/.agent/skills/app-builder/templates/SKILL.md +70 -0
  53. package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
  54. package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
  55. package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
  56. package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
  57. package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
  58. package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
  59. package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
  60. package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
  61. package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
  62. package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
  63. package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
  64. package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
  65. package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
  66. package/.agent/skills/appflow-wireframe/SKILL.md +42 -0
  67. package/.agent/skills/architecture/SKILL.md +42 -0
  68. package/.agent/skills/authentication-best-practices/SKILL.md +42 -0
  69. package/.agent/skills/bash-linux/SKILL.md +42 -0
  70. package/.agent/skills/behavioral-modes/SKILL.md +42 -0
  71. package/.agent/skills/brainstorming/SKILL.md +42 -0
  72. package/.agent/skills/building-native-ui/SKILL.md +42 -0
  73. package/.agent/skills/clean-code/SKILL.md +42 -0
  74. package/.agent/skills/code-review-checklist/SKILL.md +42 -0
  75. package/.agent/skills/config-validator/SKILL.md +42 -0
  76. package/.agent/skills/csharp-developer/SKILL.md +42 -0
  77. package/.agent/skills/data-validation-schemas/SKILL.md +320 -0
  78. package/.agent/skills/database-design/SKILL.md +42 -0
  79. package/.agent/skills/deployment-procedures/SKILL.md +42 -0
  80. package/.agent/skills/devops-engineer/SKILL.md +42 -0
  81. package/.agent/skills/devops-incident-responder/SKILL.md +42 -0
  82. package/.agent/skills/doc.md +1 -1
  83. package/.agent/skills/documentation-templates/SKILL.md +42 -0
  84. package/.agent/skills/edge-computing/SKILL.md +42 -0
  85. package/.agent/skills/error-resilience/SKILL.md +420 -0
  86. package/.agent/skills/extract-design-system/SKILL.md +42 -0
  87. package/.agent/skills/framer-motion-expert/SKILL.md +42 -1
  88. package/.agent/skills/frontend-design/SKILL.md +42 -0
  89. package/.agent/skills/game-design-expert/SKILL.md +42 -0
  90. package/.agent/skills/game-engineering-expert/SKILL.md +42 -0
  91. package/.agent/skills/geo-fundamentals/SKILL.md +42 -0
  92. package/.agent/skills/github-operations/SKILL.md +42 -0
  93. package/.agent/skills/gsap-core/SKILL.md +300 -0
  94. package/.agent/skills/gsap-frameworks/SKILL.md +199 -0
  95. package/.agent/skills/gsap-performance/SKILL.md +125 -0
  96. package/.agent/skills/gsap-plugins/SKILL.md +472 -0
  97. package/.agent/skills/gsap-react/SKILL.md +181 -0
  98. package/.agent/skills/gsap-scrolltrigger/SKILL.md +342 -0
  99. package/.agent/skills/gsap-timeline/SKILL.md +153 -0
  100. package/.agent/skills/gsap-utils/SKILL.md +330 -0
  101. package/.agent/skills/i18n-localization/SKILL.md +42 -0
  102. package/.agent/skills/intelligent-routing/SKILL.md +72 -1
  103. package/.agent/skills/lint-and-validate/SKILL.md +42 -0
  104. package/.agent/skills/llm-engineering/SKILL.md +42 -0
  105. package/.agent/skills/local-first/SKILL.md +42 -0
  106. package/.agent/skills/mcp-builder/SKILL.md +42 -0
  107. package/.agent/skills/mobile-design/SKILL.md +42 -0
  108. package/.agent/skills/monorepo-management/SKILL.md +326 -0
  109. package/.agent/skills/motion-engineering/SKILL.md +42 -0
  110. package/.agent/skills/nextjs-react-expert/SKILL.md +42 -0
  111. package/.agent/skills/nodejs-best-practices/SKILL.md +42 -0
  112. package/.agent/skills/observability/SKILL.md +42 -0
  113. package/.agent/skills/parallel-agents/SKILL.md +42 -0
  114. package/.agent/skills/performance-profiling/SKILL.md +42 -0
  115. package/.agent/skills/plan-writing/SKILL.md +42 -0
  116. package/.agent/skills/platform-engineer/SKILL.md +42 -0
  117. package/.agent/skills/playwright-best-practices/SKILL.md +42 -0
  118. package/.agent/skills/powershell-windows/SKILL.md +42 -0
  119. package/.agent/skills/project-idioms/SKILL.md +42 -0
  120. package/.agent/skills/python-patterns/SKILL.md +42 -0
  121. package/.agent/skills/python-pro/SKILL.md +42 -0
  122. package/.agent/skills/react-specialist/SKILL.md +42 -0
  123. package/.agent/skills/readme-builder/SKILL.md +42 -0
  124. package/.agent/skills/realtime-patterns/SKILL.md +42 -0
  125. package/.agent/skills/red-team-tactics/SKILL.md +42 -0
  126. package/.agent/skills/rust-pro/SKILL.md +42 -0
  127. package/.agent/skills/seo-fundamentals/SKILL.md +42 -0
  128. package/.agent/skills/server-management/SKILL.md +42 -0
  129. package/.agent/skills/shadcn-ui-expert/SKILL.md +42 -0
  130. package/.agent/skills/skill-creator/SKILL.md +42 -0
  131. package/.agent/skills/sql-pro/SKILL.md +42 -0
  132. package/.agent/skills/supabase-postgres-best-practices/SKILL.md +42 -0
  133. package/.agent/skills/swiftui-expert/SKILL.md +42 -0
  134. package/.agent/skills/systematic-debugging/SKILL.md +42 -0
  135. package/.agent/skills/tailwind-patterns/SKILL.md +42 -0
  136. package/.agent/skills/tdd-workflow/SKILL.md +42 -0
  137. package/.agent/skills/test-result-analyzer/SKILL.md +42 -0
  138. package/.agent/skills/testing-patterns/SKILL.md +42 -0
  139. package/.agent/skills/trend-researcher/SKILL.md +42 -0
  140. package/.agent/skills/typescript-advanced/SKILL.md +327 -0
  141. package/.agent/skills/ui-ux-pro-max/SKILL.md +42 -0
  142. package/.agent/skills/ui-ux-researcher/SKILL.md +42 -0
  143. package/.agent/skills/vue-expert/SKILL.md +42 -0
  144. package/.agent/skills/vulnerability-scanner/SKILL.md +42 -0
  145. package/.agent/skills/web-accessibility-auditor/SKILL.md +42 -0
  146. package/.agent/skills/web-design-guidelines/SKILL.md +42 -0
  147. package/.agent/skills/webapp-testing/SKILL.md +42 -0
  148. package/.agent/skills/whimsy-injector/SKILL.md +42 -0
  149. package/.agent/skills/workflow-optimizer/SKILL.md +42 -0
  150. package/.agent/workflows/audit.md +6 -6
  151. package/.agent/workflows/deploy.md +1 -1
  152. package/.agent/workflows/generate.md +23 -6
  153. package/.agent/workflows/session.md +5 -5
  154. package/.agent/workflows/swarm.md +2 -2
  155. package/.agent/workflows/tribunal-backend.md +13 -2
  156. package/.agent/workflows/tribunal-full.md +15 -8
  157. package/.agent/workflows/tribunal-speed.md +183 -0
  158. package/README.md +64 -8
  159. package/bin/tribunal-kit.js +281 -41
  160. package/package.json +9 -6
  161. package/scripts/changelog.js +167 -0
  162. package/scripts/sync-version.js +81 -0
  163. package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
  164. package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
  165. package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
  166. package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
  167. package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
  168. package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
  169. package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
  170. package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
  171. package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
  172. package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
  173. package/.agent/scripts/auto_preview.py +0 -180
  174. package/.agent/scripts/bundle_analyzer.py +0 -259
  175. package/.agent/scripts/case_law_manager.py +0 -525
  176. package/.agent/scripts/checklist.py +0 -209
  177. package/.agent/scripts/compress_skills.py +0 -167
  178. package/.agent/scripts/consolidate_skills.py +0 -173
  179. package/.agent/scripts/deep_compress.py +0 -202
  180. package/.agent/scripts/dependency_analyzer.py +0 -247
  181. package/.agent/scripts/lint_runner.py +0 -188
  182. package/.agent/scripts/minify_context.py +0 -80
  183. package/.agent/scripts/patch_skills_meta.py +0 -177
  184. package/.agent/scripts/patch_skills_output.py +0 -285
  185. package/.agent/scripts/schema_validator.py +0 -279
  186. package/.agent/scripts/security_scan.py +0 -224
  187. package/.agent/scripts/session_manager.py +0 -261
  188. package/.agent/scripts/skill_evolution.py +0 -563
  189. package/.agent/scripts/skill_integrator.py +0 -234
  190. package/.agent/scripts/strengthen_skills.py +0 -220
  191. package/.agent/scripts/strip_tribunal.py +0 -41
  192. package/.agent/scripts/swarm_dispatcher.py +0 -350
  193. package/.agent/scripts/test_runner.py +0 -192
  194. package/.agent/scripts/test_swarm_dispatcher.py +0 -163
  195. package/.agent/scripts/verify_all.py +0 -195
  196. package/.agent/skills/gsap-expert/SKILL.md +0 -194
@@ -1,525 +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 re
26
- from pathlib import Path
27
- from datetime import datetime
28
-
29
- # ── Colours ──────────────────────────────────────────────────────────────────
30
- GREEN = "\033[92m"
31
- YELLOW = "\033[93m"
32
- CYAN = "\033[96m"
33
- RED = "\033[91m"
34
- BLUE = "\033[94m"
35
- BOLD = "\033[1m"
36
- DIM = "\033[2m"
37
- RESET = "\033[0m"
38
-
39
- # ── Paths ─────────────────────────────────────────────────────────────────────
40
- def find_agent_dir() -> Path:
41
- """Walk up until we find .agent/"""
42
- current = Path.cwd()
43
- while current != current.parent:
44
- candidate = current / ".agent"
45
- if candidate.is_dir():
46
- return candidate
47
- current = current.parent
48
-
49
- print("\033[91m✖ Error: '.agent' directory not found. Please run 'npx tribunal-kit init' first.\033[0m")
50
- sys.exit(1)
51
-
52
- AGENT_DIR = find_agent_dir()
53
- HISTORY_DIR = AGENT_DIR / "history" / "case-law"
54
- CASES_DIR = HISTORY_DIR / "cases"
55
- INDEX_FILE = HISTORY_DIR / "index.json"
56
-
57
- VALID_DOMAINS = {
58
- "backend", "frontend", "database", "security",
59
- "performance", "mobile", "testing", "devops", "general"
60
- }
61
-
62
- VALID_VERDICTS = {"REJECTED", "APPROVED_WITH_CONDITIONS", "PRECEDENT_SET"}
63
-
64
- # ── Trivial-change filter (Semantic Delta) ────────────────────────────────────
65
- TRIVIAL_PATTERNS = [
66
- r"^\s*$", # blank lines
67
- r"^\s*//.*$", # comment-only lines
68
- r"^\s*#.*$", # python comments
69
- r"^\s*\*.*$", # JSDoc lines
70
- r"^[\+\-]\s*(import\s+\{[^}]+\}|from\s+['\"])", # import reorders
71
- ]
72
-
73
- def is_trivial_line(line: str) -> bool:
74
- for pattern in TRIVIAL_PATTERNS:
75
- if re.match(pattern, line):
76
- return True
77
- return False
78
-
79
- def semantic_delta(diff_text: str) -> str:
80
- """
81
- Strip trivial changes from a diff and return only the meaningful
82
- architectural delta. This is the core token-saving mechanism:
83
- 80% of whitespace/comment/import-order noise is removed before
84
- any LLM sees the content.
85
- """
86
- lines = diff_text.splitlines()
87
- meaningful = []
88
- for line in lines:
89
- if line.startswith(("+++", "---", "@@")):
90
- meaningful.append(line)
91
- continue
92
- if line.startswith(("+", "-")):
93
- code_part = line[1:]
94
- if not is_trivial_line(code_part):
95
- meaningful.append(line)
96
- else:
97
- meaningful.append(line)
98
-
99
- filtered = "\n".join(meaningful)
100
- # Collapse runs of 3+ blank context lines into separator
101
- filtered = re.sub(r"(\n[ ]{0,1}\n){3,}", "\n\n", filtered)
102
- return filtered.strip()
103
-
104
- def content_hash(text: str) -> str:
105
- """Stable 8-char fingerprint of meaningful content."""
106
- cleaned = semantic_delta(text)
107
- return hashlib.sha256(cleaned.encode()).hexdigest()[:8]
108
-
109
- # ── Index helpers ─────────────────────────────────────────────────────────────
110
- def load_index() -> dict:
111
- HISTORY_DIR.mkdir(parents=True, exist_ok=True)
112
- CASES_DIR.mkdir(parents=True, exist_ok=True)
113
- if INDEX_FILE.exists():
114
- try:
115
- return json.loads(INDEX_FILE.read_text(encoding="utf-8"))
116
- except (json.JSONDecodeError, IOError):
117
- pass
118
- return {"version": "1.0", "cases": [], "next_id": 1}
119
-
120
- def save_index(index: dict) -> None:
121
- HISTORY_DIR.mkdir(parents=True, exist_ok=True)
122
- INDEX_FILE.write_text(json.dumps(index, indent=2), encoding="utf-8")
123
-
124
- def load_case(case_id: int) -> dict | None:
125
- path = CASES_DIR / f"case-{case_id:04d}.json"
126
- if not path.exists():
127
- return None
128
- try:
129
- return json.loads(path.read_text(encoding="utf-8"))
130
- except Exception:
131
- return None
132
-
133
- def save_case(case: dict) -> None:
134
- path = CASES_DIR / f"case-{case['id']:04d}.json"
135
- path.write_text(json.dumps(case, indent=2), encoding="utf-8")
136
-
137
- # ── Keyword/tag extraction ─────────────────────────────────────────────────────
138
- def extract_tags(text: str) -> list[str]:
139
- """Extract meaningful code-level keywords from diff/reason text."""
140
- # Pull identifiers: camelCase, snake_case, method calls
141
- tokens = re.findall(r"\b[a-zA-Z_][a-zA-Z0-9_]{2,}\b", text)
142
- stop_words = {
143
- "the", "and", "for", "was", "this", "with", "that",
144
- "from", "are", "not", "use", "but", "also", "code",
145
- "have", "will", "should", "must", "can", "may", "any",
146
- "all", "new", "old", "add", "get", "set", "var", "let",
147
- "const", "function", "return", "import", "export", "class",
148
- "async", "await", "true", "false", "null", "undefined"
149
- }
150
- seen = set()
151
- tags = []
152
- for token in tokens:
153
- lower = token.lower()
154
- if lower not in stop_words and lower not in seen:
155
- seen.add(lower)
156
- tags.append(lower)
157
- if len(tags) >= 20:
158
- break
159
- return tags
160
-
161
- # ── Similarity scoring ────────────────────────────────────────────────────────
162
- def jaccard_similarity(tags_a: list[str], tags_b: list[str]) -> float:
163
- """Simple token overlap — no LLM required."""
164
- if not tags_a or not tags_b:
165
- return 0.0
166
- set_a, set_b = set(tags_a), set(tags_b)
167
- return len(set_a & set_b) / len(set_a | set_b)
168
-
169
- # ── Commands ──────────────────────────────────────────────────────────────────
170
- def cmd_add_case(args: list[str]) -> None:
171
- """
172
- Interactive case recording. All fields collected via prompts so the
173
- agent can call this without any arguments — the script does the rest.
174
- """
175
- print(f"\n{BOLD}{CYAN}━━━ Recording New Case ━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
176
-
177
- # ── Gather fields ─────
178
- diff_text = prompt_multiline("Paste the REJECTED diff (code snippet):", "END_DIFF")
179
- if not diff_text.strip():
180
- print(f"{RED}✖ Diff cannot be empty. Aborting.{RESET}")
181
- sys.exit(1)
182
-
183
- reason = prompt_line("Rejection reason (1-2 sentences):")
184
- if not reason.strip():
185
- print(f"{RED}✖ Reason cannot be empty. Aborting.{RESET}")
186
- sys.exit(1)
187
-
188
- domain = prompt_choice(
189
- "Domain",
190
- sorted(VALID_DOMAINS),
191
- default="general"
192
- )
193
-
194
- verdict = prompt_choice(
195
- "Verdict",
196
- sorted(VALID_VERDICTS),
197
- default="REJECTED"
198
- )
199
-
200
- pr_ref = prompt_line("PR / commit reference (optional, e.g. PR-404):").strip() or None
201
- reviewer = prompt_line("Reviewer agent (optional, e.g. security-auditor):").strip() or None
202
-
203
- # ── Build delta ──────
204
- delta = semantic_delta(diff_text)
205
- fingerprint = content_hash(diff_text)
206
- tags = extract_tags(diff_text + " " + reason)
207
-
208
- # ── Persist ──────────
209
- index = load_index()
210
- case_id = index["next_id"]
211
-
212
- case = {
213
- "id": case_id,
214
- "fingerprint": fingerprint,
215
- "timestamp": datetime.now().isoformat(timespec="seconds"),
216
- "domain": domain,
217
- "verdict": verdict,
218
- "reason": reason.strip(),
219
- "pr_ref": pr_ref,
220
- "reviewer": reviewer,
221
- "tags": tags,
222
- "diff_raw": diff_text.strip(),
223
- "diff_delta": delta
224
- }
225
-
226
- save_case(case)
227
-
228
- # Update index
229
- index["cases"].append({
230
- "id": case_id,
231
- "fingerprint": fingerprint,
232
- "domain": domain,
233
- "verdict": verdict,
234
- "tags": tags,
235
- "timestamp": case["timestamp"],
236
- "reason_summary": reason.strip()[:120]
237
- })
238
- index["next_id"] = case_id + 1
239
- save_index(index)
240
-
241
- print(f"\n{GREEN}✔ Case #{case_id:04d} recorded{RESET}")
242
- print(f" {DIM}Fingerprint : {fingerprint}{RESET}")
243
- print(f" {DIM}Domain : {domain}{RESET}")
244
- print(f" {DIM}Tags : {', '.join(tags[:8])}{RESET}")
245
- print(f" {DIM}Stored at : {CASES_DIR / f'case-{case_id:04d}.json'}{RESET}")
246
- print()
247
-
248
-
249
- def cmd_search_cases(args: list[str]) -> None:
250
- """
251
- Search past cases by query text using Jaccard tag similarity.
252
- Token-free — no LLM call required.
253
- """
254
- query = " ".join(args)
255
- if not query:
256
- # Try reading from --query flag
257
- try:
258
- qi = sys.argv.index("--query")
259
- query = " ".join(sys.argv[qi + 1:])
260
- except (ValueError, IndexError):
261
- pass
262
-
263
- if not query:
264
- print(f"{RED}✖ Provide a search query: search-cases --query \"forEach side effects\"{RESET}")
265
- sys.exit(1)
266
-
267
- query_tags = extract_tags(query)
268
- index = load_index()
269
-
270
- if not index["cases"]:
271
- print(f"{YELLOW}No cases recorded yet. Use 'add-case' to record your first rejection.{RESET}")
272
- return
273
-
274
- # Score every case
275
- scored = []
276
- for entry in index["cases"]:
277
- score = jaccard_similarity(query_tags, entry.get("tags", []))
278
- if score > 0.0:
279
- scored.append((score, entry))
280
-
281
- scored.sort(key=lambda x: x[0], reverse=True)
282
- top = scored[:5]
283
-
284
- if not top:
285
- print(f"{YELLOW}No matching cases found for: \"{query}\"{RESET}")
286
- print(f" {DIM}Try broader terms or check 'list' for available cases.{RESET}")
287
- return
288
-
289
- print(f"\n{BOLD}{CYAN}━━━ Case Law Search Results ━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
290
- print(f" Query : {BOLD}{query}{RESET}")
291
- print(f" Matches: {len(top)} of {len(index['cases'])} cases\n")
292
-
293
- for score, entry in top:
294
- verdict_color = RED if entry["verdict"] == "REJECTED" else YELLOW
295
- print(f" {BOLD}Case #{entry['id']:04d}{RESET} {verdict_color}[{entry['verdict']}]{RESET} "
296
- f"{DIM}{entry['timestamp'][:10]}{RESET} score={score:.2f}")
297
- print(f" {DIM}Domain: {entry['domain']}{RESET}")
298
- print(f" {entry['reason_summary']}")
299
- print(f" {DIM}Tags: {', '.join(entry['tags'][:8])}{RESET}")
300
- print()
301
-
302
- print(f" {DIM}Run 'show --id <N>' to see the full diff for any case.{RESET}")
303
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
304
-
305
-
306
- def cmd_list(args: list[str]) -> None:
307
- index = load_index()
308
- cases = index.get("cases", [])
309
- if not cases:
310
- print(f"{YELLOW}No cases recorded yet.{RESET}")
311
- return
312
-
313
- domain_filter = None
314
- if "--domain" in args:
315
- try:
316
- domain_filter = args[args.index("--domain") + 1].lower()
317
- except IndexError:
318
- pass
319
-
320
- filtered = [c for c in cases if not domain_filter or c["domain"] == domain_filter]
321
- total = len(filtered)
322
-
323
- print(f"\n{BOLD}{CYAN}━━━ Case Law Index ({total} cases) ━━━━━━━━━━━━━━━━━━━━{RESET}")
324
- if domain_filter:
325
- print(f" {DIM}Filtered by domain: {domain_filter}{RESET}\n")
326
-
327
- for entry in reversed(filtered[-20:]):
328
- verdict_color = RED if entry["verdict"] == "REJECTED" else YELLOW
329
- print(f" {BOLD}#{entry['id']:04d}{RESET} "
330
- f"{verdict_color}[{entry['verdict']}]{RESET} "
331
- f"{DIM}{entry['domain'].upper()}{RESET} "
332
- f"{entry['timestamp'][:10]}")
333
- print(f" {entry['reason_summary'][:80]}")
334
-
335
- if total > 20:
336
- print(f"\n {YELLOW}... showing last 20 of {total}. Use 'export' for full history.{RESET}")
337
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
338
-
339
-
340
- def cmd_show(args: list[str]) -> None:
341
- case_id = None
342
- if "--id" in args:
343
- try:
344
- case_id = int(args[args.index("--id") + 1])
345
- except (IndexError, ValueError):
346
- pass
347
-
348
- if case_id is None:
349
- print(f"{RED}✖ Provide a case ID: show --id 7{RESET}")
350
- sys.exit(1)
351
-
352
- case = load_case(case_id)
353
- if not case:
354
- print(f"{RED}✖ Case #{case_id:04d} not found.{RESET}")
355
- sys.exit(1)
356
-
357
- verdict_color = RED if case["verdict"] == "REJECTED" else YELLOW
358
- print(f"\n{BOLD}{CYAN}━━━ Case #{case['id']:04d} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
359
- print(f" Verdict : {verdict_color}{BOLD}{case['verdict']}{RESET}")
360
- print(f" Domain : {case['domain']}")
361
- print(f" Recorded : {case['timestamp']}")
362
- if case.get("pr_ref"):
363
- print(f" PR / Ref : {case['pr_ref']}")
364
- if case.get("reviewer"):
365
- print(f" Reviewer : {case['reviewer']}")
366
- print(f"\n {BOLD}Reason:{RESET}")
367
- print(f" {case['reason']}")
368
- print(f"\n {BOLD}Semantic Delta (meaningful changes only):{RESET}")
369
- print(f" {DIM}─────────────────────────────────────────{RESET}")
370
- for line in case.get("diff_delta", case["diff_raw"]).splitlines()[:40]:
371
- if line.startswith("+"):
372
- print(f" {GREEN}{line}{RESET}")
373
- elif line.startswith("-"):
374
- print(f" {RED}{line}{RESET}")
375
- else:
376
- print(f" {DIM}{line}{RESET}")
377
- print(f"\n {BOLD}Tags:{RESET} {', '.join(case.get('tags', []))}")
378
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
379
-
380
-
381
- def cmd_export(args: list[str]) -> None:
382
- to_stdout = "--stdout" in args
383
- index = load_index()
384
- cases = index.get("cases", [])
385
-
386
- if not cases:
387
- print(f"{YELLOW}No cases to export.{RESET}")
388
- return
389
-
390
- lines = [
391
- "# Tribunal Case Law — Full Export\n",
392
- f"Generated: {datetime.now().isoformat(timespec='seconds')}",
393
- f"Total Cases: {len(cases)}\n",
394
- "---\n"
395
- ]
396
-
397
- for entry in cases:
398
- case = load_case(entry["id"]) or entry
399
- verdict_badge = f"[{case.get('verdict', 'REJECTED')}]"
400
- lines.append(f"## Case #{entry['id']:04d} {verdict_badge}")
401
- lines.append(f"**Domain:** {entry['domain']} ")
402
- lines.append(f"**Recorded:** {entry['timestamp'][:10]} ")
403
- if case.get("pr_ref"):
404
- lines.append(f"**PR/Ref:** {case['pr_ref']} ")
405
- lines.append(f"\n**Reason:** {entry['reason_summary']}\n")
406
- lines.append(f"**Tags:** `{', '.join(entry['tags'][:8])}`\n")
407
- lines.append("---\n")
408
-
409
- content = "\n".join(lines)
410
-
411
- if to_stdout:
412
- print(content)
413
- else:
414
- out_path = HISTORY_DIR / "case-law-export.md"
415
- out_path.write_text(content, encoding="utf-8")
416
- print(f"{GREEN}✔ Exported {len(cases)} cases to {out_path}{RESET}")
417
-
418
-
419
- def cmd_stats(args: list[str]) -> None:
420
- index = load_index()
421
- cases = index.get("cases", [])
422
-
423
- domain_counts: dict[str, int] = {}
424
- verdict_counts: dict[str, int] = {}
425
- for c in cases:
426
- domain_counts[c["domain"]] = domain_counts.get(c["domain"], 0) + 1
427
- verdict_counts[c["verdict"]] = verdict_counts.get(c["verdict"], 0) + 1
428
-
429
- print(f"\n{BOLD}{CYAN}━━━ Case Law Statistics ━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}")
430
- print(f" Total cases: {BOLD}{len(cases)}{RESET}")
431
- print(f"\n {BOLD}By Verdict:{RESET}")
432
- for verdict, count in sorted(verdict_counts.items()):
433
- color = RED if verdict == "REJECTED" else YELLOW
434
- print(f" {color}{verdict:<30}{RESET} {count}")
435
- print(f"\n {BOLD}By Domain:{RESET}")
436
- for domain, count in sorted(domain_counts.items(), key=lambda x: -x[1]):
437
- print(f" {CYAN}{domain:<20}{RESET} {count}")
438
- print(f"{CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━{RESET}\n")
439
-
440
-
441
- # ── Input helpers ─────────────────────────────────────────────────────────────
442
- def prompt_multiline(prompt: str, sentinel: str) -> str:
443
- print(f" {BOLD}{prompt}{RESET}")
444
- print(f" {DIM}(Type or paste content. Type '{sentinel}' on its own line when done.){RESET}")
445
- lines = []
446
- while True:
447
- try:
448
- line = input()
449
- except EOFError:
450
- break
451
- if line.strip() == sentinel:
452
- break
453
- lines.append(line)
454
- return "\n".join(lines)
455
-
456
-
457
- def prompt_line(prompt: str) -> str:
458
- print(f" {BOLD}{prompt}{RESET}", end=" ")
459
- try:
460
- return input()
461
- except EOFError:
462
- return ""
463
-
464
-
465
- def prompt_choice(label: str, choices: list[str], default: str) -> str:
466
- opts = " / ".join(
467
- f"{BOLD}{c}{RESET}" if c == default else c
468
- for c in choices
469
- )
470
- print(f" {BOLD}{label}{RESET} [{opts}] (default: {default}): ", end="")
471
- try:
472
- value = input().strip().lower()
473
- except EOFError:
474
- return default
475
- if not value or value not in choices:
476
- return default
477
- return value
478
-
479
-
480
- # ── Main ──────────────────────────────────────────────────────────────────────
481
- COMMANDS = {
482
- "add-case": cmd_add_case,
483
- "search-cases": cmd_search_cases,
484
- "list": cmd_list,
485
- "show": cmd_show,
486
- "export": cmd_export,
487
- "stats": cmd_stats,
488
- }
489
-
490
- def main() -> None:
491
- # Ensure Unicode output works on Windows terminals
492
- if hasattr(sys.stdout, "reconfigure"):
493
- sys.stdout.reconfigure(encoding="utf-8", errors="replace")
494
-
495
- argv = sys.argv[1:]
496
- if not argv or argv[0] in ("-h", "--help", "help"):
497
- print(f"""
498
- {BOLD}case_law_manager.py{RESET} — Tribunal Case Law Engine
499
-
500
- {BOLD}Commands:{RESET}
501
- add-case Record a new rejected pattern
502
- search-cases --query <text> Find relevant precedents (token-free)
503
- list [--domain <domain>] List all recorded cases
504
- show --id <N> Show full diff for a case
505
- export [--stdout] Export all cases to Markdown
506
- stats Show breakdown by domain/verdict
507
-
508
- {BOLD}Domains:{RESET} {', '.join(sorted(VALID_DOMAINS))}
509
- {BOLD}Verdicts:{RESET} {', '.join(sorted(VALID_VERDICTS))}
510
- """)
511
- return
512
-
513
- cmd = argv[0]
514
- rest = argv[1:]
515
-
516
- if cmd not in COMMANDS:
517
- print(f"{RED}✖ Unknown command: '{cmd}'{RESET}")
518
- print(f" Valid: {', '.join(COMMANDS)}")
519
- sys.exit(1)
520
-
521
- COMMANDS[cmd](rest)
522
-
523
-
524
- if __name__ == "__main__":
525
- main()