feat(style-acq T14): שער-יו"ר לאישור הצעות-curator → הטמעה לפרופיל

סוגר את הלולאה מקצה-לקצה (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>
This commit is contained in:
2026-06-06 19:17:56 +00:00
parent ee76455a9a
commit f20a3a09fd
4 changed files with 244 additions and 5 deletions

View File

@@ -4151,6 +4151,93 @@ async def api_learning_style_distance(case_number: str):
return await _sd.style_distance(case_number)
def _coerce_json(raw):
if isinstance(raw, str):
try:
return json.loads(raw)
except (json.JSONDecodeError, TypeError):
return None
return raw
@app.get("/api/learning/pairs/{pair_id}")
async def api_learning_pair_detail(pair_id: str):
"""פירוט שורת-פנקס כולל הצעת-הדיסטילציה (analysis) לאישור יו"ר (T14)."""
try:
pid = UUID(pair_id)
except ValueError:
raise HTTPException(400, "pair_id לא תקין")
p = await db.get_draft_final_pair(pid)
if not p:
raise HTTPException(404, "לא נמצא")
analysis = _coerce_json(p.get("analysis")) or {}
# surface only style_method changes (INV-LRN5 — substance never enters the voice)
changes = [c for c in analysis.get("changes", []) if c.get("domain") == "style_method"]
return {
"id": str(p["id"]),
"case_number": p.get("case_number") or "",
"title": p.get("title") or "",
"status": p.get("status") or "",
"diff_stats": _coerce_json(p.get("diff_stats")),
"overall_assessment": analysis.get("overall_assessment", ""),
"changes": changes,
"new_expressions": analysis.get("new_expressions", []),
}
class PromoteLearningRequest(BaseModel):
lessons: list[str] = [] # style_method lessons → discussion_rules['universal']
phrases: list[str] = [] # new transition phrases → transition_phrases['universal']
async def _append_methodology_override(category: str, key: str, items: list[str]) -> None:
"""Read current (override-or-default) list value, append new items, upsert override.
Shared by the T14 approval gate to fold approved learnings into writer-consumed channels."""
pool = await db.get_pool()
row = await pool.fetchrow(
"SELECT rule_value FROM appeal_type_rules "
"WHERE appeal_type = '_global' AND rule_category = $1 AND rule_key = $2",
category, key,
)
if row:
current = _coerce_json(row["rule_value"]) or []
else:
current = list(_METHODOLOGY_DEFAULTS.get(category, {}).get(key, []))
if not isinstance(current, list):
current = []
merged = current + [s for s in items if s and s not in current]
await pool.execute(
"INSERT INTO appeal_type_rules (id, appeal_type, rule_category, rule_key, rule_value) "
"VALUES (gen_random_uuid(), '_global', $1, $2, $3::text::jsonb) "
"ON CONFLICT (appeal_type, rule_category, rule_key) DO UPDATE SET rule_value = $3::text::jsonb",
category, key, json.dumps(merged, ensure_ascii=False),
)
@app.post("/api/learning/pairs/{pair_id}/promote")
async def api_learning_promote(pair_id: str, req: PromoteLearningRequest):
"""שער-יו"ר (INV-G10/LRN1): מאשר לקחי-סגנון + ביטויי-מעבר מהצעת-הדיסטילציה
ומטמיע אותם בערוצים שהכותב צורך (methodology overrides → T15). מקדם status."""
try:
pid = UUID(pair_id)
except ValueError:
raise HTTPException(400, "pair_id לא תקין")
p = await db.get_draft_final_pair(pid)
if not p:
raise HTTPException(404, "לא נמצא")
if req.lessons:
await _append_methodology_override("discussion_rules", "universal", req.lessons)
if req.phrases:
await _append_methodology_override("transition_phrases", "universal", req.phrases)
await db.update_draft_final_pair(pid, status="lessons_folded")
return {
"id": pair_id, "status": "lessons_folded",
"folded_lessons": len(req.lessons), "folded_phrases": len(req.phrases),
}
# ── Skill Management API ───────────────────────────────────────────