Skip to Content

← All archived runs

Run: sdm-market-signals

run.md

Run: sdm-market-signals

  • branch: claude/pipeline-sdm-market-signals-rh0xbp
  • pr: #530

00_intake/stub.md

Stub: SDM dashboard — market signals (bid-price analytics)

  • feature-slug: sdm-market-signals
  • epic: sdm-dashboard-data
  • personas: SDM
  • initiative: Build the Bridge / objective: Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow
  • depends-on: sdm-area-foundation
  • sequence: 7 of 8

Problem

The market signals section (marketSignals in apps/web/lib/mock/sdm.ts, rendered by market-signals-section.tsx) reports bid-price economics: avgBidPrice, bidRange, the low/market/high distribution (pctLow, pctMarket, pctHigh), and a per-band performance table (byBand: winRate, revenue, csat, sla, repeatRate). None of this is computed — bids/quotes have prices and win/loss outcomes, but nothing buckets them into price bands or correlates each band with delivery outcomes.

Proposed change

  • Compute the bid-price distribution over the area's quotes/proposals: avgBidPrice, bidRange (min–max), and the share of bids in the low / market / high bands (band thresholds relative to the market average).
  • Build the per-band performance rollup: for each band, winRate (won ÷ submitted), revenue, and the delivery outcomes csat / sla / repeatRate — the last three reused from expert-quality-metrics + sla-stage-targets, not recomputed.
  • Wire the market signals section.

Acceptance criteria (rough)

  • avgBidPrice, bidRange and the low/market/high distribution are computed over the area's quotes/proposals.
  • Each band shows real win rate + revenue + CSAT + SLA + repeat rate, the quality/SLA figures reused from the expert models.
  • The market signals section renders from real data — no lib/mock/sdm.

Out of scope (this feature)

  • A pricing-recommendation engine — this reports the distribution and outcomes; it does not recommend or set prices.
  • The revenue/margin rollup (feature 8) — different lens; this is bid-price economics, not delivered revenue.

Notes for Define

  • Pin the band thresholds (what counts as low / market / high relative to the average) — keep them centralised so this section and any future pricing logic agree.
  • Confirm where bid prices + win/loss live (quote vs proposal) and how "won" is determined.
  • touches: packages/services/src/server (bid-price analytics over quotes/proposals + reused metrics), apps/web/components/dashboard/sdm/market-signals-section.tsx.

01_define/output/spec.md

Spec: SDM dashboard — market signals (bid-price analytics)

  • slug: sdm-market-signals
  • personas: SDM
  • touches: packages/services/src/db/services/sdm-market-signals/, packages/services/src/db/services/index.ts, packages/services/src/db/services/tenant-setting/{index.ts,registry.ts}, apps/web/app/(app)/sdm/dashboard/page.tsx, apps/web/components/dashboard/sdm/market-signals-section.tsx, apps/web/lib/mock/sdm.ts
  • complexity: standard

Problem

The market signals section of the web SDM "Go live readiness" dashboard (apps/web/components/dashboard/sdm/market-signals-section.tsx, rendered by apps/web/app/(app)/sdm/dashboard/page.tsx) reports bid-price economics — avgBidPrice, bidRange, the low/market/high distribution (pctLow, pctMarket, pctHigh), and a per-band performance table (byBand: winRate, revenue, csat, sla, repeatRate) — entirely from apps/web/lib/mock/sdm.ts. None of it is computed: bids carry prices (proposal.price) and win/loss outcomes (an accepted quote), but nothing buckets them into price bands or correlates each band with delivery outcomes. This is sequence 7 of 8 in the sdm-dashboard-data epic — an independent analytics rollup that hangs off the area foundation (sequence 2). It advances Build the Bridge / Q2 2026 Objective 3 — Validate Technical Infrastructure & Payout Flow by turning the last-but-one mock section into real, per-tenant, per-SDM-area data, proving the dashboard's pricing-insight layer resolves end to end.

Proposed change

