Skip to content

Narrow component search to each project's latest scan (Q2) - #218

Merged
haksungjang merged 4 commits into
mainfrom
feat/search-latest-scan-only-q2
Aug 23, 2026
Merged

Narrow component search to each project's latest scan (Q2)#218
haksungjang merged 4 commits into
mainfrom
feat/search-latest-scan-only-q2

Conversation

@haksungjang

Copy link
Copy Markdown
Contributor

Summary

  • Component search (/v1/search palette and /v1/search/results?kind=components) joined scan_components through every scan a project had ever run instead of just its current one, so search cost grew with retained scan history (up to 30x under the scan-series retention policy) rather than catalog size. Both surfaces now resolve through services.scan_resolution.latest_succeeded_scan_select, the same helper vulnerability/licence search already used.
  • Contract change: a component that only ever appeared in an older, since-superseded scan no longer surfaces in either search surface. "Was it ever here" is answered by the scan detail/history screens instead.
  • Widened beyond the plan's literal file citation (search_service.py) to also cover search_results_service.py's component search, matching the precedent set by Q1: the results-page surface had the identical defect, and M3's own miniature regression test (test_search_query_plan_contracts.py) measures the results-page query, so leaving it unfixed would have made that test's "flat" assertion false.
  • Removed the inventory page's "Search every scan" empty-state escape hatch: it linked to /search, which now reads the identical latest-scan-only scope inventory already used, so the link would always land on another empty result.
  • EN/KO docs updated for both surfaces (docs-site/docs/user-guide/search.md, inventory.md + KO mirrors); SearchPage.tsx's on-screen scope indicator flips the Components tab from "all scans" to "current scan".

Test plan

  • New tests seed TWO real scans per project from disjoint captured CycloneDX fixtures (tests/fixtures/sbom_ingest/realistic.cdx.json + centos7-rpm-no-os.cdx.json) via the shipped persist_sbom_components, asserting an old-scan-only component drops out and a latest-scan component stays found (test_search_api.py, test_search_results_api.py).
  • M3's miniature EXPLAIN-plan test flipped from asserting growth with scan history to asserting flatness (test_search_query_plan_contracts.py::test_scan_components_rows_stay_flat_across_scan_history), per the note M3 left for this exact landing.
  • Heavy nightly baseline (test_search_explain_load_baseline.py) updated to compare the shipped latest-only query against a rebuilt legacy all-history variant (was skip-by-default locally, no load-test seed).
  • ruff check, mypy . (773 files), backend tests/unit + tests/integration green locally (10 pre-existing failures reproduce in isolation and are this laptop's disk-usage guard tripping at 95%+, unrelated to these files).
  • Frontend typecheck, vitest run (2491 passed), lint (0 errors), i18n:check green.
  • node tools/ko-style/lint.mjs --all --fail-on S2: 0 findings. node tools/em-dash/lint.mjs --base origin/main: clean.

Component search (both the palette and the full results page) joined
scan_components through every scan a project had ever run instead of just
its current one, so search cost grew with retained scan history (up to 30x)
rather than catalog size. Both now resolve through
services.scan_resolution.latest_succeeded_scan_select, the same helper
vulnerability/licence search already used, closing that asymmetry.

Contract change: a component that only ever appeared in an older,
superseded scan no longer surfaces in search. Pinned with real captured
CycloneDX scan-history fixtures (two disjoint SBOMs across two scans of one
project) plus M3's miniature EXPLAIN-plan test, whose growth assertion now
asserts flatness instead.

Also updates the inventory page's now-dead "search every scan" escape
hatch (search reads the same latest-scan scope inventory already did) and
the EN/KO user docs for both surfaces.
Q2 added a scan-id resolution query as statement 0 ahead of components
search's COUNT/page/facet queries, shifting every later index by one. The
EXPLAIN-capture tests still pointed at index=1 expecting the page SELECT,
which now lands on COUNT instead — CI's fresh database exposed the mismatch
(local runs happened to pass by coincidence). Bumped to index=2 in both the
PR-gate miniature test and the nightly heavy baseline helper.
The EXPLAIN-based physical row count (total_actual_rows on the
scan_components relation) was fragile against the CI integration suite's
large shared database: at that scale Postgres can choose a plan for the
marker's highly-selective ILIKE where scan_components never surfaces as a
named EXPLAIN node, even though the join still correctly touches it (the
real search call logged the correct total=1 both times in the failing run,
so the search itself was never wrong, only the plan introspection).

Reasserts the same property against the search RESULT instead, which is
also what the plan's own regression contract names and is insensitive to
which access path the planner picks.
Neither component search surface had a test for a team with no succeeded
scan anywhere, so the empty-scan-id short-circuit each _components/
_search_components takes before building the main join was unexercised.
Coverage gate flagged both lines on the PR.
@haksungjang
haksungjang merged commit 23e6c68 into main Aug 23, 2026
30 checks passed
@haksungjang
haksungjang deleted the feat/search-latest-scan-only-q2 branch August 23, 2026 17:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant