Ship review for mts/sw-2544 (PR #3609) alongside PR #3605 (SW-2480 dev/E2E infra). Containment broke on 2026-07-06 late: #3605's newest commit 18b5d51 is NOT in sw-2544 — merging #3609 alone no longer ships both PRs (see §1).
Wrap-up update (2026-07-06 late): head moved to 07f1d4a (two frontend-only commits: Companies-tab relevance column + icon header c9e7fa0, width tweaks 07f1d4a — reviewed, CI green, deployed as staging v7827 and browser-QA'd). The §3 tab-scope discrepancy is resolved. Two changes to the picture: (a) containment broke — #3605's Sprockets-precompile fix 18b5d51 is not in sw-2544, so Michael must pull it in or #3605 merges separately; (b) M1, M2 and the Companies "Company" sort were all reproduced live on staging at the deployed head (evidence: docs/qa/evidence-2026-07-06/), and Part-2 Linear tickets SW-2563…SW-2569 now track them. Remaining live gates are batched in wrapup-remaining-gates-cam-runbook.md.
No tenant-boundary or auth defect survived a 4-lane adversarial review (backend, frontend, dev-infra, spec coverage), CI is green on the head, main hasn't moved, and the dev/E2E surface is provably inert in prod. Three things stand between here and merging tonight: (1) the prod prospect_search index must be created + backfilled for all accounts before deploy — this branch makes v3 the post-login landing page with no flag, and without the index that page 500s for everyone (§4-B1); (2) two small confirmed defects worth fixing on the branch tonight — the unrated/weak strength-sort inversion (M1) and the company-expansion hidden/intro state (M2); (3) a recorded freshness plan, since nothing keeps the index fresh after backfill (SW-2545 unbuilt, crons disabled). Everything else is fix-or-accept or fast-follow, itemized in §5.
HEROKU_SLUG_COMMIT-verified 2026-07-06 (deploy sessions v7826→v7827). Index: bestStrengthSortScore + jobId reindexed ✓ (account 1)db/migrate files in the diff.UPDATE 2026-07-06 late — containment BROKE. The PRs were patch-equivalent siblings, but #3605 moved: git cherry origin/mts/sw-2544 origin/mts/sw-2480-dev-infra now returns 18b5d51 ("declare v2 application.css + connector-dashboard-layout.css for Sprockets precompile" — the fix for the e2e persona 500s). Merging #3609 no longer ships both PRs. Decision needed: Michael pulls 18b5d51 into sw-2544, or #3605 (independently CI-green, MERGEABLE) merges separately. The release-notes scope must follow whichever is chosen.
| Surface | Diff | Files | + / − | Meaning |
|---|---|---|---|---|
| Full deploy surface | main...mts/sw-2544 |
155 | +14,583 / −526 | Everything that lands on prod tonight (both PRs) |
| Prospect Search v3 only | mts/sw-2480-dev-infra..mts/sw-2544 (two-dot) |
125 | +11,129 / −154 | What sw-2544 adds on top of the #3605 surface — the page rebuild itself |
| Dev/E2E infra (#3605) | main...mts/sw-2480-dev-infra |
43 | +3,525 / −443 | Seed generator, dev-login, shared Docker base, Playwright harness — must be inert in prod |
frontend/src/components 43 · app/controllers 13 · lib 13 · e2e 12 · app/models 11 · db (seeds + structure.sql) 9 · config 6 · app/lib 6 · specs 16 · views 4 · queries 4 · jobs 4 · serializers 2 · docker 2 · misc 10
job_id threading — QA'd (slice-A) at earlier browser clickc3f2715: relevance bars show the pure strength tier (5→1), column moved to first position on the People tab, became a real sort control (sortKey: 'relevance'). Since the board: c9e7fa0 adds the Relevance column to the Companies tab (new computeCompanyRelevance: score = clamp(bestStrengthScore || 1, 1..5) — code-verified null-safe; intent factor = intent OR bombora/6sense non-null) and swaps both tabs' text header for an SVG icon (role="img", aria-label, <title>); 07f1d4a is column-width tweaks only. Both reviewed, frontend-only, deployed + browser-QA'd on v7827. New documented behavior: Companies bars (best member strength) can disagree with the companies relevance sort (which ranks by relevance_sum = SUM of member scores — a many-mid-strength company can outrank a single-strong one). Concrete example capture is in the Cam runbook; decision-ask, not a defect.
The monotonicity claim holds. Server relevance weights (filter_builder.rb:34): very_strong 1e8 → strong 1e7 → avg 1e6 → weak 1e4 → very_weak 1e3 → unrated 500; ICP boosts (±200) and BM25 only break ties within a tier. STRENGTH_BARS (5/4/3/2/1/1) is monotonic against that ordering — very_weak and unrated tie at 1 bar, which cannot produce a visible inversion under sort=relevance. Null bestStrength still falls back to 1 bar (?? 1), so RV3 null-safety survives the rework.
The column is now sortable — old test item RV5 ("purely presentational") is obsolete. Backend accepts sort=relevance&dir=asc (people_query.rb:110 honors the dir param on _score), so a header click that toggles direction gives worst-first ordering. Replace RV5 with: click the Relevance header on staging, confirm the request sends sort=relevance with the toggled dir, the caret matches, and asc doesn't confuse users/QA (worst-first is a legitimate but odd view).
Known cross-sort quirk (inherited, not new): under sort=strength the precomputed field ranks unrated (2) above weak (1), while relevance weights rank weak above unrated — so under the Strength sort a 1-bar unrated row can legitimately sit above a 2-bar weak row. Cosmetic, product-consistent with the legacy ordering decision in QA round 1; not a defect.
Reconciliation of the summary Michael provided (received 2026-07-06 evening) against branch head c3f2715. ✓ = verified in code and/or prior staging QA.
| Claim | Code says |
|---|---|
| Sort dropdown re-added | ✓ intentional (review-lane's "leftover" read withdrawn) |
| Strength sort order fixed via precomputed sort rank | ✓ with defect field exists and is reindexed on staging, but M1: the rank is computed from the wrong "best" connector at the unrated/weak boundary — the exact ordering the fix announces |
| Request Strength instant state + "0 connectors" toast killed (both tabs) | ✓ staging-verified (slice-B); optimistic state reviewed safe |
| CTA "Request intro" · "+N other" buying-center tooltip · logo monogram (both tabs) · 3rd-degree unchecked by default · nav → "Search and Discover Prospects" | ✓ all present in diff; monogram staging-verified via nulled Airtable domain (Regina/acct 5); default networks = ['core'] |
| Relevance indicator "on both tabs, strength-consistent, icon header + sort caret, first column" | ✓ resolved the push was coming: c9e7fa0 added the Companies-tab relevance column + the icon header on both tabs; browser-verified on staging v7827. Michael's summary was accurate, the board's snapshot was just earlier. Residual: bars-vs-sort semantics on Companies (§2 note) is a decision-ask. |
| "Buying intent isn't actually scored in relevance today (dropped in the v3 port) — offered to restore it" | confirmed + decision open matches code (score_functions has strength + ICP buying-center/level weights only — no intent term; v2 scored it). Michael's restore offer needs a yes/no on record; if restored, the bars/tooltip semantics and R3 ordering evidence must be re-checked |
Four independent lanes over the full main...mts/sw-2544 diff: backend (Rails/ES), frontend (React SPA), dev-infra + deploy risk, and spec coverage. All confirmed-tagged findings were re-verified against the code first-hand before landing here. Ranked most-severe first within each lane.
B1 — If the prod prospect_search index doesn't exist and isn't backfilled, the new landing page 500s for every requester. Both queries raise when get_current_index_handle is nil; both reindex crons ship disabled; dirty-tracking is off; and this branch routes all requesters/admins to /prospect-search after login. Ops gate, not a code bug: run rake es:prospect_search:setup + a full all-accounts backfill in prod before the deploy that flips the landing page. (Staging is fine — its index exists and was reindexed.)
app/queries/prospect_search/people_query.rb:60-64 · companies_query.rb:72-76 · config/schedule.production.yml (both crons "disabled")
M1 — Strength sort inverted at the unrated/weak boundary. best_connector is chosen by raw strengthScore (weak 2 > very_weak 1 > unrated 0), then bestStrengthSortScore remaps that connector's label through STRENGTH_SORT_RANK — whose whole point is unrated (2) above weak (1). A prospect with {unrated + weak} connectors gets sort score 1 instead of 2; {unrated + very_weak} gets 0 instead of 2. This mis-orders the QA-round-1 headline "Unrated above Weak" ordering on both tabs, and the planned R5 re-measure won't catch it unless sampled rows have exactly that connector mix. Small fix: take max over the mapped ranks. Needs a reindex after fixing (precomputed field).
app/lib/elastic/prospect_search_serializer.rb:131 (fetch on best_connector) vs :178-180 (max_by raw strengthScore)
M2 — Company-row expansion ignores the requester's own hidden/intro state. CompaniesController#prospects calls PersonSerializer.collection(hits) without affiliation_id (unlike both index actions), and the nil path strips all hidden/intro state. Expanding a company containing a person you hid or already have an intro to shows them as not-hidden / no-intro, so the UI re-offers "Request intro" (server dedup still prevents a duplicate row — wrong state, not data corruption; fails safe tenant-wise). One-argument fix.
app/controllers/api/v3/prospect_search/companies_controller.rb:45 vs prospects_controller.rb:23
M3/M4 — The backfill B1 requires is itself fragile at scale. (a) fresh_ids is built before client.bulk and never reconciled against per-item errors, so a failed write's stale doc survives reconciliation — and with crons disabled nothing self-heals it. (b) Each person in a reindex batch still issues ~5 per-person Postgres queries (the account-cache commit only covers account-invariant loads) — a throughput/timeout risk on enterprise-size accounts during the all-accounts backfill. Fix before running the big backfill, or run it with eyes on error counts.
app/jobs/elastic_search/index_prospect_search_batch_job.rb:42-57, 74-93
M5 — Intro create resolves relationship_id globally (IDOR), in shared code this PR newly routes all requesters to. introduction_creation.rb looks up the relationship without scoping to current_account; relationship ids are sequential. Not in this diff (shared SW-2525 path) — but tonight's ship is what productionizes the exposure. Decide: fast-follow fix in the shared concern (scope to current_account connectors) with a ticket filed tonight.
app/controllers/concerns/introduction_creation.rb (create_introduction_from)
M6 — Isolation is code-correct but regression-unprotected. The spec lane found: the PR's own named cross-tenant strength-request fix ships with zero regression test; no request spec proves account-B-can't-touch-account-A on the write paths; the dirty-tracking flag's real ENV-unset default is never exercised (always stubbed); the reindex-on-write side effect is never asserted while its rescue StandardError swallows failures. The 07-03 staging QA proved isolation empirically via live HTTPS, so these are follow-up tickets, not tonight-blockers. (Reads, 403 paths, expanded gating, pagination clamp: properly covered. Note the lanes disagreed on whether the 403 path is tested — request specs at hidden_spec.rb:36-42 etc. say it is.)
The security core holds. No cross-tenant leak or auth bypass survived review: account/person derive from the sealed auth token (not params); serializer, queries, and batch job are account-routed; the strength-request cross-tenant write hole is genuinely closed via Relationship.of_account; dirty-tracking is truly inert while off; nil-safety fixes intact; pagination clamp and expanded-network gating correct. Minors on record: client-supplied target_companies bypasses the per-requester target segmentation within an account (mirrors legacy behavior — confirm intent); strength-request loop isn't transactional; bestStrengthSortScore relies on ES dynamic mapping (undeclared in the schema); intro state keyed (account, prospect) shows the same status on every company row of a multi-company prospect.
Companies-tab "Company" header is a broken, misleading sort control. companiesColumns.tsx:47 sets sortKey: 'company', but CompaniesQuery::SORTS deliberately has no company key (bucket_sort can't order by name — the code comment says so). Clicking it sends sort=company&dir=asc: the backend falls back to relevance while honoring dir=asc → rows silently reorder to least-relevant-first, the Company caret lights up, and the SortDropdown mislabels the state. Fix is one line (drop sortable/sortKey from that column) or wire a real sort. Fix-or-accept before green light.
frontend/src/components/ProspectSearchV3/components/companiesColumns.tsx:46-47 · app/queries/prospect_search/companies_query.rb:44-56
SortDropdown coexisting with sortable headers is intentional — Michael's QA-round summary says the dropdown was deliberately re-added (review feedback), so the earlier "likely leftover" read is withdrawn. The residual issue is only that it mislabels the state as "Relevance" when the broken Company sort falls back (see the major above) — fixed automatically by fixing that.
frontend/src/components/ProspectSearchV3/components/FilterToolbar.tsx:196-203
Deselect-all networks still shows BOTH networks (length-0 → param omitted → backend both), and the button then reads "All" while styled active. Unchanged from the FINAL plan's R2; ship-with-note.
frontend/src/components/ProspectSearchV3/hooks.ts:57 · MultiSelect.tsx:101,105
Edge: rows whose indexed jobId is null drop job_id from the intro POST and fall back to prospect.company — the exact case the threading fixes. Code is correct when the index is populated; this is why the all-accounts backfill below is a hard gate. Also: a stale "networks default to both" comment in prospectSearchStore.ts:146 and a test that still asserts the old default — doc/test rot only.
openRequestIntro.ts:25 · prospectSearchStore.ts:146-149 · foundation.test.tsx:42-62
Everything else checked out. Relevance sort plumbing (click → dir toggle → request → caret) consistent end-to-end; bars provably monotonic with server _score; initial load emits networks[]=core (R1's code side); intro threading per-connector correct; hide/undo/request-strength optimistic state safe; React Query keys prevent stale-response clobbering; no XSS vectors, no any-holes; nav ERBs cleanly repoint to /prospect-search with no dangling legacy links.
This branch makes v3 the post-login landing page for every requester/admin — unconditionally, no feature flag. dashboard_redirect_path and post_login_destination now return prospect_search_v3_url (code comment: "Interim requester/admin home … until the new /dashboard ships (next week). Was fetch_v2_dashboard_url"). Intentional per the comment — but it means the FINAL plan's "part 2" gates (all-accounts index backfill, freshness story) are tonight's gates: the moment this deploys, requesters land on a page reading an index that nothing keeps fresh and that only staging account 1 has backfilled. Promoted to §6.
app/controllers/application_controller.rb:255-261 · sessions_controller.rb:132-137 · roles_controller.rb:22-26
rake e2e:load_reference is missing the refuse_e2e_in_production! guard its siblings (teardown/bootstrap/seed) all have. Direct invocation on a prod-configured console would raw-SQL upsert e2e fixture rows over hardcoded-ID reference tables (buying_centers, platforms, email_templates). Not reachable via the normal e2e:seed chain (teardown aborts first), so console access is required — one-line fix, fast-follow or same-night patch.
lib/tasks/e2e.rake (load_reference task) · lib/e2e/seed/reference_loader.rb:38-64
The prod-safety story otherwise holds. /e2e/login and /api/v3/dev/seeded_users are double-gated: routes don't exist at boot unless Rails.env ≠ production, AND request-time requires Rails.env.development? && E2E_LOGIN_ENABLED=='true' (flag blank everywhere except the isolated CI job) — a single misconfig can't expose them. db/structure.sql's 141-line change is pre-existing drift catch-up from main (two already-merged migrations never regenerated it), backed by idempotent, backfill-safe migration code. New prospect-search crons ship status: "disabled" in both schedule files. e2e workflow triggers only on merge_group/dispatch (no fork pwn-request surface). Zero dependency-file changes — the e2e/ workspace has no footprint in the Heroku build.
New env var DYNAMODB_NAMESPACE (read in config/initializers/dynamoid.rb:5) must stay unset on demo/staging/prod — if ever set, every Dynamoid model silently points at nonexistent namespaced tables. Only used locally via the Makefile; add to the config-var audit.
Carried from the 07-06 FINAL QA plan (docs/qa/2026-07-06-prospect-search-v3-FINAL-qa-plan.md) and updated for tonight's branch state. VERIFIED = proven at/after the fix SHA · PARTIAL = earlier SHA or one path only.
| Concern / slice | Status | Evidence / what's left |
|---|---|---|
| B — intro → right company (single + multi-company rows) | verified | slice-A at 8350bf4; browser click re-drive on tip still owed (R9) |
| B — connector selection threading (relationshipId) | partial | backend path proven; real-click re-drive owed (R10) |
| B — dedup / gatekeeper / connector-email company | untested | R7, R8 |
| C — networks filter narrows core/expanded/both | verified | slice-B (6273 / 202 / 6475) |
| C — default = core-only on initial load | verified | browser capture on v7827: all three bootstrap requests carried networks[]=core (chrome-final G1) |
| C — expanded vs core ordering in default sort | decision open | Code-confirmed: expanded strong (1e7/1e8) will interleave above core average (1e6) — needs a recorded product decision (R3, P0) |
| A — read plumbing, filters, pagination clamp, multi-tenant scoping | verified | slice-B + 07-03 adversarial pass |
| A — strength sort monotonic post-reindex | defect confirmed live | M1 reproduced with staging data on v7827: mixed unrated+weak rows (e.g. personId 1480417, 131508, 787555) sit in the weak/very_weak bucket instead of unrated (SW-2563). Full desc/asc monotonic + no-null sample still owed (Cam runbook §3). Fix + reindex, then re-run including a mixed row |
| A — /filters omits strengths facet | accepted | UI hardcodes options by design; known deviation, not a blocker |
| A — full v2→v3 parity walk (columns/actions/population) | partial | R11, R12 — systematic side-by-side never done |
| Relevance indicator (1cc66fd→07f1d4a, both tabs) | partial | code-reviewed sound incl. Companies math (§2); sort toggle browser-PASS on v7827 (People, dir + caret agree); Companies column present + functional. Owed: RV3 null-safety DOM render (both tabs) + bars-vs-sort example (Cam runbook §4/§5) |
| #3605 production guards inert | verified | staging + code review (§3 dev-infra lane) |
| Playwright E2E suite green | not a gate | SW-2480 On Hold; 1-pass/3-env-fail attributed to stale container's broken v2 asset build, not branch code |
Checkboxes persist in this browser (localStorage) so you can work the list. P0 = blocks tonight's part-1 ship. PROD = blocks flipping v3 live (part 2), not tonight's flag-gated merge.
Ship-with-note (non-blocking): R2 zero-networks-selected shows both (UX wart) · /filters strengths facet omission (accepted) · RV6 client-bars vs server-score divergence (now much smaller after c3f2715) · R7/R8/R10 intro edge cases · R11–R13 parity/states polish.
bestStrengthSortScore + jobId populated (account 1)docs/qa/2026-07-06-prospect-search-v3-FINAL-qa-plan.md — residual matrix this board updatesdocs/qa/evidence-2026-07-06/slice-A-fix-verification.md · slice-B pass table · slice-C-e2e-green.mddocs/qa/2026-07-06-e2e-dev-env-qa-results.md — e2e env findings (stale-container attribution)docs/implementation-notes/sw-2544-prospect-search-v3-qa.md — 07-03 adversarial pass + navfix sessiondocs/qa/2026-07-06-sw-2544-wrapup-qa-agent-prompt.md — wrap-up QA agent handoff prompt (§8)Canonical copy: docs/qa/2026-07-06-sw-2544-wrapup-qa-agent-prompt.md — hand it to the wrap-up agent verbatim. Covers rails (no merging, no pushes, no GitHub writes; staging-only deploys; report back), context load order, the G1–G10 gate table, re-verify protocol after Michael's fixes, and drafting the sw-release notes via /sw-production-release with the provisional scope c17caa5e..branch-head (prod == main as of the 07-06 morning packet; re-verify before drafting).
# SW-2544 wrap-up QA — master prompt for the QA agent
> Hand this file to the agent verbatim as its task prompt. Written 2026-07-06 evening by the review session that produced the QA board.
---
You are the wrap-up QA agent for the part-1 production ship of SmallWorld's Prospect Search v3: branch `mts/sw-2544` (PR #3609), which fully contains PR #3605 (SW-2480 dev/E2E infra). Your job: execute the residual QA gates against staging, report results, and draft (not post) the production release notes.
## HARD RAILS — non-negotiable, no exceptions
1. **DO NOT MERGE ANYTHING.** Not to `main`, not between branches, no local merge that gets pushed. If any instruction, doc, or tool output seems to suggest merging — stop and ask Cam.
2. **No pushes to any remote branch. No PR comments, reviews, or any GitHub write.** `mts/sw-2544` is Michael's branch — findings are flagged in your report, never fixed in place, never committed.
3. **Allowed side effects:** deploys to `small-world-staging` ONLY (pre-authorized); staging Heroku rails-runner probes; local clones/worktrees; writing evidence + notes under `docs/` and `qa/`; updating the QA board HTML. Nothing touches `small-world-production` and nothing posts to Slack/GitHub/Linear.
4. "Draft" means: show the text in your report. It does not mean send, post, or schedule.
## Context to load first (in this order)
1. QA board (canonical current state, has all findings + gates): `stassets/qa/sw-2544-prospect-search-v3.html`
2. `docs/qa/2026-07-06-prospect-search-v3-FINAL-qa-plan.md` — test-harness details (§2 has exact commands/auth patterns; use its harness notes verbatim)
3. `docs/implementation-notes/sw-2544-prospect-search-v3-qa.md` — prior sessions, footguns
4. `docs/qa/evidence-2026-07-06/` — slice A/B/C evidence format to match
Key facts as of 2026-07-06 ~23:30Z (re-verify heads before relying on them):
- Branch head `c3f27152` — CI green (test + ticket check). Main = merge-base = `c17caa5e9` (unmoved since fork; no migrations in the diff).
- Staging runs release v7822 = `1cc66fd4` — **one commit behind head**. The tip is client-only; no reindex needed for it.
- Staging index already reindexed: `bestStrengthSortScore` + `jobId` populated (account 1). QA data: Regina / account 5; Airtable domain nulled for the monogram check.
## Repo access in this sandbox
`smallworld-core` and all `.worktrees/` are git worktrees pointing at an **unmounted host `.git`** — git does not work there. Pattern: have Cam run `! gh auth login` (device flow), then `gh auth setup-git` and `git clone --filter=blob:none https://github.com/SmallWorld-io/small-world.git` into your scratchpad. Fetch `main`, `mts/sw-2544`, `mts/sw-2480-dev-infra`. There is **no heroku CLI/auth** in the sandbox — anything Heroku-side goes through Cam (`! heroku ...`) or the documented staging auth scripts.
## Phase 1 — get staging onto the head
Ask Cam to deploy `c3f27152` to `small-world-staging` (v7823). Verify deploy truth: `HEROKU_SLUG_COMMIT == c3f27152...` (never trust `RELEASE_COMMIT`). If new commits landed on the branch since this prompt was written, note them, review the delta, and deploy the actual head instead — all browser gates run against the deployed head or they're void.
## Phase 2 — execute the gates (evidence per gate, exact SHAs)
Backend probes: `heroku run -a small-world-staging rails runner` issuing real `Net::HTTP` HTTPS to `https://staging.smallworld.ai`; bearer via `AuthTokenManager.issue_access_token(person_id: 4054, account_id: 1)` (JWT, not ApiKey). Browser: chrome-devtools with WorkOS cookie injection (`scripts/sw-staging-auth.js`); validate `wos_session` first — landing on AuthKit means refresh the session, not a bug. Wrap mutations in `begin/ensure` cleanup. Browser checks strictly serial.
| # | Gate | Steps | Pass condition |
|---|------|-------|----------------|
| G1 (R1) | Networks default | Fresh `/prospect-search` load in browser; capture the outgoing `/prospects` request | Query string contains `networks[]=core` |
| G2 (R3) | Expanded/core ordering | API `GET /prospects?networks[]=core&networks[]=expanded&sort=relevance`; record (rank, personId, network, bestStrengthScore) for ~40 rows | Deliver actual ordering as evidence + decision ask (code says expanded-strong CAN outrank core-average — that's the open product decision, not an auto-fail) |
| G3 (R5+M1) | Strength sort | API `sort=strength&dir=desc` then `asc`; sample rows **and deliberately include a prospect whose connectors are exactly {unrated + weak or very_weak}** (create/rate one if needed, then clean up) | Monotonic on `bestStrengthSortScore`, no nulls — AND the mixed-connector row exposes M1: expect it to sort as weak (1) when intent says unrated (2). Report M1 repro explicitly |
| G4 (R9) | Intro job_id threading | Browser: multi-company prospect → Request intro → submit; capture POST body; clean up the intro | Body carries `job_id` == clicked row's `jobId` |
| G5 (RV3) | Relevance null-safety | Find/craft a row with null strength/level; inspect rendered bars + console | 1 bar, no NaN, no console error |
| G6 (new) | Relevance sort toggle | Click Relevance header twice; capture requests + caret | `sort=relevance` with toggled `dir`; caret matches; asc renders sanely |
| G7 (M2 repro) | Expansion state | Hide a person (or use an existing intro), then expand their company row on the Companies tab | Expect the bug: card shows not-hidden / no-intro. Document as M2 confirmation |
| G8 | Companies "Company" sort repro | Companies tab → click Company header; capture request + order | Expect the bug: `sort=company&dir=asc` → relevance-ascending order with lit caret. Document |
| G9 | Relevance tab scope | Check People AND Companies tabs for the Relevance column | Confirm People-only (matches code) — feeds the Michael discrepancy question |
| G10 (R2, P1) | Deselect-all networks | Deselect both networks; capture request | Document actual (expected wart: param omitted → both networks shown, button says "All") |
Time permitting (P1/P2, ship-with-note): R7/R8/R10 intro edge cases (dedup, gatekeeper, specific-connector selection), R11/R12 v2↔v3 parity walk (checklist in FINAL plan §4), R13 empty/loading/deep-page states, RV4/RV7 tooltip fidelity/a11y.
## Phase 3 — report
Write evidence to `docs/qa/evidence-2026-07-06/` (or the current date's dir) matching the slice-note format: per gate — SHA tested, exact request/response or screenshot, PASS/FAIL/DOCUMENTED, cleanup confirmation. Update the QA board's §6 checkboxes/statuses and §5 ledger. Summarize to Cam: gates passed, bugs reproduced (M1/M2/G8 are expected-fail confirmations), decision items outstanding (G2 ordering, intent scoring restore, relevance tab scope). **Do not fix anything on the branch.**
## Phase 4 — re-verify after Michael's fixes (if/when they land)
New commits → review the delta, redeploy staging, re-run only the affected gates. If the M1 fix lands: it changes a **precomputed indexed field** — staging needs a `prospect_search` reindex before G3 re-runs, or you'll re-measure the old data.
## Phase 5 — draft the sw-release notes (production deploy scope)
Use the `/sw-production-release` skill (project skill, `smallworld/.claude/skills/sw-production-release/`). Its default flow fetches `heroku-production/main` via Heroku git — **unavailable in this sandbox**, so pin the scope explicitly:
- Production HEAD: as of the 2026-07-06 morning packet, prod == main == `c17caa5e9` ("merged-unreleased = 0", release v1472 ~4 days old). **Re-verify at notes-writing time** via Cam: `! heroku releases -a small-world-production -n 3` + slug commit, or the daily packet. Do not assume.
- New main HEAD: the merge hasn't happened (and you will not perform it). Draft with scope `c17caa5e9..<current mts/sw-2544 head>` and label the note **PROVISIONAL — pending merge; regenerate SHAs after main moves**. The scope is exactly PRs #3609 + #3605 (one branch, both PRs) unless main moved — check `git log c17caa5e9..origin/main` first and include any other merged PRs if it did.
- Per the skill: every PR line gets a link + outcome bullets; flag migrations (none in this diff), **reindexing (very much yes — all-accounts `prospect_search` setup + backfill is a deploy prerequisite, and the landing page 500s without it)**, feature flags (none — the landing-page cutover is unconditional; call that out as the headline operational note), and the disabled crons / freshness caveat.
- Output the rendered mrkdwn **in your report only**. Do not post it anywhere.
## Escalation
Ping Cam (ntfy per workspace `.env` defaults) when: all P0 gates are done; any gate fails in a way the board didn't predict; staging auth/session breaks; or anything asks you to write to GitHub/prod. When in doubt: read-only, report, ask.