Merge pull request 'feat(learning): שער-אישור ל-decision_lessons — רק לקח מאושר זורם לכותב (INV-LRN1, #126)' (#202) from worktree-lesson-approval-gate into main
All checks were successful
Build & Deploy / build-and-deploy (push) Successful in 2m14s
G12 Leak-Guard / leak-guard (push) Successful in 6s

This commit was merged in pull request #202.
This commit is contained in:
2026-06-11 18:14:38 +00:00
4 changed files with 110 additions and 9 deletions

View File

@@ -1421,10 +1421,12 @@ class LessonPatch(BaseModel):
lesson_text: str | None = None
category: str | None = None
applied_to_skill: bool | None = None
review_status: str | None = None # proposed | approved | rejected (INV-LRN1 gate)
_LESSON_CATEGORIES = {"style", "structure", "lexicon", "tabular", "general"}
_LESSON_SOURCES = {"manual", "curator", "chair", "style_analyzer"}
_LESSON_REVIEW_STATUSES = {"proposed", "approved", "rejected"}
def _lesson_to_json(row: dict) -> dict:
@@ -1435,6 +1437,8 @@ def _lesson_to_json(row: dict) -> dict:
"category": row["category"],
"source": row["source"],
"applied_to_skill": bool(row["applied_to_skill"]),
# review gate (INV-LRN1/G10): only 'approved' flows to the writer.
"review_status": row.get("review_status", "proposed"),
"created_by": row.get("created_by", ""),
"created_at": row["created_at"].isoformat() if row.get("created_at") else "",
"updated_at": row["updated_at"].isoformat() if row.get("updated_at") else "",
@@ -1464,8 +1468,11 @@ async def add_corpus_lesson(corpus_id: str, body: LessonCreate):
raise HTTPException(400, f"invalid category; allowed: {sorted(_LESSON_CATEGORIES)}")
if body.source not in _LESSON_SOURCES:
raise HTTPException(400, f"invalid source; allowed: {sorted(_LESSON_SOURCES)}")
# A lesson the chair authors directly here is approved by construction — it skips
# the proposal gate (which exists to vet machine-generated panel lessons).
row = await db.add_decision_lesson(
cid, lesson_text=text, category=body.category, source=body.source,
review_status="approved",
)
if not row:
raise HTTPException(500, "failed to insert lesson")
@@ -1480,11 +1487,14 @@ async def patch_corpus_lesson(lesson_id: str, body: LessonPatch):
raise HTTPException(400, "invalid lesson_id")
if body.category is not None and body.category not in _LESSON_CATEGORIES:
raise HTTPException(400, f"invalid category; allowed: {sorted(_LESSON_CATEGORIES)}")
if body.review_status is not None and body.review_status not in _LESSON_REVIEW_STATUSES:
raise HTTPException(400, f"invalid review_status; allowed: {sorted(_LESSON_REVIEW_STATUSES)}")
result = await db.update_decision_lesson(
lid,
lesson_text=body.lesson_text,
category=body.category,
applied_to_skill=body.applied_to_skill,
review_status=body.review_status,
)
if not result.get("updated"):
if result.get("reason") == "not found":