feat: drawer bulk significance re-rate per chat (T110.4)
The drawer's Significance review panel previously only supported per-memory edits. Adds a bulk control: pick ``level_from`` and ``level_to``, and every memory in the chat at ``level_from`` is moved to ``level_to``. Implementation emits one ``manual_edit`` event per matching memory (not a single bulk event) so the §6.4 per-row audit trail stays intact — each affected memory carries its own ``prior_value -> new_value`` snapshot, so an inverse edit can restore an individual row without needing to inspect a bulk payload's member list. Reuses the existing ``memory_significance`` ``manual_edit`` projector branch (T25), so no state-layer changes are required. The route rejects no-op submissions (``level_from == level_to``) with 400 to avoid padding the event log with empty edits, and clamps both levels to 0..3 (matching ``edit_memory_significance``). UI: a small ``<details>`` block in the Significance review section with two number inputs and a submit button. Test: tests/test_drawer_phase4.py::test_bulk_significance_re_rate_emits_manual_edit_per_memory.
This commit is contained in:
@@ -547,6 +547,25 @@
|
||||
</ul>
|
||||
</details>
|
||||
{% endif %}
|
||||
{# T110.4: bulk significance re-rate. Move every memory in this chat
|
||||
at level_from to level_to with one manual_edit event per row, so
|
||||
the audit trail stays per-memory. #}
|
||||
<details class="bulk-significance">
|
||||
<summary>Bulk re-rate significance</summary>
|
||||
<form class="inline-edit"
|
||||
hx-post="/chats/{{ chat.id }}/drawer/memory/significance/bulk"
|
||||
hx-target="#drawer" hx-swap="innerHTML">
|
||||
<label>
|
||||
From:
|
||||
<input type="number" name="level_from" min="0" max="3" value="0" required>
|
||||
</label>
|
||||
<label>
|
||||
To:
|
||||
<input type="number" name="level_to" min="0" max="3" value="1" required>
|
||||
</label>
|
||||
<button type="submit">Re-rate all</button>
|
||||
</form>
|
||||
</details>
|
||||
</section>
|
||||
|
||||
<section class="drawer-section">
|
||||
|
||||
@@ -411,6 +411,64 @@ async def edit_memory_significance(
|
||||
return await drawer(chat_id, request, conn)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chats/{chat_id}/drawer/memory/significance/bulk",
|
||||
response_class=HTMLResponse,
|
||||
)
|
||||
async def bulk_re_rate_significance(
|
||||
chat_id: str,
|
||||
request: Request,
|
||||
level_from: int = Form(...),
|
||||
level_to: int = Form(...),
|
||||
conn=Depends(get_conn),
|
||||
):
|
||||
"""T110.4: bulk re-rate every memory in this chat at ``level_from``
|
||||
to ``level_to``.
|
||||
|
||||
Fans out into one ``manual_edit`` event per matching memory rather
|
||||
than a single bulk event so the §6.4 audit trail stays per-row —
|
||||
each affected memory carries its own ``prior_value -> new_value``
|
||||
snapshot, so an inverse edit can restore an individual row without
|
||||
needing to inspect a bulk payload's member list. The drawer's
|
||||
significance-distribution panel surfaces the new buckets on the
|
||||
refreshed partial.
|
||||
|
||||
Both levels are clamped to 0..3 (matching ``edit_memory_significance``)
|
||||
and a no-op (``level_from == level_to``) is rejected with 400 so a
|
||||
misclick can't pad the event log with empty edits.
|
||||
"""
|
||||
chat = get_chat(conn, chat_id)
|
||||
if chat is None:
|
||||
raise HTTPException(status_code=404, detail=f"chat not found: {chat_id}")
|
||||
|
||||
lf = max(0, min(3, int(level_from)))
|
||||
lt = max(0, min(3, int(level_to)))
|
||||
if lf == lt:
|
||||
raise HTTPException(
|
||||
status_code=400,
|
||||
detail=f"level_from and level_to must differ (both = {lf})",
|
||||
)
|
||||
|
||||
rows = conn.execute(
|
||||
"SELECT id FROM memories WHERE chat_id = ? AND significance = ? "
|
||||
"ORDER BY id ASC",
|
||||
(chat_id, lf),
|
||||
).fetchall()
|
||||
for row in rows:
|
||||
memory_id = int(row[0])
|
||||
append_and_apply(
|
||||
conn,
|
||||
kind="manual_edit",
|
||||
payload={
|
||||
"target_kind": "memory_significance",
|
||||
"target_id": memory_id,
|
||||
"prior_value": lf,
|
||||
"new_value": lt,
|
||||
},
|
||||
)
|
||||
return await drawer(chat_id, request, conn)
|
||||
|
||||
|
||||
@router.post(
|
||||
"/chats/{chat_id}/drawer/memory/{memory_id}/pin",
|
||||
response_class=HTMLResponse,
|
||||
|
||||
Reference in New Issue
Block a user