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:
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
|
||||
Reference in New Issue
Block a user