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:
87
web/app.py
87
web/app.py
@@ -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 ───────────────────────────────────────────
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user