Restore skip and move search to find

This commit is contained in:
ben
2026-03-20 13:35:07 -04:00
parent 2847d2d59f
commit afadd0c0d0
3 changed files with 90 additions and 8 deletions

View File

@@ -718,12 +718,12 @@ replace the old observed/canonical workflow with a review-first pipeline that us
- Direct numeric selection works well for suggestion-heavy review, while `[l]ink existing` remains available as a fallback when the suggestion list is empty or incomplete. - Direct numeric selection works well for suggestion-heavy review, while `[l]ink existing` remains available as a fallback when the suggestion list is empty or incomplete.
- I kept the review data model unchanged from `t1.15`; this task only tightened the prompt format, field order, and save behavior. - I kept the review data model unchanged from `t1.15`; this task only tightened the prompt format, field order, and save behavior.
* [x] t1.16.1: add catalog search flow to review ui (2-3 commits) * [X] t1.16.1: add catalog search flow to review ui (2-3 commits)
enable fast lookup of catalog items during review via tokenized search and replace manual list scanning enable fast lookup of catalog items during review via tokenized search and replace manual list scanning
** acceptance criteria ** acceptance criteria
1. replace `[l]ink existing` with `[s]earch` in review prompt: 1. replace `[l]ink existing` with `[f]ind` in review prompt:
- `[#] link to suggestion [s]earch [n]ew [x]exclude [q]uit >` - `[#] link to suggestion [f]ind [n]ew [s]kip [x]exclude [q]uit >`
2. implement search flow: 2. implement search flow:
- on `s`, prompt: `search: ` - on `s`, prompt: `search: `
- tokenize input using same normalization rules as suggestion matching - tokenize input using same normalization rules as suggestion matching
@@ -755,12 +755,13 @@ enable fast lookup of catalog items during review via tokenized search and repla
** evidence ** evidence
- commit: `f93b9aa` - commit: `f93b9aa`
- tests: `./venv/bin/python -m unittest discover -s tests`; `./venv/bin/python review_products.py --help`; `./venv/bin/python review_products.py --refresh-only` - tests: `./venv/bin/python -m unittest discover -s tests`; `./venv/bin/python review_products.py --help`; `./venv/bin/python review_products.py --refresh-only`
- datetime: 2026-03-20 13:32:09 EDT - datetime: 2026-03-20 13:34:57 EDT
** notes ** notes
- The search path reuses the same lightweight token matching rules as suggestion ranking, so there is still only one matching system to maintain. - The search path reuses the same lightweight token matching rules as suggestion ranking, so there is still only one matching system to maintain.
- Direct numeric suggestion-pick remains the fastest happy path; search is the fallback when suggestions are sparse or missing. - Direct numeric suggestion-pick remains the fastest happy path; search is the fallback when suggestions are sparse or missing.
- Search intentionally optimizes for manual speed rather than smart ranking: simple token overlap, max 10 rows, and immediate persistence on selection. - Search intentionally optimizes for manual speed rather than smart ranking: simple token overlap, max 10 rows, and immediate persistence on selection.
- Follow-up fix: search moved to `[f]ind` so `[s]kip` remains available at the main prompt.
* [ ] 1t.10: add optional llm-assisted suggestion workflow for unresolved normalized retailer items (2-4 commits) * [ ] 1t.10: add optional llm-assisted suggestion workflow for unresolved normalized retailer items (2-4 commits)

View File

