Add methodology settings page with golden ratios, discussion rules, and checklists
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:
2026-04-15 16:30:39 +00:00
parent 5dd24729e2
commit 3288624349
7 changed files with 766 additions and 0 deletions

View File

@@ -0,0 +1,47 @@
"use client";
import { AppShell } from "@/components/app-shell";
import { Card, CardContent } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { GoldenRatiosPanel } from "@/components/methodology/golden-ratios-panel";
import { DiscussionRulesPanel } from "@/components/methodology/discussion-rules-panel";
import { ContentChecklistsPanel } from "@/components/methodology/content-checklists-panel";
export default function MethodologyPage() {
return (
<AppShell>
<section className="space-y-6">
<div>
<h1 className="text-xl font-bold text-navy">מתודולוגיה</h1>
<p className="text-sm text-ink-muted mt-1">
הגדרות ניסוח יחסי אורך, כללי דיון, וצ׳קליסטים לפי סוג ערר
</p>
</div>
<Card className="bg-surface border-rule shadow-sm">
<CardContent className="px-6 py-5">
<Tabs defaultValue="ratios" dir="rtl">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="ratios">יחסי זהב</TabsTrigger>
<TabsTrigger value="rules">כללי דיון</TabsTrigger>
<TabsTrigger value="checklists">צ׳קליסטים</TabsTrigger>
</TabsList>
<TabsContent value="ratios" className="mt-5">
<GoldenRatiosPanel />
</TabsContent>
<TabsContent value="rules" className="mt-5">
<DiscussionRulesPanel />
</TabsContent>
<TabsContent value="checklists" className="mt-5">
<ContentChecklistsPanel />
</TabsContent>
</Tabs>
</CardContent>
</Card>
</section>
</AppShell>
);
}

View File