Add a read-only sdmMarketSignalsService in @sustentus/services/server (new db/services/sdm-market-signals/, following the established index.ts class + instance.ts singleton pattern, exported through db/services/index.ts) that returns one typed market-signals envelope for the signed-in SDM's area, taking the same scope: "mine" | "tenant" the rest of the epic uses. It consumes the area envelope's in-scope leads (sdmAreaService) — it does not re-resolve the area — and the shared expert quality + SLA models. Then wire the section to it.

  • Bid set. A bid is a proposal raised against one of the area's in-scope leads; its bid price is proposal.price. The bid set is every proposal on the area's leads (scope controls which leads are in the area). A bid is submitted by existing; it is won when its lead's quote was accepted (quote.acceptedAt set / quote.status accepted — equivalently the lead advanced to work_in_progress / delivered / completed), and lost otherwise (qualified_out / quote rejected). The lost-vs-still-open distinction does not affect win rate (won ÷ submitted); only the numerator and denominator matter.

  • Bid-price distribution. Over the bid set compute avgBidPrice (mean proposal.price), bidRange (min–max), and the share of bids in the low / market / high bands (pctLow, pctMarket, pctHigh, summing to 100).

  • Band thresholds — a ±margin window around the area average, centralised. A bid is Low when its price is below avg × (1 − margin), High when above avg × (1 + margin), else Market. The single margin (default 0.15) is registered in the tenant-setting registry under a marketSignals category (marketSignals.bandMarginPct) and read via a tenantSettingService.getMarketSignalsThresholds(tenantId) accessor, mirroring the existing coverage.* thresholds + getCoverageThresholds. This keeps the band definition in one place so this section and any future pricing logic agree on what "low/market/high" means.

  • Per-band performance rollup (byBand). For each band (Low, Market, High):

    • winRate = won ÷ submitted bids in that band (percent, 0 when the band is empty).
    • revenue = the realised value of the band's won bids — summed from their accepted quote total (quote.total ?? amount + serviceFee), formatted in the dashboard's currency convention.
    • csat, sla, repeatRate are reused, not recomputed, from the expert epic's models, aggregated across the experts who authored the band's bids (and their leads):
      • csat and repeatRate from getExpertQualityMetrics (the matching service) — the same source sdm-supply-coverage already consumes.
      • sla (per-band SLA adherence) from deriveLeadSlaTimingForLeads (the sla service), over the band's won leads — the same timing layer the coverage + expert-strength features use.
  • Wire the section. apps/web/app/(app)/sdm/dashboard/page.tsx passes the resolved marketSignals envelope (driven by the page's existing "my area" vs "all" scope toggle) to MarketSignalsSection, replacing its lib/mock/sdm source. The component's existing props shape (avgBidPrice, bidRange, pctLow/Market/High, byBand[]) is preserved so the markup is unchanged; only the data source moves. Render the section's defined empty state when the area has no bids (no avg, zeroed distribution, empty byBand) — no crash, no mock fallback. Remove the marketSignals block from apps/web/lib/mock/sdm.ts.

Acceptance criteria

  • sdmMarketSignalsService returns a typed market-signals envelope (avgBidPrice, bidRange, pctLow/pctMarket/pctHigh, byBand[]) from @sustentus/services/server, computed over the area's in-scope bids (proposal.price on the area's leads), tenant-scoped, taking scope: "mine" | "tenant" and reusing sdmAreaService for the in-scope leads.
  • avgBidPrice, bidRange and the low/market/high distribution (summing to 100) are computed from real proposal.price data; bids are bucketed by the ±margin window around the area average.
  • The band margin is read from a single centralised source — marketSignals.bandMarginPct in the tenant-setting registry via getMarketSignalsThresholds — defaulting to 0.15, not hard-coded in the analytics code.
  • Each band's winRate (won ÷ submitted, won = accepted quote) and revenue (sum of the band's won bids' quote total) are computed from real bid/quote data.
  • Each band's csat, sla and repeatRate are reused from the expert epic's models (getExpertQualityMetrics for CSAT/repeat, deriveLeadSlaTimingForLeads for SLA), not recomputed in this feature.
  • The market signals section renders from the service for both scope-toggle states, with a defined empty state when the area has no bids; MarketSignalsSection's props shape is unchanged and the marketSignals block is removed from apps/web/lib/mock/sdm.ts.

