feat(status): single source of truth for the case-status model + canonicalize intermediates
Fixes the case where the H1 chip, the pipeline stepper and the manual-changer dropdown disagreed (e.g. 1043-02-26 on analyst_verified): the status sat between the canonical 10 and a legacy bucket, so each surface read a different list. Root fix — ONE authoritative status model + canonicalize the real intermediate states (chair-approved): - New mcp-server/.../case_status_model.py — the single registry: ordered StatusDef list (key/label/description/phase/selectable/terminal/on_enter), the 5 phases, STATUS_ORDER, phase_of/label_of, and a drift assertion that the CaseStatus enum matches it. `on_enter` is the seam for a status to *do* something on entry, declared next to its definition (no dispatcher wired yet). - models.CaseStatus enum: analyst_verified + research_complete promoted to first-class canonical statuses (the agents set them — they belong in the model, not a legacy fallback). - tools/cases.py: the forward-only STATUS_ORDER guard now derives from the registry instead of an inline list. - GET /api/status-model exposes the model so the frontend mirror can be generated against it. - web-ui case-status.ts mirrors it: the two intermediates moved from the legacy map into CASE_STATUSES / PHASES(thinking) / STATUS_LABELS / STATUS_DESCRIPTIONS; status-badge icons+tones extended. So the chip (status), stepper (its phase) and dropdown (now includes it) all agree. Invariants: G2 — collapses the scattered status definitions (enum, inline STATUS_ORDER, two frontend label maps) onto one backend authority + a documented frontend mirror; no parallel status list remains. Agent-prompt alignment + backfill follow in a separate change. py_compile + tsc + eslint clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,8 @@ const STATUS_ICONS: Record<CaseStatus, LucideIcon> = {
|
||||
new: FilePlus2,
|
||||
processing: Loader2,
|
||||
documents_ready: FileCheck,
|
||||
analyst_verified: SearchCheck,
|
||||
research_complete: Compass,
|
||||
outcome_set: Target,
|
||||
direction_approved: Compass,
|
||||
qa_review: SearchCheck,
|
||||
@@ -36,6 +38,8 @@ const STATUS_TONE: Record<CaseStatus, string> = {
|
||||
new: "bg-rule-soft text-ink-muted border-rule",
|
||||
processing: "bg-info-bg text-info border-info/30",
|
||||
documents_ready: "bg-info-bg text-info border-info/40",
|
||||
analyst_verified: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
research_complete: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
outcome_set: "bg-gold-wash text-gold-deep border-gold/40",
|
||||
direction_approved:"bg-gold-wash text-gold-deep border-gold/50",
|
||||
qa_review: "bg-warn-bg text-warn border-warn/40",
|
||||
|
||||
@@ -1,16 +1,19 @@
|
||||
/**
|
||||
* Single source of truth for the case-status lifecycle (UI-B1 / G2).
|
||||
* Frontend mirror of the canonical case-status lifecycle.
|
||||
*
|
||||
* The 17-status manual menu was trimmed to the **10 core statuses** that the
|
||||
* pipeline actually sets or that gate real logic. Decorative mid-stage markers
|
||||
* (uploading, analyst_verified, research_complete, brainstorming,
|
||||
* analysis_enriched, ready_for_writing, drafting) plus the legacy/dead
|
||||
* `in_progress` and `qa_failed` were removed — no pipeline code ever set them.
|
||||
* The ONE source of truth is the backend status model
|
||||
* (`mcp-server/src/legal_mcp/case_status_model.py`), exposed at
|
||||
* `GET /api/status-model`; this file mirrors it (order / labels / phases) so the
|
||||
* UI keeps compile-time `CaseStatus` typing. Keep the two in sync — the backend
|
||||
* registry is authoritative (it also declares per-status `on_enter` actions).
|
||||
*
|
||||
* Every status consumer (badge, changer, timeline, guide, donut, KPI cards,
|
||||
* compose chip) imports the list / labels / phases from here instead of
|
||||
* re-declaring its own array. Keep this file in sync with the backend
|
||||
* STATUS_ORDER in `mcp-server/src/legal_mcp/tools/cases.py`.
|
||||
* The analyst/research intermediate states (`analyst_verified`,
|
||||
* `research_complete`) are first-class canonical statuses — the agents set them,
|
||||
* so the chip, stepper and manual changer all agree instead of the status
|
||||
* falling between the canonical set and a legacy bucket.
|
||||
*
|
||||
* Every status consumer (badge, changer, timeline, guide, donut, KPI cards)
|
||||
* imports the list / labels / phases from here instead of re-declaring its own.
|
||||
*/
|
||||
|
||||
/** Ordered lifecycle — also the order shown in the manual status dropdown. */
|
||||
@@ -18,6 +21,8 @@ export const CASE_STATUSES = [
|
||||
"new",
|
||||
"processing",
|
||||
"documents_ready",
|
||||
"analyst_verified",
|
||||
"research_complete",
|
||||
"outcome_set",
|
||||
"direction_approved",
|
||||
"qa_review",
|
||||
@@ -35,22 +40,18 @@ export type PhaseKey = "intake" | "prep" | "thinking" | "writing" | "done";
|
||||
export const PHASES: { key: PhaseKey; label: string; statuses: CaseStatus[] }[] = [
|
||||
{ key: "intake", label: "קליטה ועיבוד", statuses: ["new", "processing"] },
|
||||
{ key: "prep", label: "הכנת תיק", statuses: ["documents_ready"] },
|
||||
{ key: "thinking", label: "ניתוח וכיוון", statuses: ["outcome_set", "direction_approved"] },
|
||||
{ key: "thinking", label: "ניתוח וכיוון", statuses: ["analyst_verified", "research_complete", "outcome_set", "direction_approved"] },
|
||||
{ key: "writing", label: "כתיבת טיוטה", statuses: ["qa_review", "drafted"] },
|
||||
{ key: "done", label: "סגירה", statuses: ["exported", "reviewed", "final"] },
|
||||
];
|
||||
|
||||
/**
|
||||
* Legacy/intermediate statuses that the trimmed PHASES list dropped but the
|
||||
* Paperclip agents still set (e.g. the analyst writes `analyst_verified`, the
|
||||
* researcher writes `research_complete`). Without this map a case parked on one
|
||||
* of them renders with NO active phase — the pipeline stepper goes blank and the
|
||||
* chair can't see where the case stands. Map each to the phase it belongs to for
|
||||
* display purposes only (these are NOT selectable statuses).
|
||||
* Truly-legacy statuses that are NOT in the canonical set but might still arrive
|
||||
* from an old client / un-migrated row — mapped to a phase for display only, so
|
||||
* the stepper never goes blank. (The analyst/research intermediates are NO
|
||||
* longer here — they are canonical statuses in CASE_STATUSES above.)
|
||||
*/
|
||||
const LEGACY_STATUS_PHASE: Record<string, PhaseKey> = {
|
||||
analyst_verified: "thinking",
|
||||
research_complete: "thinking",
|
||||
analysis_enriched: "thinking",
|
||||
ready_for_writing: "writing",
|
||||
drafting: "writing",
|
||||
@@ -80,6 +81,8 @@ export const STATUS_LABELS: Record<CaseStatus, string> = {
|
||||
new: "חדש",
|
||||
processing: "בעיבוד",
|
||||
documents_ready: "מסמכים מוכנים",
|
||||
analyst_verified: "ניתוח אומת",
|
||||
research_complete: "מחקר הושלם",
|
||||
outcome_set: "תוצאה נקבעה",
|
||||
direction_approved: "כיוון אושר",
|
||||
qa_review: "בדיקת איכות",
|
||||
@@ -97,8 +100,6 @@ export const STATUS_LABELS: Record<CaseStatus, string> = {
|
||||
const LEGACY_STATUS_LABELS: Record<string, string> = {
|
||||
in_progress: "בעבודה",
|
||||
uploading: "מעלה",
|
||||
analyst_verified: "ניתוח אומת",
|
||||
research_complete: "מחקר הושלם",
|
||||
brainstorming: "סיעור מוחות",
|
||||
analysis_enriched: "ניתוח הועמק",
|
||||
ready_for_writing: "מוכן לכתיבה",
|
||||
@@ -124,6 +125,8 @@ export const STATUS_DESCRIPTIONS: Record<CaseStatus, string> = {
|
||||
new: "התיק נוצר וממתין להעלאת מסמכים",
|
||||
processing: "המערכת מעבדת ומנתחת את המסמכים",
|
||||
documents_ready: "כל המסמכים עובדו ומוכנים לעבודה",
|
||||
analyst_verified: "המנתח סיים ואימת את הניתוח — ממתין להכרעת תוצאה",
|
||||
research_complete: "חקר התקדימים הושלם (מסלול נפרד מהמנתח)",
|
||||
outcome_set: "נקבעה תוצאה צפויה לערר",
|
||||
direction_approved: "כיוון ההחלטה אושר — בהעמקת ניתוח וכתיבה",
|
||||
qa_review: "הטיוטה בבדיקת איכות אוטומטית",
|
||||
|
||||
Reference in New Issue
Block a user