From c54bca4c21fb90cb10d982c8fefb7081864ac6a6 Mon Sep 17 00:00:00 2001 From: Renn F Date: Mon, 22 Jun 2026 14:31:41 +0200 Subject: [PATCH] =?UTF-8?q?feat(panel):=20full=20Conventions=20editor=20?= =?UTF-8?q?=E2=80=94=20manage=20modules,=20rules,=20custom=20rules,=20waiv?= =?UTF-8?q?ers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Conventions tab was read-mostly: it listed modules and toggled rule levels, but you could not add a module, a custom rule, or a waiver from the UI — you had to hand-edit YAML, which defeated the point of a managed standard. It is now a real editor: add / edit / remove module boundaries (with click-to-toggle forbidden kinds), add / edit / remove custom regex rules and their level, and add / edit / remove waivers (path + rule + reason). Saving commits the edited map back to the repo via PR, the same as before. --- .../conventions/conventions-tab.tsx | 210 ++++++++++++++++-- 1 file changed, 195 insertions(+), 15 deletions(-) diff --git a/panel/src/components/conventions/conventions-tab.tsx b/panel/src/components/conventions/conventions-tab.tsx index 97742f88..2de0278f 100644 --- a/panel/src/components/conventions/conventions-tab.tsx +++ b/panel/src/components/conventions/conventions-tab.tsx @@ -5,7 +5,11 @@ import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import { conventionsApi, type ConventionsActionResult, + type ConventionsCustomRule, + type ConventionsModule, type ConventionsStandard, + type ConventionsWaiver, + type DefinitionKind, type RuleLevel, } from "@/lib/api/conventions"; import { @@ -17,9 +21,18 @@ import { } from "@/components/ui/card"; import { Badge } from "@/components/ui/badge"; import { Button } from "@/components/ui/button"; +import { Input } from "@/components/ui/input"; import { Switch } from "@/components/ui/switch"; import { toast } from "sonner"; +const FORBIDDABLE_KINDS: DefinitionKind[] = [ + "model", + "route", + "helper", + "business_logic", + "component", +]; + function actionToast(verb: string, result: ConventionsActionResult): void { if (result.created && result.pr_number != null) { toast.success(`${verb}: opened PR #${result.pr_number} on ${result.branch}`); @@ -87,11 +100,50 @@ export function ConventionsTab({ projectId }: { projectId: string }) { ); } + const edit = (next: Partial) => + setDraft({ ...standard, ...next }); + const setRuleLevel = (name: string, level: RuleLevel) => - setDraft({ - ...standard, - rules: { ...standard.rules, [name]: { name, level } }, + edit({ rules: { ...standard.rules, [name]: { name, level } } }); + + const updateModule = (index: number, next: Partial) => + edit({ + modules: standard.modules.map((m, i) => (i === index ? { ...m, ...next } : m)), }); + const addModule = () => + edit({ modules: [...standard.modules, { path: "", purpose: "", forbidden: [] }] }); + const removeModule = (index: number) => + edit({ modules: standard.modules.filter((_, i) => i !== index) }); + const toggleForbidden = (index: number, kind: DefinitionKind) => { + const current = standard.modules[index].forbidden; + const forbidden = current.includes(kind) + ? current.filter((k) => k !== kind) + : [...current, kind]; + updateModule(index, { forbidden }); + }; + + const updateCustom = (index: number, next: Partial) => + edit({ + custom: standard.custom.map((c, i) => (i === index ? { ...c, ...next } : c)), + }); + const addCustom = () => + edit({ + custom: [ + ...standard.custom, + { id: "", pattern: "", message: "", level: "warn", languages: [] }, + ], + }); + const removeCustom = (index: number) => + edit({ custom: standard.custom.filter((_, i) => i !== index) }); + + const updateWaiver = (index: number, next: Partial) => + edit({ + waivers: standard.waivers.map((w, i) => (i === index ? { ...w, ...next } : w)), + }); + const addWaiver = () => + edit({ waivers: [...standard.waivers, { path: "", rule: "", reason: "" }] }); + const removeWaiver = (index: number) => + edit({ waivers: standard.waivers.filter((_, i) => i !== index) }); const status = data.health.status; // "degraded" is the only problem state: a committed file that won't parse. @@ -133,31 +185,57 @@ export function ConventionsTab({ projectId }: { projectId: string }) { Module boundaries - Which definition kinds are forbidden in each module. + Which definition kinds are forbidden in each module. Click a kind to + toggle it. - + {standard.modules.length === 0 && (

No modules mapped yet.

)} - {standard.modules.map((module) => ( + {standard.modules.map((module, index) => (
-
- {module.path}{" "} - — {module.purpose} +
+ updateModule(index, { path: e.target.value })} + /> +
-
- {module.forbidden.map((kind) => ( - + updateModule(index, { purpose: e.target.value })} + /> +
+ {FORBIDDABLE_KINDS.map((kind) => ( + toggleForbidden(index, kind)} + > no {kind} ))}
))} + @@ -191,6 +269,106 @@ export function ConventionsTab({ projectId }: { projectId: string }) { + + + Custom rules + + Project-specific regex rules — a pattern, a message, and a level. + + + + {standard.custom.map((rule, index) => ( +
+
+ updateCustom(index, { id: e.target.value })} + /> + + {rule.level} + + + updateCustom(index, { level: checked ? "block" : "warn" }) + } + /> + +
+ updateCustom(index, { pattern: e.target.value })} + /> + updateCustom(index, { message: e.target.value })} + /> +
+ ))} + +
+
+ + + + Waivers + + Accountable escapes — exempt a file from a rule with a reason + (reviewed in the PR, never a silent in-code suppression). + + + + {standard.waivers.map((waiver, index) => ( +
+
+ updateWaiver(index, { path: e.target.value })} + /> + updateWaiver(index, { rule: e.target.value })} + /> + +
+ updateWaiver(index, { reason: e.target.value })} + /> +
+ ))} + +
+
+ Recent violations @@ -215,7 +393,9 @@ export function ConventionsTab({ projectId }: { projectId: string }) { {" "} {finding.message}
- + {finding.rule}