Out of scope

  • A pricing-recommendation engine — this reports the bid-price distribution and per-band outcomes; it does not recommend or set prices (epic-level out of scope).
  • The revenue/margin service-performance rollup (feature 8, sdm-service-performance) — a different lens (delivered revenue/margin by region/product/expert-type), shipped separately.
  • Redefining the shared models — CSAT/repeat (matching/expert epic) and SLA timing (sla) are consumed as-is; this feature does not fork or recompute them.
  • An admin UI for editing marketSignals.bandMarginPct — the threshold is registry-defaulted and resolved per tenant; back-office editing is a later concern (the registry seam is established here).
  • Replicating any change back into apps/dashboards — that app stays the dummy source-of-truth.
  • Currency normalisation across mixed-currency bids — bids are aggregated in the dashboard's existing single-currency convention, consistent with the sibling SDM sections.

Open questions

  • none

02_build/output/notes.md

Build notes: sdm-market-signals

  • commits: feat: sdm-market-signals — bid-price analytics service + wire section

What changed

  • packages/services/src/db/services/sdm-market-signals/{index.ts,instance.ts}: new read-only sdmMarketSignalsService.getMarketSignalsForSdm(tenantId, sdmUserId, scope). Resolves the SDM's area (same region/platform membership rule as sdmAreaService/sdmDemandService), fetches the in-scope leads' bids (proposal.price) and their quotes, then computes the bid-price distribution (avgBidPrice, bidRange, pctLow/pctMarket/pctHigh) and the per-band performance rollup (byBand). Bands are a ±margin window around the area average; win rate = won ÷ submitted (won = a proposal whose quote was accepted); revenue = the band's won quotes' totals. CSAT + repeat rate are reused from getExpertQualityMetrics (matching) and SLA adherence from deriveLeadSlaTimingForLeads (sla) — none recomputed here.
  • packages/services/src/db/services/tenant-setting/{registry.ts,index.ts}: added the marketSignals.bandMarginPct registry entry (default 0.15, marketSignals category) and the getMarketSignalsThresholds(tenantId) accessor + MarketSignalsThresholds type — mirroring the coverage.* thresholds, so the band margin lives in one tunable place.
  • packages/services/src/db/services/index.ts: export sdmMarketSignalsService + its types, after its dependency services (tenant-setting, matching, sla) per the established load-order guard.
  • apps/web/app/(app)/sdm/dashboard/page.tsx: resolve marketSignals in the same Promise.all as coverage/risk (degrading to an empty envelope on failure) and pass it to MarketSignalsSection, replacing d.marketSignals. Driven by the page's existing mine vs tenant scope toggle.
  • apps/web/lib/mock/sdm.ts: removed the marketSignals block (now real data).