@@ -382,7 +382,7 @@ def prompt_resolution(queue_row, related_rows, purchase_rows, catalog_rows, queu
prompt_bits = [] prompt_bits = []
if suggestions: if suggestions:
prompt_bits.append("[#] link to suggestion") prompt_bits.append("[#] link to suggestion")
prompt_bits.extend(["[s]earch", "[n]ew", "e[x]clude", "[q]uit"]) prompt_bits.extend(["[f]ind", "[n]ew", "[s]kip", "e[x]clude", "[q]uit"])
click.secho(" ".join(prompt_bits) + " >", fg=PROMPT_COLOR) click.secho(" ".join(prompt_bits) + " >", fg=PROMPT_COLOR)
action = click.prompt("", type=str, prompt_suffix=" ").strip().lower() action = click.prompt("", type=str, prompt_suffix=" ").strip().lower()
if action.isdigit() and suggestions: if action.isdigit() and suggestions:
@@ -403,6 +403,15 @@ def prompt_resolution(queue_row, related_rows, purchase_rows, catalog_rows, queu
if action == "q": if action == "q":
return None, None return None, None
if action == "s": if action == "s":
return {
"normalized_item_id": queue_row["normalized_item_id"],
"catalog_id": "",
"resolution_action": "skip",
"status": "pending",
"resolution_notes": queue_row.get("resolution_notes", ""),
"reviewed_at": str(date.today()),
}, None
if action == "f":
while True: while True:
query = click.prompt(click.style("search", fg=PROMPT_COLOR), default="", show_default=False).strip() query = click.prompt(click.style("search", fg=PROMPT_COLOR), default="", show_default=False).strip()
if not query: if not query:

View File

@@ -219,7 +219,7 @@ class ReviewWorkflowTests(unittest.TestCase):
self.assertIn("Review guide:", result.output) self.assertIn("Review guide:", result.output)
self.assertIn("Review 1/1: MIXED PEPPER", result.output) self.assertIn("Review 1/1: MIXED PEPPER", result.output)
self.assertIn("2 matched items:", result.output) self.assertIn("2 matched items:", result.output)
self.assertIn("[#] link to suggestion [s]earch [n]ew e[x]clude [q]uit >", result.output) self.assertIn("[#] link to suggestion [f]ind [n]ew [s]kip e[x]clude [q]uit >", result.output)
first_item = result.output.index("[1] MIXED PEPPER 6-PACK | costco | 2026-03-14 | 7.49 | ") first_item = result.output.index("[1] MIXED PEPPER 6-PACK | costco | 2026-03-14 | 7.49 | ")
second_item = result.output.index("[2] MIXED PEPPER 6-PACK | costco | 2026-03-12 | 6.99 | https://example.test/mixed-pepper.jpg") second_item = result.output.index("[2] MIXED PEPPER 6-PACK | costco | 2026-03-12 | 6.99 | https://example.test/mixed-pepper.jpg")
self.assertLess(first_item, second_item) self.assertLess(first_item, second_item)
@@ -401,7 +401,7 @@ class ReviewWorkflowTests(unittest.TestCase):
"--limit", "--limit",
"1", "1",
], ],
input="s\nmixed pepper\n1\nlinked by test\n", input="f\nmixed pepper\n1\nlinked by test\n",
color=True, color=True,
) )
@@ -492,13 +492,85 @@ class ReviewWorkflowTests(unittest.TestCase):
"--links-csv", "--links-csv",
str(links_csv), str(links_csv),
], ],
input="s\nzzz\nq\nq\n", input="f\nzzz\nq\nq\n",
color=True, color=True,
) )
self.assertEqual(0, result.exit_code) self.assertEqual(0, result.exit_code)
self.assertIn("no matches found", result.output) self.assertIn("no matches found", result.output)
def test_skip_remains_available_from_main_prompt(self):
with tempfile.TemporaryDirectory() as tmpdir:
purchases_csv = Path(tmpdir) / "purchases.csv"
queue_csv = Path(tmpdir) / "review_queue.csv"
resolutions_csv = Path(tmpdir) / "review_resolutions.csv"
catalog_csv = Path(tmpdir) / "catalog.csv"
links_csv = Path(tmpdir) / "product_links.csv"
with purchases_csv.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(
handle,
fieldnames=[
"purchase_date",
"retailer",
"order_id",
"line_no",
"normalized_item_id",
"catalog_id",
"raw_item_name",
"normalized_item_name",
"image_url",
"upc",
"line_total",
],
)
writer.writeheader()
writer.writerow(
{
"purchase_date": "2026-03-14",
"retailer": "giant",
"order_id": "g1",
"line_no": "1",
"normalized_item_id": "gnorm_skip",
"catalog_id": "",
"raw_item_name": "TEST ITEM",
"normalized_item_name": "TEST ITEM",
"image_url": "",
"upc": "",
"line_total": "1.00",
}
)
with catalog_csv.open("w", newline="", encoding="utf-8") as handle:
writer = csv.DictWriter(handle, fieldnames=review_products.build_purchases.CATALOG_FIELDS)
writer.writeheader()
result = CliRunner().invoke(
review_products.main,
[
"--purchases-csv",
str(purchases_csv),
"--queue-csv",
str(queue_csv),
"--resolutions-csv",
str(resolutions_csv),
"--catalog-csv",
str(catalog_csv),
"--links-csv",
str(links_csv),
"--limit",
"1",
],
input="s\n",
color=True,
)
self.assertEqual(0, result.exit_code)
with resolutions_csv.open(newline="", encoding="utf-8") as handle:
rows = list(csv.DictReader(handle))
self.assertEqual("skip", rows[0]["resolution_action"])
self.assertEqual("pending", rows[0]["status"])
def test_review_products_creates_catalog_and_resolution(self): def test_review_products_creates_catalog_and_resolution(self):
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
purchases_csv = Path(tmpdir) / "purchases.csv" purchases_csv = Path(tmpdir) / "purchases.csv"