fix(learning): honest 3-channel curator card + capture curator findings (source='curator')
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 4s
Lint — undefined names / undefined-names (pull_request) Successful in 9s

תיקוני-מבנה ללולאת-הלמידה (TaskMaster #157):

1. כרטיס ה-curator (/training טאב "אוצֵר") ספר decision_lessons WHERE source='curator'
   — ערך שאף מסלול-קוד לא כתב → תמיד 0. עוצב-מחדש (דרך שער-העיצוב Claude Design, אושר)
   להציג ביושר את שלושת ערוצי-ההזנה לכותב: דיסטילציה→appeal_type_rules (180, זורם),
   פאנל→decision_lessons (81, ממתין), אוצֵר→source='curator'. get_curator_stats שוכתב.

2. drift ספ↔סוכן: 07-learning.md §1.1 + INV-LRN3 קבעו שהאוצֵר רושם ממצאים כ-decision_lesson
   source='curator', אך הסוכן כתב comments בלבד — הממצאים האיכותיים אבדו. נוסף כלי-MCP
   record_curator_findings + §A.5b ב-hermes-curator.md (read-only נשמר; הצעה מגודרת-שער).

3. get_recent_decision_lessons(limit=15) חתך בשקט — נוסף WARN על מה שנחתך (חוקה §6);
   הפתרון האמיתי = סינתזת-לקחים (TaskMaster #158).

Invariants: מקיים INV-LRN1/G10 (שער-יו"ר), INV-LRN3 (לכידה מובנית), INV-IA2/IA5 (מקור-אמת
יחיד), G12 (leak-guard עובר), G2 (אין מסלול מקביל — אותם מאגרים). פער-מימוש פתוח מתועד:
§A לא רץ אוטומטית על mark-final (pipeline-wake exits) → נדחה לתכנון נפרד.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 21:20:05 +00:00
parent 4de555367d
commit be774ab87e
9 changed files with 392 additions and 58 deletions

View File

@@ -45,6 +45,15 @@ export function CuratorPortraitPanel() {
<StatsCard />
<RecentFindings />
<div className="bg-info-bg border border-info/40 rounded-lg px-4 py-3 flex items-start gap-2.5 text-[0.78rem] text-ink-soft leading-relaxed">
<span>🔒</span>
<div>
<b className="text-navy">שער-היו״ר (INV-LRN1 / G10):</b> כל הערוצים <b>מציעים</b> בלבד.
ממצא או לקח משפיע על הכתיבה רק אחרי שדפנה אישרה אותו. האוצֵר עצמו read-only על התוכן
מזהה דפוסים ומגיש, לעולם לא משנה קבצים או סגנון בעצמו.
</div>
</div>
<Tabs defaultValue="curator-prompt" dir="rtl">
<TabsList className="bg-rule-soft/60">
<TabsTrigger value="curator-prompt">פרומפט ה-Curator</TabsTrigger>
@@ -67,38 +76,127 @@ export function CuratorPortraitPanel() {
// ── stats card ─────────────────────────────────────────────────────
function SectionLabel({ children }: { children: React.ReactNode }) {
return (
<div className="text-[0.72rem] uppercase tracking-wider text-gold-deep font-bold mb-3">
{children}
</div>
);
}
/** One of the three learning channels — engine, backing store, count, flow gate. */
function ChannelCard({
letter, title, engine, target, store, value, unit, flows, flowLabel, meta,
}: {
letter: string; title: string; engine: string; target: string; store: string;
value: number; unit: string; flows: boolean; flowLabel: string; meta: string;
}) {
return (
<Card className="bg-surface border-rule">
<CardContent className="px-[18px] py-4 flex flex-col gap-2.5">
<div className="flex items-baseline gap-2">
<span className="w-[22px] h-[22px] rounded-md flex items-center justify-center font-bold text-xs text-parchment shrink-0 bg-navy">
{letter}
</span>
<div>
<h3 className="text-[0.9rem] text-navy font-semibold leading-tight">{title}</h3>
<div className="text-[0.72rem] text-ink-muted">{engine}</div>
</div>
</div>
<div className="text-[0.72rem] text-ink-soft bg-rule-soft rounded-md px-2.5 py-1.5">
{target} <code className="text-[0.68rem] text-gold-deep">{store}</code>
</div>
<div className="flex items-baseline gap-1.5">
<span className="text-[1.9rem] leading-none font-bold text-navy tabular-nums">{value}</span>
<span className="text-[0.78rem] text-ink-muted">{unit}</span>
</div>
<span
className={`inline-flex items-center gap-1.5 rounded-full text-[0.72rem] font-semibold px-2.5 py-0.5 w-fit border ${
flows
? "bg-success-bg text-success border-success/40"
: "bg-gold-wash text-gold-deep border-gold/40"
}`}
>
<span className="text-[0.5rem]"></span>{flowLabel}
</span>
<div className="text-[0.72rem] text-ink-muted leading-relaxed">{meta}</div>
</CardContent>
</Card>
);
}
function StatsCard() {
const { data, isPending } = useCuratorStats();
if (isPending) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
{[...Array(4)].map((_, i) => <Skeleton key={i} className="h-20 w-full" />)}
<div className="space-y-4">
<Skeleton className="h-16 w-full" />
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{[...Array(3)].map((_, i) => <Skeleton key={i} className="h-44 w-full" />)}
</div>
</div>
);
}
if (!data) return null;
const { distillation: a, panel: b, curator: c } = data.channels;
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Kpi label="ממצאי curator" value={data.total_findings} icon={<Sparkles className="w-4 h-4" />} />
<Kpi label="החלטות שנסקרו" value={`${data.decisions_with_findings}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
<Kpi label="ממצאים מאושרים (זורמים לכותב)" value={data.findings_approved} icon={<CheckCircle2 className="w-4 h-4" />} />
<Kpi label="ממוצע ממצאים להחלטה"
value={
data.decisions_with_findings > 0
? (data.total_findings / data.decisions_with_findings).toFixed(1)
: "—"
}
icon={<Brain className="w-4 h-4" />}
/>
<div className="space-y-5">
<div className="bg-gold-wash border border-gold rounded-lg px-5 py-4 flex items-start gap-3">
<span className="text-gold-deep text-xl leading-tight"></span>
<p className="text-ink-soft text-sm leading-relaxed">
הלמידה זורמת לכותב ב<b className="text-gold-deep font-semibold">שלושה ערוצים נפרדים</b>, כל אחד
מופעל כשדפנה מסמנת החלטה כסופית. הכרטיס מראה כמה כל ערוץ תרם ומה כבר עובר בפועל לכותב
רק ידע ש<b className="text-gold-deep font-semibold">אושר בשער-היו״ר</b> משפיע על הטיוטות (INV-LRN1).
</p>
</div>
<div>
<SectionLabel>שלושת ערוצי-ההזנה לכותב</SectionLabel>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<ChannelCard
letter="א" title="דיסטילציה" engine="Claude · השוואת טיוטה↔סופי"
target="כללי-מתודולוגיה" store="appeal_type_rules"
value={a.items_total} unit="פריטים זורמים"
flows={a.items_total > 0} flowLabel="זורם לכותב"
meta={`${a.discussion_rules} כללי-דיון + ${a.transition_phrases} ביטויי-מעבר · אושרו ב-${a.pairs_folded} זוגות דרך טאב ״למידה״.`}
/>
<ChannelCard
letter="ב" title="פאנל דו-סוכני" engine="DeepSeek + Gemini · הצבעה 2/2"
target="לקחי-סגנון" store="decision_lessons"
value={b.total} unit="לקחים"
flows={b.approved > 0} flowLabel={`${b.approved} אושרו · ${b.proposed} ממתינים`}
meta={`על פני ${b.decisions} החלטות · אישור per-החלטה בטאב הקורפוס.`}
/>
<ChannelCard
letter="ג" title="ממצאי-אוצֵר" engine="האוצֵר · קריאת הסופי + דפוסים"
target="ממצאי-סגנון" store="source=curator"
value={c.total} unit="ממצאים"
flows={c.approved > 0} flowLabel={`${c.approved} אושרו · ${c.proposed} ממתינים`}
meta="35 דפוסים מתויגים להחלטה (סגנון/מבנה/לקסיקון/טבלאי) — נתפסים מבנית, לא רק כהערה."
/>
</div>
</div>
<div>
<SectionLabel>ערוץ האוצֵר מבט מקרוב</SectionLabel>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<Kpi label="סך ממצאי-אוצֵר" value={c.total} icon={<Sparkles className="w-4 h-4" />} />
<Kpi label="החלטות שנסקרו" value={`${c.decisions_reviewed}/${data.decisions_total}`} icon={<FileText className="w-4 h-4" />} />
<Kpi label="מאושרים · זורמים לכותב" value={c.approved} tone="success" icon={<CheckCircle2 className="w-4 h-4" />} />
<Kpi label="ממתינים לאישור" value={c.proposed} tone="gold" icon={<Brain className="w-4 h-4" />} />
</div>
</div>
</div>
);
}
function Kpi({
label, value, icon,
}: { label: string; value: string | number; icon: React.ReactNode }) {
label, value, icon, tone,
}: { label: string; value: string | number; icon: React.ReactNode; tone?: "success" | "gold" }) {
const valueCls = tone === "success" ? "text-success" : tone === "gold" ? "text-gold-deep" : "text-navy";
return (
<Card className="bg-surface border-rule">
<CardContent className="px-4 py-3">
@@ -106,7 +204,7 @@ function Kpi({
{icon}
<span>{label}</span>
</div>
<p className="text-2xl text-navy font-semibold tabular-nums mt-1">{value}</p>
<p className={`text-2xl font-semibold tabular-nums mt-1 ${valueCls}`}>{value}</p>
</CardContent>
</Card>
);
@@ -123,10 +221,10 @@ function RecentFindings() {
if (!data || data.recent_findings.length === 0) {
return (
<Card className="bg-rule-soft/40 border-rule">
<CardContent className="px-6 py-5 text-center text-ink-muted text-sm">
אין עדיין ממצאים של ה-Curator. הוא מופעל אוטומטית כאשר דפנה מסמנת
החלטה כסופית (mark-final), ושומר את ממצאיו כ-decision_lessons עם
source=&quot;curator&quot;.
<CardContent className="px-6 py-5 text-center text-ink-muted text-sm leading-relaxed">
אין עדיין ממצאי-אוצֵר. כשדפנה מסמנת החלטה כסופית, האוצֵר קורא את הסופי, מזהה
35 דפוסי-סגנון, ושומר אותם כ-<code>decision_lessons</code> (source=&quot;curator&quot;)
הממתינים לאישורה בנפרד מלקחי-הפאנל ומכללי-המתודולוגיה שלמעלה.
</CardContent>
</Card>
);

View File

@@ -197,7 +197,7 @@ export function StyleReportPanel() {
<h3 className="text-navy text-lg m-0">אוצֵר ממצאי-סגנון</h3>
<div className="flex items-center gap-4 rounded-lg border border-success bg-success-bg px-[18px] py-3.5">
<span className="text-success font-bold text-[1.75rem] leading-none tabular-nums">
{curator.data ? curator.data.findings_approved : "—"}
{curator.data ? curator.data.channels.curator.approved : "—"}
</span>
<div className="min-w-0">
<b className="text-navy text-sm font-semibold block">
@@ -214,8 +214,8 @@ export function StyleReportPanel() {
{curator.data
? Math.max(
0,
curator.data.total_findings -
curator.data.findings_approved,
curator.data.channels.curator.total -
curator.data.channels.curator.approved,
)
: "—"}
</div>
@@ -223,7 +223,7 @@ export function StyleReportPanel() {
</div>
<div className="flex-1 rounded-lg border border-rule bg-rule-soft px-3.5 py-3">
<div className="text-ink-muted font-bold text-[1.35rem] tabular-nums leading-none">
{curator.data ? curator.data.total_findings : "—"}
{curator.data ? curator.data.channels.curator.total : "—"}
</div>
<div className="text-[0.78rem] text-ink-soft mt-1">סך ממצאים</div>
</div>

View File

@@ -302,14 +302,39 @@ export type CuratorFinding = {
created_at: string;
};
// One decision_lessons channel split by the INV-LRN1 review gate.
export type LessonChannelStats = {
total: number;
approved: number;
proposed: number;
rejected: number;
};
// The three honest learning channels that feed the writer (INV-IA2/IA5).
// Earlier this was a single flat count of source='curator' — which nothing
// wrote, so it always read 0 while the panel/methodology channels carried all
// the real learning. See get_curator_stats (web/app.py).
export type CuratorStats = {
total_findings: number;
decisions_with_findings: number;
decisions_total: number;
// LRN-5 (INV-IA5): the count of curator lessons with review_status='approved'
// (the real INV-LRN1 writer gate) — replaces the old findings_applied, which
// counted the informative-only applied_to_skill flag.
findings_approved: number;
channels: {
// A — Claude draft↔final distillation → methodology overrides; flows now.
distillation: {
discussion_rules: number;
transition_phrases: number;
items_total: number;
pairs_folded: number;
pairs_total: number;
};
// B — DeepSeek+Gemini panel → decision_lessons; flows when chair-approved.
panel: LessonChannelStats & {
decisions: number;
by_practice_area: Record<string, number>;
};
// C — curator agent's qualitative findings → decision_lessons (source='curator').
curator: LessonChannelStats & {
decisions_reviewed: number;
};
};
recent_findings: CuratorFinding[];
};