Add methodology settings page with golden ratios, discussion rules, and checklists
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 1m29s
New /methodology page with 3 tabs for viewing and editing decision
writing methodology. Uses DB override pattern: hardcoded Python
constants serve as defaults, edits saved to appeal_type_rules table,
delete restores default.
Backend: 3 generic endpoints (GET/PUT/DELETE /api/methodology/{category}/{key})
with validation per category type.
Frontend: methodology.ts hooks, GoldenRatiosPanel (number inputs per
outcome/section), DiscussionRulesPanel (accordion with textarea per
rule), ContentChecklistsPanel (markdown editor with preview toggle).
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
101
web/app.py
101
web/app.py
@@ -22,6 +22,7 @@ import zipfile
|
||||
|
||||
from fastapi import FastAPI, File, Form, HTTPException, UploadFile
|
||||
from fastapi.responses import FileResponse, StreamingResponse
|
||||
from typing import Any
|
||||
from pydantic import BaseModel
|
||||
|
||||
import asyncpg
|
||||
@@ -2332,6 +2333,106 @@ async def api_delete_tag_mapping(mapping_id: str):
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
# ── Methodology Settings ───────────────────────────────────────────
|
||||
|
||||
from legal_mcp.services.lessons import (
|
||||
GOLDEN_RATIOS,
|
||||
DISCUSSION_RULES,
|
||||
CONTENT_CHECKLISTS,
|
||||
)
|
||||
|
||||
_METHODOLOGY_DEFAULTS: dict[str, dict] = {
|
||||
"golden_ratios": {k: {s: list(v) for s, v in sec.items()} for k, sec in GOLDEN_RATIOS.items()},
|
||||
"discussion_rules": dict(DISCUSSION_RULES),
|
||||
"content_checklists": dict(CONTENT_CHECKLISTS),
|
||||
}
|
||||
|
||||
_VALID_CATEGORIES = set(_METHODOLOGY_DEFAULTS.keys())
|
||||
|
||||
|
||||
@app.get("/api/methodology/{category}")
|
||||
async def api_get_methodology(category: str):
|
||||
"""Get methodology settings with DB overrides merged over defaults."""
|
||||
if category not in _VALID_CATEGORIES:
|
||||
raise HTTPException(400, f"Unknown category: {category}. Valid: {sorted(_VALID_CATEGORIES)}")
|
||||
|
||||
defaults = _METHODOLOGY_DEFAULTS[category]
|
||||
pool = await db.get_pool()
|
||||
rows = await pool.fetch(
|
||||
"SELECT rule_key, rule_value, created_at FROM appeal_type_rules "
|
||||
"WHERE appeal_type = '_global' AND rule_category = $1",
|
||||
category,
|
||||
)
|
||||
overrides = {r["rule_key"]: r for r in rows}
|
||||
|
||||
items = {}
|
||||
for key, default_val in defaults.items():
|
||||
if key in overrides:
|
||||
items[key] = {
|
||||
"value": overrides[key]["rule_value"],
|
||||
"is_override": True,
|
||||
"updated_at": overrides[key]["created_at"].isoformat() if overrides[key]["created_at"] else None,
|
||||
}
|
||||
else:
|
||||
items[key] = {"value": default_val, "is_override": False, "updated_at": None}
|
||||
|
||||
return {"items": items}
|
||||
|
||||
|
||||
class MethodologyUpdateRequest(BaseModel):
|
||||
value: Any
|
||||
|
||||
|
||||
@app.put("/api/methodology/{category}/{key}")
|
||||
async def api_update_methodology(category: str, key: str, req: MethodologyUpdateRequest):
|
||||
"""Upsert a methodology override. Validates value shape per category."""
|
||||
if category not in _VALID_CATEGORIES:
|
||||
raise HTTPException(400, f"Unknown category: {category}")
|
||||
if key not in _METHODOLOGY_DEFAULTS[category]:
|
||||
raise HTTPException(400, f"Unknown key '{key}' for category '{category}'")
|
||||
|
||||
# Validate value shape
|
||||
if category == "golden_ratios":
|
||||
if not isinstance(req.value, dict):
|
||||
raise HTTPException(422, "golden_ratios value must be a dict of section → [min, max]")
|
||||
for sec, rng in req.value.items():
|
||||
if not (isinstance(rng, list) and len(rng) == 2 and all(isinstance(x, (int, float)) for x in rng)):
|
||||
raise HTTPException(422, f"Section '{sec}' must be [min, max] (integers 0-100)")
|
||||
elif category == "discussion_rules":
|
||||
if not isinstance(req.value, list) or not all(isinstance(s, str) and s.strip() for s in req.value):
|
||||
raise HTTPException(422, "discussion_rules value must be a list of non-empty strings")
|
||||
elif category == "content_checklists":
|
||||
if not isinstance(req.value, str) or not req.value.strip():
|
||||
raise HTTPException(422, "content_checklists value must be a non-empty string")
|
||||
|
||||
pool = await db.get_pool()
|
||||
await pool.execute(
|
||||
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
|
||||
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::jsonb) "
|
||||
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::jsonb",
|
||||
category, key, json.dumps(req.value, ensure_ascii=False),
|
||||
)
|
||||
|
||||
return {"key": key, "value": req.value, "is_override": True}
|
||||
|
||||
|
||||
@app.delete("/api/methodology/{category}/{key}")
|
||||
async def api_reset_methodology(category: str, key: str):
|
||||
"""Delete methodology override, restoring the hardcoded default."""
|
||||
if category not in _VALID_CATEGORIES:
|
||||
raise HTTPException(400, f"Unknown category: {category}")
|
||||
if key not in _METHODOLOGY_DEFAULTS[category]:
|
||||
raise HTTPException(400, f"Unknown key '{key}' for category '{category}'")
|
||||
|
||||
pool = await db.get_pool()
|
||||
await pool.execute(
|
||||
"DELETE FROM appeal_type_rules WHERE appeal_type = '_global' AND rule_category = $1 AND rule_key = $2",
|
||||
category, key,
|
||||
)
|
||||
|
||||
return {"key": key, "value": _METHODOLOGY_DEFAULTS[category][key], "is_override": False}
|
||||
|
||||
|
||||
# ── Skill Management API ───────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user