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: the full pending → scouting → filtering → confirmed
loop is implemented: GET .../scout (category inference), POST
.../confirm-category (locks category, populates facets + initial results),
POST .../apply-filter (Package/Manufacturer only — narrows facets +
results), POST .../confirm (locks a pick). Not yet built: per-component
param filters (Capacitance/Tolerance/...), pagination, removing an applied
filter, retry/re-scout, and export (see docs/backlog.md).
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.POST .../confirm-category — user
accepts the top candidate or picks an alternative; row locks in
resolved_category_id/resolved_category_name, resets applied_filters,
and moves to filtering.apply-filter call after) eagerly calls
LcscAdapter.query_facets(category_id, applied_filters) and
LcscAdapter.search(category_id, params, applied_filters, page=1),
storing results on row.facet_groups/row.search_results. POST
.../apply-filter lets the user narrow by a Package or Manufacturer facet
value; per-component param filters (Capacitance/Tolerance/...) are not
wired — the request shape LCSC expects to select a param value was never
captured live, so it's deferred rather than guessed. applied_filters is
kept strictly as the raw LCSC filter payload fragment (no app-internal
keys mixed in) so it passes straight through with no translation layer.POST .../confirm — user selects a specific
part by productCode from row.search_results; row locks
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 6–7 and per-param filtering within step 4 are the remaining gaps — see
docs/backlog.md → Todo → Scouting Loop / API.
docs/architecture.md — full intended module designdocs/backlog.md — current Done/Todo status per component