feat(ia): IA גל-2 — איחוד-משטחים: ערוץ-למידה אחד · /operations⊇/diagnostics · MET-2/3 (#131, X17)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 9s
גל-2 מבקלוג #127 — איחוד-משטחים לפי משטח-היעד של X17. מקיים INV-IA1/IA3/IA4 + דלתות-הספ (X6 INV-UI7/8, 07-learning §0.4, 00-constitution G2). שומר G10/INV-LRN1 (לא הוסר שום שער-אנושי — רק שער/דגל כפול). א) תיבת-אישור אחת (INV-IA1): כרטיסי "אישור הלכות"+"פסיקה חסרה" ב-/operations מצביעים ל-/approvals (לתיבת-האישורים ←) — /operations מנטר, /approvals מחליט. ב) ערוץ-למידה אחד (INV-IA3): הוסר applied_to_skill end-to-end — - UI: כפתור "סמן כ'אומץ'" + badge "אומץ" ב-lessons-tab; badge ב-curator-portrait. - API: LessonPatch, _lesson_to_json, patch call, curator recent_findings (→review_status). - db.py: list/add/update_decision_lesson לא בוחרים/כותבים applied_to_skill; הפרמטר הוסר. העמודה+אינדקס נשמרים (back-compat, ללא migration), מסומנים DEPRECATED. - types: DecisionLesson/LessonPatch/CuratorFinding. review_status='approved' = הסטטוס היחיד "זורם-לכותב" (INV-LRN1, #126). ג) MET-2/3 lost-update (INV-IA3): _append_methodology_override רץ עכשיו בטרנזקציה אחת עם SELECT ... FOR UPDATE — אין read-modify-write מתפצל מול עורך-המתודולוגיה או promote מקביל. /methodology = העורך-הקנוני; promote מבטל את ה-cache (גל-1 MET-1). ד) /operations⊇/diagnostics (INV-IA4): גוף /diagnostics חולץ ל-<SystemHealthSection/> ומורנדר ב-/operations תחת "בריאות-מערכת". /diagnostics → redirect ל-/operations. /diagnostics הוסר מהניווט. משטח-ניטור יחיד. ה) דלתות-ספ (≥3 מקורות ב-X17, אושר ע"י חיים /goal): - X6: INV-UI7 (aggregate=SSoT, mutation מבטל queryKey) + INV-UI8 (render-or-remove, חלקיות). - 07-learning §0.4: שער-אחד + טרנזקציה-אחת + applied_to_skill מוסר. - 00-constitution G2: תאום-המתודולוגיה כהפרה-ידועה-ממותנת. - X17 דלתות-ספ סומנו ✅ קודדו. בדיקות: py_compile app.py + db.py ✓ · tsc --noEmit ✓ · eslint ✓ (לבד מ-learning-panel:109 קיים-מראש). next build נכשל ב-worktree רק בגלל symlink (Turbopack) — Docker/CI תקין. api:types יתרענן בדפלוי (curator/lessons אינם response-modeled; הטיפוסים יד-כתובים עודכנו). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
55
web/app.py
55
web/app.py
@@ -1338,7 +1338,7 @@ async def get_curator_stats():
|
||||
# Last 10 curator findings — newest first
|
||||
recent_rows = await conn.fetch(
|
||||
"""
|
||||
SELECT dl.id, dl.lesson_text, dl.category, dl.applied_to_skill,
|
||||
SELECT dl.id, dl.lesson_text, dl.category, dl.review_status,
|
||||
dl.created_at,
|
||||
sc.decision_number, sc.decision_date
|
||||
FROM decision_lessons dl
|
||||
@@ -1358,7 +1358,7 @@ async def get_curator_stats():
|
||||
"id": str(r["id"]),
|
||||
"lesson_text": r["lesson_text"],
|
||||
"category": r["category"],
|
||||
"applied_to_skill": bool(r["applied_to_skill"]),
|
||||
"review_status": r["review_status"] or "proposed",
|
||||
"decision_number": r["decision_number"] or "",
|
||||
"decision_date": str(r["decision_date"]) if r["decision_date"] else "",
|
||||
"created_at": r["created_at"].isoformat() if r["created_at"] else "",
|
||||
@@ -1454,7 +1454,6 @@ class LessonCreate(BaseModel):
|
||||
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)
|
||||
|
||||
|
||||
@@ -1470,8 +1469,8 @@ def _lesson_to_json(row: dict) -> dict:
|
||||
"lesson_text": row["lesson_text"],
|
||||
"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.
|
||||
# (applied_to_skill removed — LRN-1/INV-IA3, was informative-only.)
|
||||
"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 "",
|
||||
@@ -1527,7 +1526,6 @@ async def patch_corpus_lesson(lesson_id: str, body: LessonPatch):
|
||||
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"):
|
||||
@@ -4657,26 +4655,35 @@ class PromoteLearningRequest(BaseModel):
|
||||
|
||||
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."""
|
||||
Shared by the T14 approval gate to fold approved learnings into writer-consumed channels.
|
||||
|
||||
MET-2/3 (INV-IA3): the read-modify-write runs inside ONE transaction with the
|
||||
existing override row locked FOR UPDATE, so a concurrent promote (or a methodology
|
||||
PUT) can't interleave between the read and the write and silently drop items.
|
||||
The methodology PUT overwrites the same row; the MET-1 invalidation (גל-1) makes
|
||||
the /methodology editor refetch on promote so it edits post-append state."""
|
||||
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),
|
||||
)
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.transaction():
|
||||
row = await conn.fetchrow(
|
||||
"SELECT rule_value FROM appeal_type_rules "
|
||||
"WHERE appeal_type = '_global' AND rule_category = $1 AND rule_key = $2 "
|
||||
"FOR UPDATE",
|
||||
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 conn.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")
|
||||
|
||||
Reference in New Issue
Block a user