tribunal-kit 3.0.0 → 4.0.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 +99 -99
- package/.agent/GEMINI.md +52 -52
- package/.agent/agents/accessibility-reviewer.md +187 -220
- package/.agent/agents/ai-code-reviewer.md +199 -233
- package/.agent/agents/backend-specialist.md +215 -238
- package/.agent/agents/code-archaeologist.md +161 -181
- package/.agent/agents/database-architect.md +184 -207
- package/.agent/agents/debugger.md +191 -218
- package/.agent/agents/dependency-reviewer.md +103 -136
- package/.agent/agents/devops-engineer.md +218 -238
- package/.agent/agents/documentation-writer.md +201 -221
- package/.agent/agents/explorer-agent.md +160 -180
- package/.agent/agents/frontend-reviewer.md +160 -194
- package/.agent/agents/frontend-specialist.md +248 -237
- package/.agent/agents/game-developer.md +48 -52
- package/.agent/agents/logic-reviewer.md +116 -149
- package/.agent/agents/mobile-developer.md +200 -223
- package/.agent/agents/mobile-reviewer.md +162 -195
- package/.agent/agents/orchestrator.md +181 -211
- package/.agent/agents/penetration-tester.md +157 -174
- package/.agent/agents/performance-optimizer.md +183 -203
- package/.agent/agents/performance-reviewer.md +178 -211
- package/.agent/agents/precedence-reviewer.md +213 -0
- package/.agent/agents/product-manager.md +142 -162
- package/.agent/agents/product-owner.md +6 -25
- package/.agent/agents/project-planner.md +142 -162
- package/.agent/agents/qa-automation-engineer.md +225 -242
- package/.agent/agents/security-auditor.md +174 -194
- package/.agent/agents/seo-specialist.md +193 -213
- package/.agent/agents/sql-reviewer.md +161 -194
- package/.agent/agents/supervisor-agent.md +184 -203
- package/.agent/agents/swarm-worker-contracts.md +17 -17
- package/.agent/agents/swarm-worker-registry.md +46 -46
- package/.agent/agents/test-coverage-reviewer.md +160 -193
- package/.agent/agents/test-engineer.md +0 -21
- package/.agent/agents/type-safety-reviewer.md +175 -208
- package/.agent/patterns/generator.md +9 -9
- package/.agent/patterns/inversion.md +12 -12
- package/.agent/patterns/pipeline.md +9 -9
- package/.agent/patterns/reviewer.md +13 -13
- package/.agent/patterns/tool-wrapper.md +9 -9
- package/.agent/rules/GEMINI.md +63 -63
- package/.agent/scripts/append_flow.js +72 -0
- package/.agent/scripts/case_law_manager.py +525 -0
- package/.agent/scripts/compress_skills.py +167 -0
- package/.agent/scripts/consolidate_skills.py +173 -0
- package/.agent/scripts/deep_compress.py +202 -0
- package/.agent/scripts/minify_context.py +80 -0
- package/.agent/scripts/security_scan.py +1 -1
- package/.agent/scripts/skill_evolution.py +563 -0
- package/.agent/scripts/strip_tribunal.py +41 -0
- package/.agent/skills/agent-organizer/SKILL.md +100 -126
- package/.agent/skills/agentic-patterns/SKILL.md +0 -70
- package/.agent/skills/ai-prompt-injection-defense/SKILL.md +134 -160
- package/.agent/skills/api-patterns/SKILL.md +123 -215
- package/.agent/skills/api-security-auditor/SKILL.md +143 -177
- package/.agent/skills/app-builder/SKILL.md +334 -50
- package/.agent/skills/app-builder/templates/SKILL.md +13 -15
- package/.agent/skills/app-builder/templates/astro-static/TEMPLATE.md +16 -16
- package/.agent/skills/app-builder/templates/chrome-extension/TEMPLATE.md +22 -22
- package/.agent/skills/app-builder/templates/cli-tool/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/electron-desktop/TEMPLATE.md +20 -20
- package/.agent/skills/app-builder/templates/express-api/TEMPLATE.md +17 -17
- package/.agent/skills/app-builder/templates/flutter-app/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/monorepo-turborepo/TEMPLATE.md +21 -21
- package/.agent/skills/app-builder/templates/nextjs-fullstack/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/nextjs-saas/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nextjs-static/TEMPLATE.md +26 -26
- package/.agent/skills/app-builder/templates/nuxt-app/TEMPLATE.md +19 -19
- package/.agent/skills/app-builder/templates/python-fastapi/TEMPLATE.md +18 -18
- package/.agent/skills/app-builder/templates/react-native-app/TEMPLATE.md +20 -20
- package/.agent/skills/appflow-wireframe/SKILL.md +95 -121
- package/.agent/skills/architecture/SKILL.md +169 -331
- package/.agent/skills/authentication-best-practices/SKILL.md +139 -173
- package/.agent/skills/bash-linux/SKILL.md +129 -154
- package/.agent/skills/behavioral-modes/SKILL.md +8 -69
- package/.agent/skills/brainstorming/SKILL.md +436 -104
- package/.agent/skills/building-native-ui/SKILL.md +152 -174
- package/.agent/skills/clean-code/SKILL.md +331 -360
- package/.agent/skills/code-review-checklist/SKILL.md +0 -62
- package/.agent/skills/config-validator/SKILL.md +115 -141
- package/.agent/skills/csharp-developer/SKILL.md +468 -528
- package/.agent/skills/database-design/SKILL.md +104 -369
- package/.agent/skills/deployment-procedures/SKILL.md +119 -145
- package/.agent/skills/devops-engineer/SKILL.md +295 -332
- package/.agent/skills/devops-incident-responder/SKILL.md +87 -113
- package/.agent/skills/doc.md +5 -5
- package/.agent/skills/documentation-templates/SKILL.md +27 -63
- package/.agent/skills/edge-computing/SKILL.md +131 -157
- package/.agent/skills/extract-design-system/SKILL.md +108 -134
- package/.agent/skills/framer-motion-expert/SKILL.md +111 -855
- package/.agent/skills/frontend-design/SKILL.md +151 -499
- package/.agent/skills/game-design-expert/SKILL.md +79 -105
- package/.agent/skills/game-engineering-expert/SKILL.md +96 -122
- package/.agent/skills/geo-fundamentals/SKILL.md +97 -124
- package/.agent/skills/github-operations/SKILL.md +279 -314
- package/.agent/skills/gsap-expert/SKILL.md +119 -826
- package/.agent/skills/i18n-localization/SKILL.md +113 -138
- package/.agent/skills/intelligent-routing/SKILL.md +167 -127
- package/.agent/skills/lint-and-validate/SKILL.md +16 -52
- package/.agent/skills/llm-engineering/SKILL.md +344 -357
- package/.agent/skills/local-first/SKILL.md +128 -154
- package/.agent/skills/mcp-builder/SKILL.md +92 -118
- package/.agent/skills/mobile-design/SKILL.md +213 -219
- package/.agent/skills/motion-engineering/SKILL.md +184 -0
- package/.agent/skills/nextjs-react-expert/SKILL.md +99 -698
- package/.agent/skills/nodejs-best-practices/SKILL.md +498 -559
- package/.agent/skills/observability/SKILL.md +293 -330
- package/.agent/skills/parallel-agents/SKILL.md +96 -122
- package/.agent/skills/performance-profiling/SKILL.md +217 -254
- package/.agent/skills/plan-writing/SKILL.md +92 -118
- package/.agent/skills/platform-engineer/SKILL.md +97 -123
- package/.agent/skills/playwright-best-practices/SKILL.md +137 -162
- package/.agent/skills/powershell-windows/SKILL.md +112 -146
- package/.agent/skills/project-idioms/SKILL.md +87 -0
- package/.agent/skills/python-patterns/SKILL.md +15 -35
- package/.agent/skills/python-pro/SKILL.md +148 -754
- package/.agent/skills/react-specialist/SKILL.md +123 -827
- package/.agent/skills/readme-builder/SKILL.md +23 -85
- package/.agent/skills/realtime-patterns/SKILL.md +269 -304
- package/.agent/skills/red-team-tactics/SKILL.md +18 -51
- package/.agent/skills/rust-pro/SKILL.md +623 -701
- package/.agent/skills/seo-fundamentals/SKILL.md +129 -154
- package/.agent/skills/server-management/SKILL.md +164 -190
- package/.agent/skills/shadcn-ui-expert/SKILL.md +181 -206
- package/.agent/skills/skill-creator/SKILL.md +24 -56
- package/.agent/skills/sql-pro/SKILL.md +579 -633
- package/.agent/skills/supabase-postgres-best-practices/SKILL.md +35 -66
- package/.agent/skills/swiftui-expert/SKILL.md +151 -176
- package/.agent/skills/systematic-debugging/SKILL.md +92 -118
- package/.agent/skills/tailwind-patterns/SKILL.md +516 -576
- package/.agent/skills/tdd-workflow/SKILL.md +111 -137
- package/.agent/skills/test-result-analyzer/SKILL.md +33 -73
- package/.agent/skills/testing-patterns/SKILL.md +512 -573
- package/.agent/skills/trend-researcher/SKILL.md +30 -71
- package/.agent/skills/ui-ux-pro-max/SKILL.md +8 -41
- package/.agent/skills/ui-ux-researcher/SKILL.md +51 -91
- package/.agent/skills/vue-expert/SKILL.md +127 -866
- package/.agent/skills/vulnerability-scanner/SKILL.md +354 -269
- package/.agent/skills/web-accessibility-auditor/SKILL.md +168 -193
- package/.agent/skills/web-design-guidelines/SKILL.md +25 -61
- package/.agent/skills/webapp-testing/SKILL.md +119 -145
- package/.agent/skills/whimsy-injector/SKILL.md +58 -132
- package/.agent/skills/workflow-optimizer/SKILL.md +28 -68
- package/.agent/workflows/api-tester.md +151 -151
- package/.agent/workflows/audit.md +127 -138
- package/.agent/workflows/brainstorm.md +110 -110
- package/.agent/workflows/changelog.md +112 -112
- package/.agent/workflows/create.md +124 -124
- package/.agent/workflows/debug.md +165 -189
- package/.agent/workflows/deploy.md +180 -189
- package/.agent/workflows/enhance.md +128 -151
- package/.agent/workflows/fix.md +114 -135
- package/.agent/workflows/generate.md +13 -4
- package/.agent/workflows/migrate.md +160 -160
- package/.agent/workflows/orchestrate.md +168 -168
- package/.agent/workflows/performance-benchmarker.md +114 -123
- package/.agent/workflows/plan.md +173 -173
- package/.agent/workflows/preview.md +80 -80
- package/.agent/workflows/refactor.md +161 -183
- package/.agent/workflows/review-ai.md +101 -129
- package/.agent/workflows/review.md +116 -116
- package/.agent/workflows/session.md +94 -94
- package/.agent/workflows/status.md +79 -79
- package/.agent/workflows/strengthen-skills.md +138 -139
- package/.agent/workflows/swarm.md +179 -179
- package/.agent/workflows/test.md +189 -211
- package/.agent/workflows/tribunal-backend.md +94 -113
- package/.agent/workflows/tribunal-database.md +95 -115
- package/.agent/workflows/tribunal-frontend.md +96 -118
- package/.agent/workflows/tribunal-full.md +93 -133
- package/.agent/workflows/tribunal-mobile.md +95 -119
- package/.agent/workflows/tribunal-performance.md +110 -133
- package/.agent/workflows/ui-ux-pro-max.md +122 -143
- package/README.md +30 -1
- package/bin/tribunal-kit.js +175 -12
- package/package.json +25 -4
- package/.agent/skills/api-patterns/api-style.md +0 -42
- package/.agent/skills/api-patterns/auth.md +0 -24
- package/.agent/skills/api-patterns/documentation.md +0 -26
- package/.agent/skills/api-patterns/graphql.md +0 -41
- package/.agent/skills/api-patterns/rate-limiting.md +0 -31
- package/.agent/skills/api-patterns/response.md +0 -37
- package/.agent/skills/api-patterns/rest.md +0 -40
- package/.agent/skills/api-patterns/security-testing.md +0 -122
- package/.agent/skills/api-patterns/trpc.md +0 -41
- package/.agent/skills/api-patterns/versioning.md +0 -22
- package/.agent/skills/app-builder/agent-coordination.md +0 -71
- package/.agent/skills/app-builder/feature-building.md +0 -53
- package/.agent/skills/app-builder/project-detection.md +0 -34
- package/.agent/skills/app-builder/scaffolding.md +0 -118
- package/.agent/skills/app-builder/tech-stack.md +0 -40
- package/.agent/skills/architecture/context-discovery.md +0 -43
- package/.agent/skills/architecture/examples.md +0 -94
- package/.agent/skills/architecture/pattern-selection.md +0 -68
- package/.agent/skills/architecture/patterns-reference.md +0 -50
- package/.agent/skills/architecture/trade-off-analysis.md +0 -77
- package/.agent/skills/brainstorming/dynamic-questioning.md +0 -360
- package/.agent/skills/database-design/database-selection.md +0 -43
- package/.agent/skills/database-design/indexing.md +0 -39
- package/.agent/skills/database-design/migrations.md +0 -48
- package/.agent/skills/database-design/optimization.md +0 -36
- package/.agent/skills/database-design/orm-selection.md +0 -30
- package/.agent/skills/database-design/schema-design.md +0 -56
- package/.agent/skills/frontend-design/animation-guide.md +0 -331
- package/.agent/skills/frontend-design/color-system.md +0 -329
- package/.agent/skills/frontend-design/decision-trees.md +0 -418
- package/.agent/skills/frontend-design/motion-graphics.md +0 -306
- package/.agent/skills/frontend-design/typography-system.md +0 -363
- package/.agent/skills/frontend-design/ux-psychology.md +0 -1116
- package/.agent/skills/frontend-design/visual-effects.md +0 -383
- package/.agent/skills/intelligent-routing/router-manifest.md +0 -65
- package/.agent/skills/mobile-design/decision-trees.md +0 -516
- package/.agent/skills/mobile-design/mobile-backend.md +0 -491
- package/.agent/skills/mobile-design/mobile-color-system.md +0 -420
- package/.agent/skills/mobile-design/mobile-debugging.md +0 -122
- package/.agent/skills/mobile-design/mobile-design-thinking.md +0 -357
- package/.agent/skills/mobile-design/mobile-navigation.md +0 -458
- package/.agent/skills/mobile-design/mobile-performance.md +0 -767
- package/.agent/skills/mobile-design/mobile-testing.md +0 -356
- package/.agent/skills/mobile-design/mobile-typography.md +0 -433
- package/.agent/skills/mobile-design/platform-android.md +0 -666
- package/.agent/skills/mobile-design/platform-ios.md +0 -561
- package/.agent/skills/mobile-design/touch-psychology.md +0 -537
- package/.agent/skills/nextjs-react-expert/1-async-eliminating-waterfalls.md +0 -312
- package/.agent/skills/nextjs-react-expert/2-bundle-bundle-size-optimization.md +0 -240
- package/.agent/skills/nextjs-react-expert/3-server-server-side-performance.md +0 -490
- package/.agent/skills/nextjs-react-expert/4-client-client-side-data-fetching.md +0 -264
- package/.agent/skills/nextjs-react-expert/5-rerender-re-render-optimization.md +0 -581
- package/.agent/skills/nextjs-react-expert/6-rendering-rendering-performance.md +0 -432
- package/.agent/skills/nextjs-react-expert/7-js-javascript-performance.md +0 -684
- package/.agent/skills/nextjs-react-expert/8-advanced-advanced-patterns.md +0 -150
- package/.agent/skills/vulnerability-scanner/checklists.md +0 -121
|
@@ -0,0 +1,525 @@
|
|
|
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()
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
compress_skills.py - Aggressive token reduction for .agent/skills/**/*.md files
|
|
4
|
+
WHAT IT DOES:
|
|
5
|
+
1. Strips ## 🏛️ Tribunal Integration sections (in GEMINI.md globally)
|
|
6
|
+
2. Strips ## ✅ Pre-Flight Self-Audit sections (duplication)
|
|
7
|
+
3. Strips ## Cross-Workflow Navigation sections
|
|
8
|
+
4. Strips ## Output Format sections
|
|
9
|
+
5. Strips inline comment blocks inside code (// Description of obvious stuff)
|
|
10
|
+
6. Collapses 3+ empty lines → 1
|
|
11
|
+
7. Removes chatty intro paragraphs (patterns like "X is Y. If you Z, you're Z.")
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os, re, sys
|
|
15
|
+
|
|
16
|
+
# ─── Section strippers ──────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
SECTION_PATTERNS = [
|
|
19
|
+
# Tribunal / pre-flight boilerplate (global in GEMINI.md)
|
|
20
|
+
r'(?m)^## 🏛️ Tribunal Integration[\s\S]*?(?=\n## |\n# |\Z)',
|
|
21
|
+
r'(?m)^## Tribunal Integration[\s\S]*?(?=\n## |\n# |\Z)',
|
|
22
|
+
r'(?m)^### ✅ Pre-Flight Self-Audit[\s\S]*?(?=\n## |\n### |\n# |\Z)',
|
|
23
|
+
r'(?m)^## Pre-Flight Self-Audit[\s\S]*?(?=\n## |\n# |\Z)',
|
|
24
|
+
r'(?m)^## Cross-Workflow Navigation[\s\S]*?(?=\n## |\n# |\Z)',
|
|
25
|
+
r'(?m)^## Output Format\s*\n```[\s\S]*?```\s*\n',
|
|
26
|
+
r'(?m)^## VBC Protocol[\s\S]*?(?=\n## |\n# |\Z)',
|
|
27
|
+
r'(?m)^## LLM Traps[\s\S]*?(?=\n## |\n# |\Z)',
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# ─── Chatty intro line patterns ───────────────────────────────────────────
|
|
31
|
+
# e.g. "React 19 is a paradigm shift. Server Components are the default."
|
|
32
|
+
# These are after the frontmatter H1 title — motivational filler.
|
|
33
|
+
CHATTY_INTRO = re.compile(
|
|
34
|
+
r'(?m)^(# .+\n)\n[A-Z][^#\n]{60,}\n[A-Z][^#\n]{40,}\n\n---',
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def strip_chatty_intro(content):
|
|
38
|
+
def replace(m):
|
|
39
|
+
return m.group(1) + '\n---'
|
|
40
|
+
return CHATTY_INTRO.sub(replace, content)
|
|
41
|
+
|
|
42
|
+
# ─── Trim verbose inline comments in code blocks ─────────────────────────
|
|
43
|
+
# Removes "// comment that just restates the surrounding code name"
|
|
44
|
+
OBVIOUS_COMMENT = re.compile(
|
|
45
|
+
r'(?m)^(\s*)(// (default for most properties|shorthand|number of repeats|default: \d+|spring tension|resistance|weight|approximate duration|deceleration rate)[^\n]*)\n'
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def strip_obvious_comments(content):
|
|
49
|
+
return OBVIOUS_COMMENT.sub('', content)
|
|
50
|
+
|
|
51
|
+
# ─── Long repetitive ✅/❌ rule blocks that repeat main section ────────────
|
|
52
|
+
# These are usually "Performance Rules" text blocks restating code above
|
|
53
|
+
PERF_TEXT_BLOCK = re.compile(
|
|
54
|
+
r'(?m)^```\n(✅ Use \w[^\n]*\n → [^\n]*\n\n?){3,}```\n'
|
|
55
|
+
)
|
|
56
|
+
def compress_perf_blocks(content):
|
|
57
|
+
def to_bullets(m):
|
|
58
|
+
text = m.group(0)
|
|
59
|
+
lines = text.strip('`\n').splitlines()
|
|
60
|
+
bullets = []
|
|
61
|
+
for line in lines:
|
|
62
|
+
stripped = line.strip()
|
|
63
|
+
if stripped.startswith('✅') or stripped.startswith('❌'):
|
|
64
|
+
bullets.append(f'- {stripped}')
|
|
65
|
+
elif stripped.startswith('→'):
|
|
66
|
+
bullets[-1] += f' ({stripped[1:].strip()})'
|
|
67
|
+
return '\n'.join(bullets) + '\n'
|
|
68
|
+
return PERF_TEXT_BLOCK.sub(to_bullets, content)
|
|
69
|
+
|
|
70
|
+
# ─── Collapse empty lines ────────────────────────────────────────────────
|
|
71
|
+
def collapse_blanks(content):
|
|
72
|
+
return re.sub(r'\n{3,}', '\n\n', content)
|
|
73
|
+
|
|
74
|
+
# ─── Remove "This is X — not Y" filler sentences before the first ## ─────
|
|
75
|
+
FILLER_BEFORE_SECTION = re.compile(
|
|
76
|
+
r'(?m)(^# .+\n\n)([A-Z][^\n]+\n){1,4}(\n---\n)',
|
|
77
|
+
re.MULTILINE
|
|
78
|
+
)
|
|
79
|
+
def remove_filler_between_title_and_hr(content):
|
|
80
|
+
def replacement(m):
|
|
81
|
+
return m.group(1) + m.group(3)
|
|
82
|
+
return FILLER_BEFORE_SECTION.sub(replacement, content)
|
|
83
|
+
|
|
84
|
+
# ─── Strip redundant version comment banners ────────────────────────────
|
|
85
|
+
# e.g. "// motion.div, motion.span, motion.button, motion.svg, motion.path, etc."
|
|
86
|
+
# "// Any HTML or SVG element can be prefixed with `motion.`" - obvious
|
|
87
|
+
REDUNDANT_NOTE = re.compile(r'(?m)^// (motion\.\w+|Any HTML|Note:|Variant names propagate|// )[^\n]*\n')
|
|
88
|
+
|
|
89
|
+
def strip_redundant_notes(content):
|
|
90
|
+
return REDUNDANT_NOTE.sub('', content)
|
|
91
|
+
|
|
92
|
+
# ─── Main pipeline ────────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
def compress_file(path):
|
|
95
|
+
with open(path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
96
|
+
original = f.read()
|
|
97
|
+
|
|
98
|
+
content = original
|
|
99
|
+
|
|
100
|
+
# 1. Strip global boilerplate sections
|
|
101
|
+
for pattern in SECTION_PATTERNS:
|
|
102
|
+
content = re.sub(pattern, '', content)
|
|
103
|
+
|
|
104
|
+
# 2. Remove chatty intro paragraphs
|
|
105
|
+
content = strip_chatty_intro(content)
|
|
106
|
+
|
|
107
|
+
# 3. Remove filler between title and first ---
|
|
108
|
+
content = remove_filler_between_title_and_hr(content)
|
|
109
|
+
|
|
110
|
+
# 4. Strip obvious inline comments from code
|
|
111
|
+
content = strip_obvious_comments(content)
|
|
112
|
+
|
|
113
|
+
# 5. Strip redundant notes
|
|
114
|
+
content = strip_redundant_notes(content)
|
|
115
|
+
|
|
116
|
+
# 6. Compress verbose perf rule text blocks to bullet lists
|
|
117
|
+
content = compress_perf_blocks(content)
|
|
118
|
+
|
|
119
|
+
# 7. Collapse 3+ blank lines
|
|
120
|
+
content = collapse_blanks(content)
|
|
121
|
+
|
|
122
|
+
# Write back
|
|
123
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
124
|
+
f.write(content.strip() + '\n')
|
|
125
|
+
|
|
126
|
+
saved = len(original) - len(content)
|
|
127
|
+
return len(original), len(content), saved
|
|
128
|
+
|
|
129
|
+
def main():
|
|
130
|
+
base = '.agent/skills'
|
|
131
|
+
if not os.path.exists(base):
|
|
132
|
+
print(f"ERROR: '{base}' not found. Run from tribunal-kit root.", file=sys.stderr)
|
|
133
|
+
sys.exit(1)
|
|
134
|
+
|
|
135
|
+
total_orig = 0
|
|
136
|
+
total_new = 0
|
|
137
|
+
results = []
|
|
138
|
+
|
|
139
|
+
for root, _, files in os.walk(base):
|
|
140
|
+
for fname in files:
|
|
141
|
+
if fname.endswith('.md'):
|
|
142
|
+
path = os.path.join(root, fname)
|
|
143
|
+
orig, new, saved = compress_file(path)
|
|
144
|
+
total_orig += orig
|
|
145
|
+
total_new += new
|
|
146
|
+
if saved > 0:
|
|
147
|
+
results.append((saved, path))
|
|
148
|
+
|
|
149
|
+
results.sort(reverse=True)
|
|
150
|
+
|
|
151
|
+
print(f"\n{'='*55}")
|
|
152
|
+
print(f" Skill Compression Complete")
|
|
153
|
+
print(f"{'='*55}")
|
|
154
|
+
print(f" Original : {total_orig:,} bytes ({total_orig//1024}KB)")
|
|
155
|
+
print(f" After : {total_new:,} bytes ({total_new//1024}KB)")
|
|
156
|
+
saved_total = total_orig - total_new
|
|
157
|
+
pct = saved_total / total_orig * 100 if total_orig else 0
|
|
158
|
+
print(f" Saved : {saved_total:,} bytes ({saved_total//1024}KB) — {pct:.1f}%")
|
|
159
|
+
print(f"\n Top savings:")
|
|
160
|
+
for saved, path in results[:15]:
|
|
161
|
+
skill = os.path.basename(os.path.dirname(path))
|
|
162
|
+
fname = os.path.basename(path)
|
|
163
|
+
print(f" -{saved//1024:2}KB {skill}/{fname}")
|
|
164
|
+
print()
|
|
165
|
+
|
|
166
|
+
if __name__ == '__main__':
|
|
167
|
+
main()
|