Restore skip and move search to find
This commit is contained in:
@@ -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.
|
||||
- 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
|
||||
|
||||
** acceptance criteria
|
||||
1. replace `[l]ink existing` with `[s]earch` in review prompt:
|
||||
- `[#] link to suggestion [s]earch [n]ew [x]exclude [q]uit >`
|
||||
1. replace `[l]ink existing` with `[f]ind` in review prompt:
|
||||
- `[#] link to suggestion [f]ind [n]ew [s]kip [x]exclude [q]uit >`
|
||||
2. implement search flow:
|
||||
- on `s`, prompt: `search: `
|
||||
- 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
|
||||
- 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`
|
||||
- datetime: 2026-03-20 13:32:09 EDT
|
||||
- datetime: 2026-03-20 13:34:57 EDT
|
||||
|
||||
** notes
|
||||
- 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.
|
||||
- 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)
|
||||
|
||||
@@ -382,7 +382,7 @@ def prompt_resolution(queue_row, related_rows, purchase_rows, catalog_rows, queu
|
||||
prompt_bits = []
|
||||
if suggestions:
|
||||
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)
|
||||
action = click.prompt("", type=str, prompt_suffix=" ").strip().lower()
|
||||
if action.isdigit() and suggestions:
|
||||
@@ -403,6 +403,15 @@ def prompt_resolution(queue_row, related_rows, purchase_rows, catalog_rows, queu
|
||||
if action == "q":
|
||||
return None, None
|
||||
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:
|
||||
query = click.prompt(click.style("search", fg=PROMPT_COLOR), default="", show_default=False).strip()
|
||||
if not query:
|
||||
|
||||
@@ -219,7 +219,7 @@ class ReviewWorkflowTests(unittest.TestCase):
|
||||
self.assertIn("Review guide:", result.output)
|
||||
self.assertIn("Review 1/1: MIXED PEPPER", 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 | ")
|
||||
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)
|
||||
@@ -401,7 +401,7 @@ class ReviewWorkflowTests(unittest.TestCase):
|
||||
"--limit",
|
||||
"1",
|
||||
],
|
||||
input="s\nmixed pepper\n1\nlinked by test\n",
|
||||
input="f\nmixed pepper\n1\nlinked by test\n",
|
||||
color=True,
|
||||
)
|
||||
|
||||
@@ -492,13 +492,85 @@ class ReviewWorkflowTests(unittest.TestCase):
|
||||
"--links-csv",
|
||||
str(links_csv),
|
||||
],
|
||||
input="s\nzzz\nq\nq\n",
|
||||
input="f\nzzz\nq\nq\n",
|
||||
color=True,
|
||||
)
|
||||
|
||||
self.assertEqual(0, result.exit_code)
|
||||
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):
|
||||
with tempfile.TemporaryDirectory() as tmpdir:
|
||||
purchases_csv = Path(tmpdir) / "purchases.csv"
|
||||
|
||||
Reference in New Issue
Block a user