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.
- package/.agent/ARCHITECTURE.md +21 -14
- package/.agent/GEMINI.md +4 -2
- package/.agent/agents/api-architect.md +66 -0
- package/.agent/agents/db-latency-auditor.md +216 -0
- package/.agent/agents/precedence-reviewer.md +41 -4
- package/.agent/agents/resilience-reviewer.md +88 -0
- package/.agent/agents/schema-reviewer.md +67 -0
- package/.agent/agents/swarm-worker-contracts.md +5 -5
- package/.agent/agents/throughput-optimizer.md +299 -0
- package/.agent/agents/ui-ux-auditor.md +292 -0
- package/.agent/agents/vitals-reviewer.md +223 -0
- package/.agent/history/case-law/cases/case-0001.json +33 -0
- package/.agent/history/case-law/index.json +35 -0
- package/.agent/rules/GEMINI.md +28 -11
- 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/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 +42 -0
- package/.agent/skills/agentic-patterns/SKILL.md +42 -0
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +42 -0
- package/.agent/skills/api-patterns/SKILL.md +42 -0
- package/.agent/skills/api-security-auditor/SKILL.md +42 -0
- package/.agent/skills/app-builder/SKILL.md +42 -0
- package/.agent/skills/app-builder/templates/SKILL.md +70 -0
- 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 +42 -0
- package/.agent/skills/architecture/SKILL.md +42 -0
- package/.agent/skills/authentication-best-practices/SKILL.md +42 -0
- package/.agent/skills/bash-linux/SKILL.md +42 -0
- package/.agent/skills/behavioral-modes/SKILL.md +42 -0
- package/.agent/skills/brainstorming/SKILL.md +42 -0
- package/.agent/skills/building-native-ui/SKILL.md +42 -0
- package/.agent/skills/clean-code/SKILL.md +42 -0
- package/.agent/skills/code-review-checklist/SKILL.md +42 -0
- package/.agent/skills/config-validator/SKILL.md +42 -0
- package/.agent/skills/csharp-developer/SKILL.md +42 -0
- package/.agent/skills/data-validation-schemas/SKILL.md +320 -0
- package/.agent/skills/database-design/SKILL.md +42 -0
- package/.agent/skills/deployment-procedures/SKILL.md +42 -0
- package/.agent/skills/devops-engineer/SKILL.md +42 -0
- package/.agent/skills/devops-incident-responder/SKILL.md +42 -0
- package/.agent/skills/doc.md +1 -1
- package/.agent/skills/documentation-templates/SKILL.md +42 -0
- package/.agent/skills/edge-computing/SKILL.md +42 -0
- package/.agent/skills/error-resilience/SKILL.md +420 -0
- package/.agent/skills/extract-design-system/SKILL.md +42 -0
- package/.agent/skills/framer-motion-expert/SKILL.md +42 -1
- package/.agent/skills/frontend-design/SKILL.md +42 -0
- package/.agent/skills/game-design-expert/SKILL.md +42 -0
- package/.agent/skills/game-engineering-expert/SKILL.md +42 -0
- package/.agent/skills/geo-fundamentals/SKILL.md +42 -0
- package/.agent/skills/github-operations/SKILL.md +42 -0
- package/.agent/skills/gsap-core/SKILL.md +300 -0
- package/.agent/skills/gsap-frameworks/SKILL.md +199 -0
- package/.agent/skills/gsap-performance/SKILL.md +125 -0
- package/.agent/skills/gsap-plugins/SKILL.md +472 -0
- package/.agent/skills/gsap-react/SKILL.md +181 -0
- package/.agent/skills/gsap-scrolltrigger/SKILL.md +342 -0
- package/.agent/skills/gsap-timeline/SKILL.md +153 -0
- package/.agent/skills/gsap-utils/SKILL.md +330 -0
- package/.agent/skills/i18n-localization/SKILL.md +42 -0
- package/.agent/skills/intelligent-routing/SKILL.md +72 -1
- package/.agent/skills/lint-and-validate/SKILL.md +42 -0
- package/.agent/skills/llm-engineering/SKILL.md +42 -0
- package/.agent/skills/local-first/SKILL.md +42 -0
- package/.agent/skills/mcp-builder/SKILL.md +42 -0
- package/.agent/skills/mobile-design/SKILL.md +42 -0
- package/.agent/skills/monorepo-management/SKILL.md +326 -0
- package/.agent/skills/motion-engineering/SKILL.md +42 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +42 -0
- package/.agent/skills/nodejs-best-practices/SKILL.md +42 -0
- package/.agent/skills/observability/SKILL.md +42 -0
- package/.agent/skills/parallel-agents/SKILL.md +42 -0
- package/.agent/skills/performance-profiling/SKILL.md +42 -0
- package/.agent/skills/plan-writing/SKILL.md +42 -0
- package/.agent/skills/platform-engineer/SKILL.md +42 -0
- package/.agent/skills/playwright-best-practices/SKILL.md +42 -0
- package/.agent/skills/powershell-windows/SKILL.md +42 -0
- package/.agent/skills/project-idioms/SKILL.md +42 -0
- package/.agent/skills/python-patterns/SKILL.md +42 -0
- package/.agent/skills/python-pro/SKILL.md +42 -0
- package/.agent/skills/react-specialist/SKILL.md +42 -0
- package/.agent/skills/readme-builder/SKILL.md +42 -0
- package/.agent/skills/realtime-patterns/SKILL.md +42 -0
- package/.agent/skills/red-team-tactics/SKILL.md +42 -0
- package/.agent/skills/rust-pro/SKILL.md +42 -0
- package/.agent/skills/seo-fundamentals/SKILL.md +42 -0
- package/.agent/skills/server-management/SKILL.md +42 -0
- package/.agent/skills/shadcn-ui-expert/SKILL.md +42 -0
- package/.agent/skills/skill-creator/SKILL.md +42 -0
- package/.agent/skills/sql-pro/SKILL.md +42 -0
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +42 -0
- package/.agent/skills/swiftui-expert/SKILL.md +42 -0
- package/.agent/skills/systematic-debugging/SKILL.md +42 -0
- package/.agent/skills/tailwind-patterns/SKILL.md +42 -0
- package/.agent/skills/tdd-workflow/SKILL.md +42 -0
- package/.agent/skills/test-result-analyzer/SKILL.md +42 -0
- package/.agent/skills/testing-patterns/SKILL.md +42 -0
- package/.agent/skills/trend-researcher/SKILL.md +42 -0
- package/.agent/skills/typescript-advanced/SKILL.md +327 -0
- package/.agent/skills/ui-ux-pro-max/SKILL.md +42 -0
- package/.agent/skills/ui-ux-researcher/SKILL.md +42 -0
- package/.agent/skills/vue-expert/SKILL.md +42 -0
- package/.agent/skills/vulnerability-scanner/SKILL.md +42 -0
- package/.agent/skills/web-accessibility-auditor/SKILL.md +42 -0
- package/.agent/skills/web-design-guidelines/SKILL.md +42 -0
- package/.agent/skills/webapp-testing/SKILL.md +42 -0
- package/.agent/skills/whimsy-injector/SKILL.md +42 -0
- package/.agent/skills/workflow-optimizer/SKILL.md +42 -0
- 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/.agent/workflows/tribunal-backend.md +13 -2
- package/.agent/workflows/tribunal-full.md +15 -8
- package/.agent/workflows/tribunal-speed.md +183 -0
- package/README.md +64 -8
- package/bin/tribunal-kit.js +281 -41
- package/package.json +9 -6
- package/scripts/changelog.js +167 -0
- package/scripts/sync-version.js +81 -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 -525
- 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
- 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()
|