feat(ui): synthesis/superseded badges + merge provenance in lessons-tab (#162)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

עוצב ואושר דרך שער-העיצוב (Claude Design 12c-training-lessons.html).
- lessons-tab: SOURCE_BADGE['synthesis']="סינתזה" (navy מלא) · REVIEW_BADGE['superseded']="מוזג"
  (אפור-מעומעם, לא נדחה); שורות superseded מעומעמות (opacity).
- provenance: לקח-על מציג "⤷ מוזג מ-N לקחים · דחייה תשחזר אותם".
- backend: list_decision_lessons + _lesson_to_json מחזירים synthesized_from; training.ts
  DecisionLesson — 'synthesis'/'superseded' ל-unions + synthesized_from?: string[].

הבאדג'ים היו מוגנים-fallback קודם (ללא קריסה) — כעת מתויגים נכון. closes #162.

בדיקות: py_compile ✓ · leak-guard ✓ · tsc --noEmit ✓.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:59:31 +00:00
parent 1136711ec8
commit 987da57da4
4 changed files with 27 additions and 4 deletions

View File

@@ -54,6 +54,9 @@ const SOURCE_BADGE: Record<string, { label: string; cls: string }> = {
curator: { label: "הרמס (סקירה)", cls: "bg-info-bg text-info" },
style_analyzer: { label: "מנתח-סגנון", cls: "bg-success-bg text-success" },
"panel:deepseek+gemini": { label: "פאנל: דיפסיק+גמיני", cls: "bg-gold-wash text-gold-deep" },
// #158/INV-LRN8 — a super-lesson merging overlapping lessons; navy-filled so it
// reads as the consolidated/elevated one.
synthesis: { label: "סינתזה", cls: "bg-navy text-parchment" },
};
const SOURCE_BADGE_FALLBACK = { label: "פאנל", cls: "bg-rule-soft text-ink-soft" };
@@ -63,6 +66,9 @@ const REVIEW_BADGE: Record<string, { label: string; cls: string }> = {
proposed: { label: "ממתין לאישור", cls: "bg-gold-wash text-gold-deep border-gold/40" },
approved: { label: "מאושר · זורם לכותב", cls: "bg-success-bg text-success border-success/40" },
rejected: { label: "נדחה", cls: "bg-rule-soft text-ink-muted line-through" },
// #158/INV-LRN8 — merged into a synthesis super-lesson; kept as provenance, no
// longer fed to the writer. Muted (not struck — it wasn't rejected).
superseded: { label: "מוזג", cls: "bg-rule-soft text-ink-muted" },
};
export function LessonsTab({ corpusId }: { corpusId: string }) {
@@ -201,8 +207,10 @@ function LessonItem({
}
};
const mergedCount = lesson.source === "synthesis" ? (lesson.synthesized_from?.length ?? 0) : 0;
return (
<Card className="bg-surface border-rule">
<Card className={`bg-surface border-rule ${lesson.review_status === "superseded" ? "opacity-70" : ""}`}>
<CardContent className="px-4 py-3 space-y-2">
<div className="flex items-center gap-2 text-[0.72rem]">
<Badge variant="outline"
@@ -220,6 +228,12 @@ function LessonItem({
</span>
</div>
{mergedCount > 0 && (
<div className="text-[0.72rem] text-ink-muted">
מוזג מ-<b className="text-navy font-semibold">{mergedCount} לקחים</b> · דחייה תשחזר אותם
</div>
)}
{editing ? (
<>
<Textarea value={text} onChange={(e) => setText(e.target.value)}

View File

@@ -504,14 +504,20 @@ export type DecisionLesson = {
category: "style" | "structure" | "lexicon" | "tabular" | "general";
// "panel:deepseek+gemini" — the two-judge style panel; (string) keeps it open
// to future panel sources without a type break.
source: "manual" | "curator" | "chair" | "style_analyzer" | (string & {});
// "synthesis" — a super-lesson merging overlapping lessons (#158/INV-LRN8);
// "panel:deepseek+gemini" — the two-judge style panel; (string) keeps it open.
source: "manual" | "curator" | "chair" | "style_analyzer" | "synthesis" | (string & {});
// review gate (INV-LRN1/G10): only "approved" lessons flow to the writer.
// Panel-written lessons start as "proposed" and wait for the chair here.
// "superseded" — a source lesson merged into a synthesis (#158); kept as
// provenance, no longer fed to the writer.
// (applied_to_skill removed — LRN-1/INV-IA3: review_status is the single gate.)
review_status: "proposed" | "approved" | "rejected";
review_status: "proposed" | "approved" | "rejected" | "superseded";
created_by: string;
created_at: string;
updated_at: string;
// #158/INV-LRN8: ids of the source lessons a 'synthesis' row merged (provenance).
synthesized_from?: string[];
};
export type LessonReviewStatus = DecisionLesson["review_status"];