סוגר את הלולאה מקצה-לקצה (INV-G10/LRN1): ה-curator מציע (status=analyzed),
היו"ר מאשרת, והלקחים נכתבים לערוצים שהכותב צורך (T15) — אין auto-commit.
- db.get_draft_final_pair(id) — שורת-פנקס מלאה כולל analysis.
- app.py: GET /api/learning/pairs/{id} (חושף רק changes מסוג style_method —
INV-LRN5) + POST .../promote (לקחים→discussion_rules['universal'],
ביטויים→transition_phrases['universal'] דרך merge ל-appeal_type_rules;
status→lessons_folded). _append_methodology_override משותף.
- web-ui: usePairDetail/usePromoteLearning + ProposalReview (בחירת לקחים/
ביטויים לאימוץ) בטאב "למידה" עבור pairs במצב analyzed.
INV-G10 (שער-יו"ר) · INV-LRN1 (אין auto-commit) · INV-LRN5 (טוהר).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
117 lines
3.4 KiB
TypeScript
117 lines
3.4 KiB
TypeScript
/**
|
|
* Style-acquisition learning surface (T6/T13) — reconciliation ledger + style-distance.
|
|
* Backs the /training "למידה" tab. Endpoints under /api/learning.
|
|
*/
|
|
|
|
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
|
import { apiRequest } from "./client";
|
|
|
|
export type DraftFinalPair = {
|
|
id: string;
|
|
case_id: string | null;
|
|
case_number: string;
|
|
title: string;
|
|
status: "final_received" | "analyzed" | "lessons_folded" | string;
|
|
change_percent: number | null;
|
|
created_at: string | null;
|
|
updated_at: string | null;
|
|
};
|
|
|
|
export type StyleDistance = {
|
|
case_number: string;
|
|
outcome: string;
|
|
golden_ratio_adherence: {
|
|
outcome: string;
|
|
total_words: number;
|
|
sections: Record<string, { actual_pct: number; target: [number, number]; deviation_pp: number }>;
|
|
max_deviation: number | null;
|
|
};
|
|
anti_pattern_hits: { total: number; by_pattern: Record<string, { count: number; note: string }> };
|
|
draft_to_final_diff: { change_percent?: number } | null;
|
|
pair_status: string | null;
|
|
summary: {
|
|
ratio_max_deviation_pp: number | null;
|
|
anti_pattern_total: number;
|
|
change_percent: number | null;
|
|
};
|
|
error?: string;
|
|
};
|
|
|
|
export const learningKeys = {
|
|
all: ["learning"] as const,
|
|
pairs: (status: string) => [...learningKeys.all, "pairs", status] as const,
|
|
distance: (caseNumber: string) => [...learningKeys.all, "distance", caseNumber] as const,
|
|
};
|
|
|
|
export function useReconciliationLedger(status = "") {
|
|
return useQuery({
|
|
queryKey: learningKeys.pairs(status),
|
|
queryFn: ({ signal }) =>
|
|
apiRequest<{ items: DraftFinalPair[]; count: number }>(
|
|
`/api/learning/pairs${status ? `?status=${status}` : ""}`,
|
|
{ signal },
|
|
),
|
|
staleTime: 15_000,
|
|
});
|
|
}
|
|
|
|
export function useStyleDistance(caseNumber: string | null) {
|
|
return useQuery({
|
|
queryKey: learningKeys.distance(caseNumber ?? ""),
|
|
queryFn: ({ signal }) =>
|
|
apiRequest<StyleDistance>(
|
|
`/api/learning/style-distance/${encodeURIComponent(caseNumber!)}`,
|
|
{ signal },
|
|
),
|
|
enabled: Boolean(caseNumber),
|
|
staleTime: 15_000,
|
|
});
|
|
}
|
|
|
|
// ── T14: curator distillation proposal + chair approval gate ──────
|
|
|
|
export type DistillationChange = {
|
|
type?: string;
|
|
domain?: string;
|
|
block?: string;
|
|
description?: string;
|
|
draft_text?: string;
|
|
final_text?: string;
|
|
lesson?: string;
|
|
};
|
|
|
|
export type PairDetail = {
|
|
id: string;
|
|
case_number: string;
|
|
title: string;
|
|
status: string;
|
|
diff_stats: { change_percent?: number } | null;
|
|
overall_assessment: string;
|
|
changes: DistillationChange[]; // style_method only (server-filtered)
|
|
new_expressions: string[];
|
|
};
|
|
|
|
export function usePairDetail(pairId: string | null) {
|
|
return useQuery({
|
|
queryKey: [...learningKeys.all, "pair", pairId ?? ""] as const,
|
|
queryFn: ({ signal }) =>
|
|
apiRequest<PairDetail>(`/api/learning/pairs/${pairId}`, { signal }),
|
|
enabled: Boolean(pairId),
|
|
staleTime: 15_000,
|
|
});
|
|
}
|
|
|
|
export function usePromoteLearning(pairId: string) {
|
|
const qc = useQueryClient();
|
|
return useMutation({
|
|
mutationFn: (body: { lessons: string[]; phrases: string[] }) =>
|
|
apiRequest<{ id: string; status: string; folded_lessons: number; folded_phrases: number }>(
|
|
`/api/learning/pairs/${pairId}/promote`,
|
|
{ method: "POST", body },
|
|
),
|
|
onSuccess: () => {
|
|
qc.invalidateQueries({ queryKey: learningKeys.all });
|
|
},
|
|
});
|
|
}
|