fix(maintenance): list each prompt the Pair repair kept (#550)

The job counted its protected prompts but listed none, so Protected 2 opened
an empty group. The apply now returns one row per kept prompt with the
review's reasons, in its order ("Prompt 7: kept, pairs disagree on category:
Adult, asian"), and the job keeps them all for the report.
This commit is contained in:
Vito Sansevero
2026-10-31 17:27:00 -07:00
parent d4508c9997
commit 0504e67fd3
6 changed files with 132 additions and 4 deletions
+2
View File
@@ -4845,6 +4845,8 @@ follows the safety backup policy like them (no forced backup). Rules
writes `prompt_pairs`, `prompt_pair_tags`, `updated_at`, a tag or Library state.
- The job lists 200 changed rows ("Prompt 12: category “night”; tags a, b"); its
Download report lists every one (`truncated: false`, `Job.record_counted(complete=True)`).
Protected lists one row per kept prompt in the review's wording and order ("Prompt 7:
kept, pairs disagree on category: Adult, asian"), also complete in the report.
- Activity shows a row with no path as a record: its detail as plain text, never an open
or locate action, and the filter reads the detail. A maintenance job of records (every
operation but Missing-file links) says "No records in this group.", "No listed record
+24 -2
View File
@@ -304,6 +304,25 @@ def _protected_detail(result, key):
return "Kept: pairs disagree on tags"
def _kept(results):
"""One row per kept prompt, its review reasons without "Kept: " (#550)."""
rows = []
for result in results:
classes = set(_protected_classes(result))
if classes:
rows.append(
{
"prompt_id": result["id"],
"reasons": [
_protected_detail(result, key).removeprefix("Kept: ")
for key in DETAIL_ORDER
if key in classes
],
}
)
return rows
def _details(results, targets):
rows = []
for key in DETAIL_ORDER:
@@ -398,8 +417,10 @@ def _fill_tags(conn, fills, expected):
def apply_pair_metadata(conn, fingerprint, targets):
"""Apply the reviewed fills on ``conn`` (inside BEGIN IMMEDIATE).
Returns ``{changed, categories, tag_sets, rows}``; ``rows`` lists every
filled prompt (``{prompt_id, category?, tags?}``), never capped.
Returns ``{changed, categories, tag_sets, rows, kept}``; ``rows`` lists
every filled prompt (``{prompt_id, category?, tags?}``) and ``kept`` every
prompt the plan keeps (``{prompt_id, reasons}``, in the review's wording
and order), both never capped.
"""
results, current = _state(conn)
if current != fingerprint or _targets(results) != targets:
@@ -427,4 +448,5 @@ def apply_pair_metadata(conn, fingerprint, targets):
"categories": len(categories),
"tag_sets": len(tags),
"rows": rows,
"kept": _kept(results),
}
+13 -1
View File
@@ -697,7 +697,19 @@ class MaintenanceRoutesMixin:
rows,
complete=plan.operation in COMPLETE_RECORD,
)
job.record_counted("protected", payload["protected"])
if "kept" in result:
# #550: each kept record with its reason, like the changed ones.
kept = [
{
"path": "",
"detail": f"Prompt {item['prompt_id']}: kept, "
+ "; ".join(item["reasons"]),
}
for item in result["kept"]
]
job.record_counted("protected", len(kept), kept, complete=True)
else:
job.record_counted("protected", payload["protected"])
line = _changed_line(
plan.operation, result["changed"], result.get("marks", 0), result
)
@@ -225,8 +225,13 @@ test('D192: Pair category and tags reviews old Pair prompts, keeps an edited one
expect(await records.first().evaluate(node => node.localName)).toBe('div');
await expect(results.locator('.outcome-files button')).toHaveCount(0);
await expect(results.getByRole('searchbox', { name: 'Filter results' })).toHaveAttribute('placeholder', 'Filter records…');
// Protected is counted, not listed: it says records, not files.
// Protected lists each kept prompt with the review's reason (#550 live re-check).
await results.locator('.outcome-list > button').filter({ hasText: 'Protected' }).click();
await expect(records).toHaveCount(1);
await expect(records.first()).toHaveText(
/^Prompt \d+: kept, edited after it was saved; its empty category or tags may be intentional$/);
// An empty group says records, not files.
await results.locator('.outcome-list > button').filter({ hasText: 'Errors' }).click();
await expect(results.locator('.outcome-files > p.empty-state')).toHaveText('No records in this group.');
const library = await serverJson(request, '/prompt_manager/library');
const lantern = library.items.find(item => item.text === 'S550 lantern');
@@ -84,6 +84,47 @@ class TestPairMetadataRoutes(PairMetadataAPITestCase):
self.assertEqual(plan["protected"]["reasons"], ["1 where pairs disagree"])
self.assertEqual(job.counters["protected"], 1)
self.assertIsNone(self.category(prompt))
# Live test (#550): the count comes with its rows, in the review's wording.
self.assertEqual(
job.outcomes["protected"],
[
{
"path": "",
"root": "",
"root_index": None,
"detail": (
f"Prompt {prompt}: kept, pairs disagree on category: x, y"
),
}
],
)
report = job.report()["outcomes"]["protected"]
self.assertEqual((report["count"], report["truncated"]), (1, False))
async def test_report_lists_every_kept_prompt(self):
PromptManagerConfig.SAFETY_BACKUP = False
count = MAX_OUTCOME_ROWS + 5
for n in range(count):
_pair, prompt = self.pair(f"kept {n:03d}", category="portrait")
with self.api.db.model.get_connection() as conn:
conn.execute(
"UPDATE prompts SET updated_at='2099-01-01T00:00:00+00:00' "
"WHERE id=?",
(prompt,),
)
_plan, _summary, job = await self.run_apply(OPERATION)
self.assertEqual(job.counters["protected"], count)
self.assertEqual(len(job.outcomes["protected"]), MAX_OUTCOME_ROWS)
protected = job.report()["outcomes"]["protected"]
self.assertEqual(
(len(protected["rows"]), protected["truncated"]), (count, False)
)
self.assertTrue(
protected["rows"][0]["detail"].endswith(
": kept, edited after it was saved; its empty category or tags "
"may be intentional"
)
)
async def test_policy_off_writes_without_a_backup(self):
PromptManagerConfig.SAFETY_BACKUP = False
+46
View File
@@ -401,6 +401,52 @@ class TestApply(PairMetadataCase):
)
self.assertEqual(self.sql("SELECT id, name FROM tags ORDER BY id"), tag_rows)
def test_apply_returns_one_kept_row_per_protected_prompt(self):
_a, conflict = self.pair("conflict", "n1", category="asian")
self.pair("conflict", "n2", category="Adult")
_b, edited = self.pair("edited", category="portrait")
self.edit_stamp(edited)
_c, both = self.pair(
"both", "n1", category="x", tags=[f"t{n}" for n in range(21)]
)
self.pair("both", "n2", category="y", tags=[f"t{n}" for n in range(21)])
_d, filled = self.pair("fill", category="portrait")
draft = self.preview()
result = self.apply(draft)
self.assertEqual(
result["kept"],
[
{
"prompt_id": conflict,
"reasons": ["pairs disagree on category: Adult, asian"],
},
{
"prompt_id": edited,
"reasons": [
"edited after it was saved; its empty category or tags "
"may be intentional"
],
},
{
"prompt_id": both,
"reasons": [
"pairs disagree on category: x, y",
"its pair has more than 20 tags",
],
},
],
)
self.assertEqual(len(result["kept"]), draft.protected["count"])
self.assertEqual(self.category(filled), "portrait")
def test_kept_rows_are_never_capped(self):
for n in range(205):
_p, prompt = self.pair(f"kept {n:03d}", category="portrait")
self.edit_stamp(prompt)
result = self.apply(self.preview())
self.assertEqual(result["changed"], 0)
self.assertEqual(len(result["kept"]), 205)
def test_second_run_finds_nothing_and_a_partial_fill_completes_later(self):
_a, prompt = self.pair("partial", "n1", category="portrait", tags=["a"])
p2, _same = self.pair("partial", "n2", category="portrait", tags=["c"])