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.
- package/.agent/ARCHITECTURE.md +21 -14
- package/.agent/agents/swarm-worker-contracts.md +5 -5
- package/.agent/agents/ui-ux-auditor.md +292 -0
- package/.agent/rules/GEMINI.md +8 -8
- package/.agent/scripts/__pycache__/_colors.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/_utils.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/case_law_manager.cpython-311.pyc +0 -0
- package/.agent/scripts/_colors.js +18 -0
- package/.agent/scripts/_utils.js +42 -0
- package/.agent/scripts/auto_preview.js +197 -0
- package/.agent/scripts/bundle_analyzer.js +290 -0
- package/.agent/scripts/case_law_manager.js +684 -0
- package/.agent/scripts/checklist.js +266 -0
- package/.agent/scripts/colors.js +17 -0
- package/.agent/scripts/compress_skills.js +141 -0
- package/.agent/scripts/consolidate_skills.js +149 -0
- package/.agent/scripts/context_broker.js +609 -0
- package/.agent/scripts/deep_compress.js +150 -0
- package/.agent/scripts/dependency_analyzer.js +272 -0
- package/.agent/scripts/graph_builder.js +199 -0
- package/.agent/scripts/graph_zoom.js +154 -0
- package/.agent/scripts/inner_loop_validator.js +465 -0
- package/.agent/scripts/lint_runner.js +187 -0
- package/.agent/scripts/minify_context.js +100 -0
- package/.agent/scripts/patch_skills_meta.js +156 -0
- package/.agent/scripts/patch_skills_output.js +244 -0
- package/.agent/scripts/schema_validator.js +297 -0
- package/.agent/scripts/security_scan.js +303 -0
- package/.agent/scripts/session_manager.js +276 -0
- package/.agent/scripts/skill_evolution.js +644 -0
- package/.agent/scripts/skill_integrator.js +313 -0
- package/.agent/scripts/strengthen_skills.js +193 -0
- package/.agent/scripts/strip_tribunal.js +47 -0
- package/.agent/scripts/swarm_dispatcher.js +360 -0
- package/.agent/scripts/test_runner.js +193 -0
- package/.agent/scripts/utils.js +32 -0
- package/.agent/scripts/verify_all.js +256 -0
- package/.agent/skills/agent-organizer/SKILL.md +12 -4
- package/.agent/skills/agentic-patterns/SKILL.md +12 -4
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +12 -4
- package/.agent/skills/api-patterns/SKILL.md +209 -201
- package/.agent/skills/api-security-auditor/SKILL.md +12 -4
- package/.agent/skills/app-builder/SKILL.md +12 -4
- package/.agent/skills/app-builder/templates/SKILL.md +76 -68
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +1 -1
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +1 -1
- package/.agent/skills/appflow-wireframe/SKILL.md +12 -4
- package/.agent/skills/architecture/SKILL.md +12 -4
- package/.agent/skills/authentication-best-practices/SKILL.md +12 -4
- package/.agent/skills/bash-linux/SKILL.md +12 -4
- package/.agent/skills/behavioral-modes/SKILL.md +12 -4
- package/.agent/skills/brainstorming/SKILL.md +12 -4
- package/.agent/skills/building-native-ui/SKILL.md +12 -4
- package/.agent/skills/clean-code/SKILL.md +12 -4
- package/.agent/skills/code-review-checklist/SKILL.md +12 -4
- package/.agent/skills/config-validator/SKILL.md +12 -4
- package/.agent/skills/csharp-developer/SKILL.md +12 -4
- package/.agent/skills/data-validation-schemas/SKILL.md +290 -282
- package/.agent/skills/database-design/SKILL.md +202 -194
- package/.agent/skills/deployment-procedures/SKILL.md +12 -4
- package/.agent/skills/devops-engineer/SKILL.md +12 -4
- package/.agent/skills/devops-incident-responder/SKILL.md +12 -4
- package/.agent/skills/doc.md +1 -1
- package/.agent/skills/documentation-templates/SKILL.md +12 -4
- package/.agent/skills/edge-computing/SKILL.md +12 -4
- package/.agent/skills/error-resilience/SKILL.md +390 -382
- package/.agent/skills/extract-design-system/SKILL.md +12 -4
- package/.agent/skills/framer-motion-expert/SKILL.md +206 -199
- package/.agent/skills/frontend-design/SKILL.md +163 -155
- package/.agent/skills/game-design-expert/SKILL.md +12 -4
- package/.agent/skills/game-engineering-expert/SKILL.md +12 -4
- package/.agent/skills/geo-fundamentals/SKILL.md +12 -4
- package/.agent/skills/github-operations/SKILL.md +12 -4
- package/.agent/skills/gsap-core/SKILL.md +54 -48
- package/.agent/skills/gsap-frameworks/SKILL.md +54 -48
- package/.agent/skills/gsap-performance/SKILL.md +54 -48
- package/.agent/skills/gsap-plugins/SKILL.md +54 -48
- package/.agent/skills/gsap-react/SKILL.md +54 -48
- package/.agent/skills/gsap-scrolltrigger/SKILL.md +54 -48
- package/.agent/skills/gsap-timeline/SKILL.md +54 -48
- package/.agent/skills/gsap-utils/SKILL.md +54 -48
- package/.agent/skills/i18n-localization/SKILL.md +12 -4
- package/.agent/skills/intelligent-routing/SKILL.md +41 -33
- package/.agent/skills/knowledge-graph/SKILL.md +36 -0
- package/.agent/skills/lint-and-validate/SKILL.md +12 -4
- package/.agent/skills/llm-engineering/SKILL.md +12 -4
- package/.agent/skills/local-first/SKILL.md +12 -4
- package/.agent/skills/mcp-builder/SKILL.md +12 -4
- package/.agent/skills/mobile-design/SKILL.md +225 -217
- package/.agent/skills/monorepo-management/SKILL.md +296 -288
- package/.agent/skills/motion-engineering/SKILL.md +195 -187
- package/.agent/skills/nextjs-react-expert/SKILL.md +196 -188
- package/.agent/skills/nodejs-best-practices/SKILL.md +12 -4
- package/.agent/skills/observability/SKILL.md +12 -4
- package/.agent/skills/parallel-agents/SKILL.md +12 -4
- package/.agent/skills/performance-profiling/SKILL.md +12 -4
- package/.agent/skills/plan-writing/SKILL.md +12 -4
- package/.agent/skills/platform-engineer/SKILL.md +12 -4
- package/.agent/skills/playwright-best-practices/SKILL.md +12 -4
- package/.agent/skills/powershell-windows/SKILL.md +12 -4
- package/.agent/skills/project-idioms/SKILL.md +12 -4
- package/.agent/skills/python-patterns/SKILL.md +12 -4
- package/.agent/skills/python-pro/SKILL.md +285 -277
- package/.agent/skills/react-specialist/SKILL.md +239 -231
- package/.agent/skills/readme-builder/SKILL.md +12 -4
- package/.agent/skills/realtime-patterns/SKILL.md +12 -4
- package/.agent/skills/red-team-tactics/SKILL.md +12 -4
- package/.agent/skills/rust-pro/SKILL.md +12 -4
- package/.agent/skills/seo-fundamentals/SKILL.md +12 -4
- package/.agent/skills/server-management/SKILL.md +12 -4
- package/.agent/skills/shadcn-ui-expert/SKILL.md +12 -4
- package/.agent/skills/skill-creator/SKILL.md +12 -4
- package/.agent/skills/sql-pro/SKILL.md +12 -4
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +12 -4
- package/.agent/skills/swiftui-expert/SKILL.md +12 -4
- package/.agent/skills/systematic-debugging/SKILL.md +12 -4
- package/.agent/skills/tailwind-patterns/SKILL.md +12 -4
- package/.agent/skills/tdd-workflow/SKILL.md +12 -4
- package/.agent/skills/test-result-analyzer/SKILL.md +12 -4
- package/.agent/skills/testing-patterns/SKILL.md +12 -4
- package/.agent/skills/trend-researcher/SKILL.md +12 -4
- package/.agent/skills/typescript-advanced/SKILL.md +297 -289
- package/.agent/skills/ui-ux-pro-max/SKILL.md +12 -4
- package/.agent/skills/ui-ux-researcher/SKILL.md +12 -4
- package/.agent/skills/vue-expert/SKILL.md +237 -229
- package/.agent/skills/vulnerability-scanner/SKILL.md +12 -4
- package/.agent/skills/web-accessibility-auditor/SKILL.md +12 -4
- package/.agent/skills/web-design-guidelines/SKILL.md +12 -4
- package/.agent/skills/webapp-testing/SKILL.md +12 -4
- package/.agent/skills/whimsy-injector/SKILL.md +12 -4
- package/.agent/skills/workflow-optimizer/SKILL.md +12 -4
- package/.agent/workflows/audit.md +6 -6
- package/.agent/workflows/deploy.md +1 -1
- package/.agent/workflows/generate.md +23 -6
- package/.agent/workflows/session.md +5 -5
- package/.agent/workflows/swarm.md +2 -2
- package/README.md +242 -186
- package/bin/tribunal-kit.js +297 -57
- package/package.json +81 -77
- package/scripts/changelog.js +167 -0
- package/scripts/sync-version.js +81 -0
- package/scripts/validate-payload.js +73 -0
- package/.agent/scripts/__pycache__/auto_preview.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/bundle_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/checklist.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/dependency_analyzer.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/security_scan.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/session_manager.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/skill_integrator.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/swarm_dispatcher.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/test_runner.cpython-311.pyc +0 -0
- package/.agent/scripts/__pycache__/verify_all.cpython-311.pyc +0 -0
- package/.agent/scripts/auto_preview.py +0 -180
- package/.agent/scripts/bundle_analyzer.py +0 -259
- package/.agent/scripts/case_law_manager.py +0 -755
- package/.agent/scripts/checklist.py +0 -209
- package/.agent/scripts/compress_skills.py +0 -167
- package/.agent/scripts/consolidate_skills.py +0 -173
- package/.agent/scripts/deep_compress.py +0 -202
- package/.agent/scripts/dependency_analyzer.py +0 -247
- package/.agent/scripts/lint_runner.py +0 -188
- package/.agent/scripts/minify_context.py +0 -80
- package/.agent/scripts/patch_skills_meta.py +0 -177
- package/.agent/scripts/patch_skills_output.py +0 -285
- package/.agent/scripts/schema_validator.py +0 -279
- package/.agent/scripts/security_scan.py +0 -224
- package/.agent/scripts/session_manager.py +0 -261
- package/.agent/scripts/skill_evolution.py +0 -563
- package/.agent/scripts/skill_integrator.py +0 -234
- package/.agent/scripts/strengthen_skills.py +0 -220
- package/.agent/scripts/strip_tribunal.py +0 -41
- package/.agent/scripts/swarm_dispatcher.py +0 -350
- package/.agent/scripts/test_runner.py +0 -192
- package/.agent/scripts/test_swarm_dispatcher.py +0 -163
- 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()
|