feat(learning): chair-feedback style corrections auto-flow to the writer (P1 #6)
All checks were successful
G12 Leak-Guard / leak-guard (pull_request) Successful in 3s
Lint — undefined names / undefined-names (pull_request) Successful in 10s

The chair-feedback chain was DEAD: 27 feedback rows captured, 0 ever became a
lesson the writer reads (no weekly-analysis job ever existed). The chair's own
corrections — the most authoritative signal of all, and exactly "learn from every
returned draft" — went nowhere.

decision_lessons can't carry them: it's FK-coupled to a style_corpus row (a signed
final), which a case-in-progress doesn't have. So chair STYLE feedback instead
rides the discussion_rules channel that already reaches the writer for all blocks —
the same path /training promote uses.

- db.append_global_rule: the locked read-modify-write append, extracted from web
  `_append_methodology_override` into one shared impl (G2). The web function is now
  a thin wrapper that seeds defaults; chair-feedback calls it directly.
- record_chair_feedback (MCP tool): a STYLE-category correction (style/wrong_tone/
  wrong_structure) with a lesson_extracted flows immediately to discussion_rules.
  The chair IS the gate — no separate approval (INV-LRN1 graduated gate). SUBSTANCE
  feedback (missing_content/factual_error/other) is case-specific → recorded only.

Invariants: INV-LRN1 (chair-authored style = highest authority, flows; substance
not auto-flowed), G2 (single append impl shared by promote + feedback), INV-LRN5
(style channel only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-28 22:06:58 +00:00
parent b0a6c2fe01
commit 8b23542dec
3 changed files with 72 additions and 31 deletions

View File

@@ -4906,36 +4906,13 @@ 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.
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()
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),
)
"""Thin wrapper over db.append_global_rule (the single locked append impl, G2) —
seeds from _METHODOLOGY_DEFAULTS when no override row exists yet. Shared by the
T14 promote gate; chair-feedback auto-flow calls db.append_global_rule directly."""
await db.append_global_rule(
category, key, items,
seed_if_missing=list(_METHODOLOGY_DEFAULTS.get(category, {}).get(key, [])),
)
@app.post("/api/learning/pairs/{pair_id}/promote")