@@ -25,6 +25,7 @@ type NavItem = {
const NAV_ITEMS: NavItem[] = [ const NAV_ITEMS: NavItem[] = [
{ href: "/", label: "בית" }, { href: "/", label: "בית" },
{ href: "/training", label: "אימון סגנון" }, { href: "/training", label: "אימון סגנון" },
{ href: "/methodology", label: "מתודולוגיה" },
{ href: "/skills", label: "מיומנויות" }, { href: "/skills", label: "מיומנויות" },
{ href: "/diagnostics", label: "אבחון" }, { href: "/diagnostics", label: "אבחון" },
{ href: "/settings", label: "הגדרות" }, { href: "/settings", label: "הגדרות" },

View File

@@ -0,0 +1,181 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import { Markdown } from "@/components/ui/markdown";
import {
useMethodology,
useUpdateMethodology,
useResetMethodology,
} from "@/lib/api/methodology";
import { toast } from "sonner";
import { Save, RotateCcw, Eye, EyeOff, Loader2 } from "lucide-react";
const CHECKLIST_LABELS: Record<string, string> = {
licensing_substantive: "ערר רישוי מהותי",
licensing_threshold: "ערר רישוי סף/סמכות",
licensing_property: "ערר רישוי קנייני",
tama38: "תמ\"א 38",
betterment_levy: "היטל השבחה",
};
const CHECKLIST_ORDER = [
"licensing_substantive",
"licensing_threshold",
"licensing_property",
"tama38",
"betterment_levy",
];
type ChecklistItem = {
key: string;
label: string;
original: string;
draft: string;
isOverride: boolean;
dirty: boolean;
};
export function ContentChecklistsPanel() {
const { data, isLoading } = useMethodology<string>("content_checklists");
const update = useUpdateMethodology("content_checklists");
const reset = useResetMethodology("content_checklists");
const [items, setItems] = useState<ChecklistItem[]>([]);
const [active, setActive] = useState(CHECKLIST_ORDER[0]);
const [preview, setPreview] = useState(false);
useEffect(() => {
if (!data?.items) return;
setItems(
CHECKLIST_ORDER
.filter((k) => k in data.items)
.map((key) => ({
key,
label: CHECKLIST_LABELS[key] ?? key,
original: data.items[key].value,
draft: data.items[key].value,
isOverride: data.items[key].is_override,
dirty: false,
})),
);
}, [data]);
const current = items.find((i) => i.key === active);
const updateDraft = (text: string) => {
setItems((prev) =>
prev.map((i) =>
i.key === active
? { ...i, draft: text, dirty: text !== i.original }
: i,
),
);
};
const handleSave = () => {
if (!current) return;
update.mutate(
{ key: current.key, value: current.draft },
{
onSuccess: () => toast.success(`${current.label} נשמר`),
onError: () => toast.error("שגיאה בשמירה"),
},
);
};
const handleReset = () => {
if (!current) return;
reset.mutate(current.key, {
onSuccess: () => toast.success(`${current.label} אופס`),
onError: () => toast.error("שגיאה באיפוס"),
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12 text-ink-faint">
<Loader2 className="w-5 h-5 animate-spin ml-2" />
טוען...
</div>
);
}
return (
<div className="space-y-4">
{/* Tab selector */}
<div className="flex gap-2 flex-wrap">
{items.map((item) => (
<Button
key={item.key}
size="sm"
variant={active === item.key ? "default" : "outline"}
onClick={() => { setActive(item.key); setPreview(false); }}
className="text-xs"
>
{item.label}
{item.isOverride && (
<Badge variant="secondary" className="text-[9px] mr-1.5 px-1">
מותאם
</Badge>
)}
</Button>
))}
</div>
{/* Editor / Preview */}
{current && (
<Card className="border-rule">
<CardContent className="px-5 py-4 space-y-3">
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-navy">{current.label}</h3>
<Button
size="sm"
variant="ghost"
onClick={() => setPreview(!preview)}
className="text-xs"
>
{preview ? <EyeOff className="w-3.5 h-3.5 ml-1" /> : <Eye className="w-3.5 h-3.5 ml-1" />}
{preview ? "עריכה" : "תצוגה מקדימה"}
</Button>
</div>
{preview ? (
<div className="border border-rule rounded-md p-4 bg-sand-soft/30 max-h-[500px] overflow-y-auto">
<Markdown content={current.draft} />
</div>
) : (
<Textarea
value={current.draft}
onChange={(e) => updateDraft(e.target.value)}
className="min-h-[400px] font-mono text-sm leading-relaxed"
dir="rtl"
/>
)}
<div className="flex items-center gap-2">
<Button size="sm" disabled={!current.dirty || update.isPending} onClick={handleSave}>
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
שמור
</Button>
{current.isOverride && (
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={handleReset}>
<RotateCcw className="w-3 h-3 ml-1" />
איפוס לברירת מחדל
</Button>
)}
<Badge
variant={current.isOverride ? "default" : "secondary"}
className="text-[10px] mr-auto"
>
{current.isOverride ? "מותאם" : "ברירת מחדל"}
</Badge>
</div>
</CardContent>
</Card>
)}
</div>
);
}

View File

@@ -0,0 +1,188 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Textarea } from "@/components/ui/textarea";
import {
useMethodology,
useUpdateMethodology,
useResetMethodology,
} from "@/lib/api/methodology";
import { toast } from "sonner";
import { Save, RotateCcw, Plus, Trash2, Loader2, ChevronDown, ChevronUp } from "lucide-react";
const OUTCOME_LABELS: Record<string, string> = {
universal: "כללי (לכל סוגי התוצאות)",
rejection: "דחייה",
full_acceptance: "קבלה מלאה",
partial_acceptance: "קבלה חלקית",
betterment_levy: "היטל השבחה",
};
type RulesSection = {
key: string;
label: string;
original: string[];
draft: string[];
isOverride: boolean;
dirty: boolean;
expanded: boolean;
};
export function DiscussionRulesPanel() {
const { data, isLoading } = useMethodology<string[]>("discussion_rules");
const update = useUpdateMethodology("discussion_rules");
const reset = useResetMethodology("discussion_rules");
const [sections, setSections] = useState<RulesSection[]>([]);
useEffect(() => {
if (!data?.items) return;
const order = ["universal", "rejection", "partial_acceptance", "full_acceptance", "betterment_levy"];
setSections(
order
.filter((k) => k in data.items)
.map((key) => ({
key,
label: OUTCOME_LABELS[key] ?? key,
original: data.items[key].value,
draft: [...data.items[key].value],
isOverride: data.items[key].is_override,
dirty: false,
expanded: key === "universal",
})),
);
}, [data]);
const toggle = (idx: number) => {
setSections((prev) =>
prev.map((s, i) => (i === idx ? { ...s, expanded: !s.expanded } : s)),
);
};
const updateRule = (sIdx: number, rIdx: number, text: string) => {
setSections((prev) =>
prev.map((s, i) => {
if (i !== sIdx) return s;
const draft = [...s.draft];
draft[rIdx] = text;
return { ...s, draft, dirty: JSON.stringify(draft) !== JSON.stringify(s.original) };
}),
);
};
const addRule = (sIdx: number) => {
setSections((prev) =>
prev.map((s, i) => {
if (i !== sIdx) return s;
const draft = [...s.draft, ""];
return { ...s, draft, dirty: true };
}),
);
};
const removeRule = (sIdx: number, rIdx: number) => {
setSections((prev) =>
prev.map((s, i) => {
if (i !== sIdx) return s;
const draft = s.draft.filter((_, j) => j !== rIdx);
return { ...s, draft, dirty: JSON.stringify(draft) !== JSON.stringify(s.original) };
}),
);
};
const handleSave = (sec: RulesSection) => {
const cleaned = sec.draft.filter((s) => s.trim());
update.mutate(
{ key: sec.key, value: cleaned },
{
onSuccess: () => toast.success(`${sec.label} נשמר`),
onError: () => toast.error("שגיאה בשמירה"),
},
);
};
const handleReset = (sec: RulesSection) => {
reset.mutate(sec.key, {
onSuccess: () => toast.success(`${sec.label} אופס`),
onError: () => toast.error("שגיאה באיפוס"),
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12 text-ink-faint">
<Loader2 className="w-5 h-5 animate-spin ml-2" />
טוען...
</div>
);
}
return (
<div className="space-y-3">
{sections.map((sec, si) => (
<Card key={sec.key} className="border-rule">
<CardContent className="px-5 py-0">
{/* Accordion header */}
<button
className="flex items-center justify-between w-full py-3 text-right"
onClick={() => toggle(si)}
>
<div className="flex items-center gap-2">
<h3 className="text-sm font-semibold text-navy">{sec.label}</h3>
<Badge variant={sec.isOverride ? "default" : "secondary"} className="text-[10px]">
{sec.isOverride ? "מותאם" : "ברירת מחדל"}
</Badge>
<span className="text-[11px] text-ink-faint">{sec.draft.length} כללים</span>
</div>
{sec.expanded ? <ChevronUp className="w-4 h-4 text-ink-faint" /> : <ChevronDown className="w-4 h-4 text-ink-faint" />}
</button>
{/* Expanded content */}
{sec.expanded && (
<div className="pb-4 space-y-2">
{sec.draft.map((rule, ri) => (
<div key={ri} className="flex gap-2">
<Textarea
value={rule}
onChange={(e) => updateRule(si, ri, e.target.value)}
className="min-h-[60px] text-sm flex-1"
dir="rtl"
/>
<Button
size="icon"
variant="ghost"
className="h-8 w-8 text-red-400 hover:text-red-600 flex-shrink-0 mt-1"
onClick={() => removeRule(si, ri)}
>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
))}
<Button size="sm" variant="outline" onClick={() => addRule(si)}>
<Plus className="w-3 h-3 ml-1" />
הוסף כלל
</Button>
<div className="flex items-center gap-2 pt-2 border-t border-rule/50">
<Button size="sm" disabled={!sec.dirty || update.isPending} onClick={() => handleSave(sec)}>
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
שמור
</Button>
{sec.isOverride && (
<Button size="sm" variant="outline" disabled={reset.isPending} onClick={() => handleReset(sec)}>
<RotateCcw className="w-3 h-3 ml-1" />
איפוס
</Button>
)}
</div>
</div>
)}
</CardContent>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,177 @@
"use client";
import { useState, useEffect } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import {
useMethodology,
useUpdateMethodology,
useResetMethodology,
type GoldenRatios,
} from "@/lib/api/methodology";
import { toast } from "sonner";
import { Save, RotateCcw, Loader2 } from "lucide-react";
const OUTCOME_LABELS: Record<string, string> = {
rejection: "דחייה",
full_acceptance: "קבלה מלאה",
partial_acceptance: "קבלה חלקית",
betterment_levy: "היטל השבחה",
};
const SECTION_LABELS: Record<string, string> = {
background: "רקע",
claims: "טענות",
discussion: "דיון",
summary: "סיכום",
};
type RatioCard = {
key: string;
label: string;
original: GoldenRatios;
draft: GoldenRatios;
isOverride: boolean;
dirty: boolean;
};
export function GoldenRatiosPanel() {
const { data, isLoading } = useMethodology<GoldenRatios>("golden_ratios");
const update = useUpdateMethodology("golden_ratios");
const reset = useResetMethodology("golden_ratios");
const [cards, setCards] = useState<RatioCard[]>([]);
// Sync from server
useEffect(() => {
if (!data?.items) return;
setCards(
Object.entries(data.items).map(([key, item]) => ({
key,
label: OUTCOME_LABELS[key] ?? key,
original: item.value,
draft: structuredClone(item.value),
isOverride: item.is_override,
dirty: false,
})),
);
}, [data]);
const setRange = (cardIdx: number, section: string, idx: 0 | 1, val: number) => {
setCards((prev) =>
prev.map((c, i) => {
if (i !== cardIdx) return c;
const next = { ...c, draft: { ...c.draft, [section]: [...c.draft[section]] as [number, number] } };
next.draft[section][idx] = val;
next.dirty = JSON.stringify(next.draft) !== JSON.stringify(next.original);
return next;
}),
);
};
const handleSave = (card: RatioCard) => {
update.mutate(
{ key: card.key, value: card.draft },
{
onSuccess: () => toast.success(`${card.label} נשמר`),
onError: () => toast.error("שגיאה בשמירה"),
},
);
};
const handleReset = (card: RatioCard) => {
reset.mutate(card.key, {
onSuccess: () => toast.success(`${card.label} אופס לברירת מחדל`),
onError: () => toast.error("שגיאה באיפוס"),
});
};
if (isLoading) {
return (
<div className="flex items-center justify-center py-12 text-ink-faint">
<Loader2 className="w-5 h-5 animate-spin ml-2" />
טוען...
</div>
);
}
return (
<div className="grid gap-4 md:grid-cols-2">
{cards.map((card, ci) => (
<Card key={card.key} className="border-rule">
<CardContent className="px-5 py-4 space-y-3">
{/* Header */}
<div className="flex items-center justify-between">
<h3 className="text-sm font-semibold text-navy">{card.label}</h3>
<Badge variant={card.isOverride ? "default" : "secondary"} className="text-[10px]">
{card.isOverride ? "מותאם" : "ברירת מחדל"}
</Badge>
</div>
{/* Table */}
<table className="w-full text-sm">
<thead>
<tr className="text-ink-faint text-[11px]">
<th className="text-right py-1">Section</th>
<th className="text-center py-1">Min %</th>
<th className="text-center py-1">Max %</th>
</tr>
</thead>
<tbody>
{Object.entries(SECTION_LABELS).map(([sec, label]) => (
<tr key={sec} className="border-t border-rule/50">
<td className="py-1.5 text-ink">{label}</td>
<td className="py-1.5 px-1">
<Input
type="number"
min={0}
max={100}
value={card.draft[sec]?.[0] ?? 0}
onChange={(e) => setRange(ci, sec, 0, Number(e.target.value))}
className="h-7 w-16 text-center text-xs mx-auto"
/>
</td>
<td className="py-1.5 px-1">
<Input
type="number"
min={0}
max={100}
value={card.draft[sec]?.[1] ?? 0}
onChange={(e) => setRange(ci, sec, 1, Number(e.target.value))}
className="h-7 w-16 text-center text-xs mx-auto"
/>
</td>
</tr>
))}
</tbody>
</table>
{/* Actions */}
<div className="flex items-center gap-2 pt-1">
<Button
size="sm"
disabled={!card.dirty || update.isPending}
onClick={() => handleSave(card)}
>
{update.isPending ? <Loader2 className="w-3 h-3 animate-spin ml-1" /> : <Save className="w-3 h-3 ml-1" />}
שמור
</Button>
{card.isOverride && (
<Button
size="sm"
variant="outline"
disabled={reset.isPending}
onClick={() => handleReset(card)}
>
<RotateCcw className="w-3 h-3 ml-1" />
איפוס
</Button>
)}
</div>
</CardContent>
</Card>
))}
</div>
);
}

View File

@@ -0,0 +1,71 @@
/**
* Methodology settings hooks — view and edit golden ratios,
* discussion rules, and content checklists.
*/
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { apiRequest } from "./client";
// ── Types ────────────────────────────────────────────────────────
export type MethodologyItem<T = unknown> = {
value: T;
is_override: boolean;
updated_at: string | null;
};
export type MethodologyResponse<T = unknown> = {
items: Record<string, MethodologyItem<T>>;
};
/** Golden ratio per section: [min%, max%] */
export type GoldenRatios = Record<string, [number, number]>;
// ── Query Keys ───────────────────────────────────────────────────
export const methodologyKeys = {
all: ["methodology"] as const,
category: (cat: string) => [...methodologyKeys.all, cat] as const,
};
// ── Hooks ────────────────────────────────────────────────────────
export function useMethodology<T = unknown>(category: string) {
return useQuery({
queryKey: methodologyKeys.category(category),
queryFn: ({ signal }) =>
apiRequest<MethodologyResponse<T>>(
`/api/methodology/${category}`,
{ signal },
),
staleTime: 30_000,
});
}
export function useUpdateMethodology(category: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: ({ key, value }: { key: string; value: unknown }) =>
apiRequest<{ key: string; value: unknown; is_override: boolean }>(
`/api/methodology/${category}/${key}`,
{ method: "PUT", body: { value } },
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: methodologyKeys.category(category) });
},
});
}
export function useResetMethodology(category: string) {
const qc = useQueryClient();
return useMutation({
mutationFn: (key: string) =>
apiRequest<{ key: string; value: unknown; is_override: boolean }>(
`/api/methodology/${category}/${key}`,
{ method: "DELETE" },
),
onSuccess: () => {
qc.invalidateQueries({ queryKey: methodologyKeys.category(category) });
},
});
}

View File

@@ -22,6 +22,7 @@ import zipfile
from fastapi import FastAPI, File, Form, HTTPException, UploadFile from fastapi import FastAPI, File, Form, HTTPException, UploadFile
from fastapi.responses import FileResponse, StreamingResponse from fastapi.responses import FileResponse, StreamingResponse
from typing import Any
from pydantic import BaseModel from pydantic import BaseModel
import asyncpg import asyncpg
@@ -2332,6 +2333,106 @@ async def api_delete_tag_mapping(mapping_id: str):
return {"ok": True} 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 ─────────────────────────────────────────── # ── Skill Management API ───────────────────────────────────────────