Acceptance criteria status

  • sdmMarketSignalsService returns the typed envelope from @sustentus/services/server, computed over the area's in-scope bids, tenant-scoped, taking scope: "mine" | "tenant", reusing the area's in-scope leads — same area-membership rule as sdmAreaService.
  • avgBidPrice, bidRange and the low/market/high distribution (forced to sum to 100) are computed from real proposal.price; bids bucketed by the ±margin window around the area average.
  • The band margin comes from marketSignals.bandMarginPct (registry default 0.15) via getMarketSignalsThresholds — not hard-coded in the analytics code.
  • Per-band winRate (won ÷ submitted, won = accepted quote) and revenue (won quotes' totals) are computed from real bid/quote data.
  • Per-band csat, sla, repeatRate reuse getExpertQualityMetrics (CSAT/repeat) and deriveLeadSlaTimingForLeads (SLA) — not recomputed.
  • The section renders from the service for both scope states, with a zeroed/empty envelope when the area has no bids; MarketSignalsSection's props shape is unchanged and the marketSignals mock block is removed.

Verify result

  • mechanical checks (format · lint · typecheck · build) run in CI + the Vercel preview, not here. Release reads them back from the PR's check runs. No check is expected to fail.

Notes for review

  • A bid is submitted by being a priced proposal on an in-scope lead, and won when an accepted quote (acceptedAt set, or status: "accepted") references that proposal (the quote→proposal link is unique, so at most one quote per bid). Empty/zero-bid bands are omitted from byBand rather than shown as all-zero rows; the distribution percentages still convey the split.
  • Per-band SLA adherence is the share of the band's won leads whose SLA timing band is not red (same not-breached definition sdm-supply-coverage uses); 0 when the band has no won leads.

03_release/output/changelog.md

Changelog: sdm-market-signals

Live entry: apps/help/app/changelog/2026-06-24-sdm-market-signals/page.mdx


See real bid-price economics for your area

Your go live readiness dashboard's market signals section now reports real bid-price data for your area, not placeholder numbers:

  • The average bid price, the full bid range, and how bids split across low, market and high price bands — all from live proposals.
  • For each band, the real win rate and revenue, alongside the CSAT, SLA adherence and repeat-work rate of the experts bidding there.
  • Figures update with your "my area" / "all" toggle, so you can compare your own slice against the wider marketplace.

03_release/output/investor-update.md

SDMs can now read real bid-price economics for their area

Who it's for: Service delivery managers What shipped: The go live readiness dashboard's market signals section now computes real bid-price distribution and per-band win rate, revenue, CSAT, SLA and repeat rate from live proposals and quotes. Why it matters: Advances Build the bridge, Q2 objective 3 — validate technical infrastructure & payout flow — by turning the SDM's pricing-insight layer into trustworthy data.

Price bands are tunable from one centralised setting; quality and SLA figures reuse the platform's expert models.

Dig deeper: <merged-PR URL> · <changelog entry URL>

03_release/output/release.md

Release: sdm-market-signals

  • pr: https://github.com/sustentus/sustentus/pull/530 · merged: <pending — Ready-to-merge ticked; awaiting green CI>
  • CI: <pending — Format/Lint/Typecheck on the latest push>
  • technical docs: no technical docs impact — technical/packages/services is high-level (no per-service list); the new sdm-market-signals service + marketSignals.bandMarginPct setting follow the established sdm-* / tenant-setting-registry patterns
  • business docs: no business docs impact — the SDM role page already covers the supply-readiness mandate; the sibling SDM dashboard features set the same precedent (no business/** edit)
  • release notes: investor + changelog — changelog entry apps/help/app/changelog/2026-06-24-sdm-market-signals/page.mdx; investor draft in 03_release/output/investor-update.md (Dig deeper links filled after merge)
  • deploy: <pending>
  • sent: <pending — investor update sends after green production deploy>

Review summary

/code-review medium over main...HEAD (scope: this PR's changes — the sdm-strength, matching, and sdm-coverage diffs in the range came from main and are already reviewed/merged). No correctness findings:

  • Quote totals are nullish-safe (total ?? amount + serviceFee); won = accepted quote referencing the proposal (quote↔proposal is unique, so at most one per bid) — no double count.
  • Distribution percentages are forced to sum to 100 (largest band absorbs rounding drift); empty/zero bands are omitted from byBand rather than shown as misleading all-zero rows.
  • CSAT/repeat/SLA are read from getExpertQualityMetrics + deriveLeadSlaTimingForLeads (reused, not recomputed); each looked up once per unique expert/lead across bands.
  • All reads are tenant-scoped; scope:"mine" with no area returns the empty envelope (no widen-to-tenant).
  • Minor (accepted): emptyBucket/average helpers declared after the class — referenced only at call time, no TDZ; consistent with module-scope helper style. Math.min/max(...prices) spread is bounded by an area's proposal count.

Acceptance check (vs spec)

  • sdmMarketSignalsService returns the typed envelope from @sustentus/services/server, computed over the area's in-scope bids, tenant-scoped, scope: "mine" | "tenant", reusing sdmAreaService.
  • avgBidPrice/bidRange/distribution (forced to sum to 100) from real proposal.price; bids bucketed by the ±margin window around the area average.
  • Band margin from marketSignals.bandMarginPct (registry default 0.15) via getMarketSignalsThresholds — not hard-coded.
  • Per-band winRate (won ÷ submitted, won = accepted quote) + revenue (won quotes' totals) from real bid/quote data.
  • Per-band csat/sla/repeatRate reuse getExpertQualityMetrics + deriveLeadSlaTimingForLeads.
  • Section renders from the service for both scope states, empty state when no bids; component props unchanged; marketSignals mock block removed.