The scouting loop drives each BomRow from pending to confirmed through a
user-in-the-loop cycle: AI infers an LCSC subcategory → user confirms or
overrides it → parametric search within that subcategory → user picks a part.
Design source of truth: docs/architecture.md (§5 "Scouting Loop", "User
Flow", "System Flow"). Build status source of truth: docs/backlog.md.
Current reality: only the first hop (pending → scouting, i.e. category
inference) is implemented, via GET /session/{id}/row/{row_id}/scout. The
rest of the loop (confirm-category, search, confirm, export) is
designed but intentionally deferred until inference itself is trusted through
interactive testing (see docs/backlog.md → API section, "deferred" notes).
RowState per bom_assistant/session/models.py. Solid edges are built,
dashed edges are designed but not yet implemented.
Source: diagrams/scouting_loop_state.puml
Note: today's orchestrator.scout_row always transitions to scouting, even
when resolve() returns zero candidates (it does not auto-flag the row) —
the empty-result case is left as scout_candidates: [] for the UI to show
"no candidates found," rather than kicking the row to flagged. Auto-flagging
on empty results, and a flag_reason field to distinguish that from
normalizer-level flagging, was scoped out as part of the deferred loop work.
Actors: User, API (bom_assistant/api/routes.py), Orchestrator
(bom_assistant/scouting/orchestrator.py), LcscCategoryResolver
(bom_assistant/suppliers/lcsc/category_resolver.py), LcscAdapter
(bom_assistant/suppliers/lcsc/lcsc.py). Dashed arrows = not yet built.
Source: diagrams/scouting_loop_sequence.puml
POST /upload runs
parse_bom → map_columns → normalize, returns a BomSession with all
rows pending (or flagged for DNP/non-electronic rows).pending row, GET .../scout calls
LcscCategoryResolver.resolve(row) (file-cached, falls back to a Claude
Haiku call) and returns up to 3 ranked LCSC subcategory candidates. Row
moves to scouting. Can be re-triggered freely.resolved_category_id and moves to
filtering.LcscAdapter.query_facets(category_id, filters) calls LCSC's
real facet API (wmsc.lcsc.com/ftps/wm/product/query/param/group) to get
manufacturer/package/param filter groups scoped to the category and
whatever's already selected; LcscAdapter.search(category_id, params,
filters, page) calls the matching product-list endpoint. Both were
reverse-engineered from live traffic and verified end-to-end against real
category-resolver output — see docs/backlog.md. No GET .../facets or
POST .../search route exists yet.confirmed_pick and moves to confirmed.scouting with adjusted parameters rather than being a
dead end.confirmed (or explicitly
skipped), GET .../export collates them into an order-ready BOM.Steps 3–7 are why the API currently exposes only /upload and /scout: the
loop is built incrementally, and the next slice only gets built once category
inference (step 2) has been exercised enough through the HTMX test page to
trust building on top of it.
docs/architecture.md — full intended module designdocs/backlog.md — current Done/Todo status per component