feat(learning): chair-feedback style corrections auto-flow to the writer (P1 #6)
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:
@@ -2875,6 +2875,48 @@ async def get_style_patterns(pattern_type: str | None = None) -> list[dict]:
|
||||
return [dict(r) for r in rows]
|
||||
|
||||
|
||||
async def append_global_rule(
|
||||
category: str, key: str, items: list[str], seed_if_missing: list | None = None,
|
||||
) -> int:
|
||||
"""Append items to a _global appeal_type_rules list value (the writer-consumed
|
||||
methodology channel), idempotently and under a row lock. Returns how many NEW
|
||||
items were added (existing duplicates are skipped). Single locked read-modify-
|
||||
write (MET-2/3) so concurrent appends can't drop items. Shared by the /training
|
||||
promote gate (web `_append_methodology_override`) and chair-feedback auto-flow
|
||||
(G2 — one append implementation)."""
|
||||
pool = await 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 = row["rule_value"]
|
||||
if isinstance(current, str):
|
||||
try:
|
||||
current = json.loads(current)
|
||||
except (json.JSONDecodeError, TypeError):
|
||||
current = []
|
||||
else:
|
||||
current = list(seed_if_missing or [])
|
||||
if not isinstance(current, list):
|
||||
current = []
|
||||
added = [s for s in items if s and s not in current]
|
||||
if not added and row:
|
||||
return 0
|
||||
merged = current + added
|
||||
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),
|
||||
)
|
||||
return len(added)
|
||||
|
||||
|
||||
async def get_methodology_overrides(category: str) -> dict:
|
||||
"""Chair's /methodology edits for one category (golden_ratios / discussion_rules /
|
||||
content_checklists). Returns {rule_key: parsed_value}. These OVERRIDE the hardcoded
|
||||
|
||||
Reference in New Issue
Block a user