Source identity
| Field | Recorded value |
|---|---|
| Source type | GitHub source |
| Original record | Janady13/missionmatch / frontend/src/lib/capture.ts |
| Date | Not specified in source |
| Repository | Janady13/missionmatch |
| Revision | 720d47fae476b0d38477ae4401697d372b1ea2bc |
| Branch | feature/opportunity-to-outcome |
| Path or archive member | frontend/src/lib/capture.ts |
| Original SHA-256 | df9686c35bc8851971a71b25821deb0b07eaf1a4bedc8089d6b409d23f4a6c59 |
| Git blob SHA | 509a2a0bcd82b4ab550905dc997e8a2b7b99abeb |
| Reading-copy SHA-256 | df9686c35bc8851971a71b25821deb0b07eaf1a4bedc8089d6b409d23f4a6c59 |
Recovered source transcript
Download complete source text ↓125 lines · 3.9 KB
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { request } from './api'
// THE CAPTURE GATE.
//
// A bid or no bid was a status. It recorded the outcome and never who decided, when, or against what, so
// the pursuits a firm walked away from taught it nothing. This is the decision record.
export const CAPTURE_GATES = ['qualify', 'pursue', 'bid'] as const
export type CaptureGate = (typeof CAPTURE_GATES)[number]
export const GATE_LABELS: Record<CaptureGate, string> = {
qualify: 'Qualify',
pursue: 'Pursue',
bid: 'Bid',
}
export interface GateDecision {
id: string
opportunity_id: string
gate: CaptureGate
decision: 'go' | 'no_go'
decided_by?: string
decided_by_name?: string
decided_at: string
rationale: string
// Absent and zero are different facts. A missing advisory must never render as a zero percent chance.
readiness_score?: string
readiness_band?: 'GO' | 'REVIEW' | 'NO_GO'
win_probability?: string
}
export interface ReviewRow {
opportunity_id: string
title: string
agency?: string
status: string
latest: Partial<Record<CaptureGate, GateDecision>>
lapsed_commitments: number
undecided_gates?: CaptureGate[]
}
export interface ReviewBoard {
pursuits: ReviewRow[]
total: number
truncated: boolean
undecided: number
}
export function decisionColor(d?: string): string {
if (d === 'go') return 'var(--color-ok)'
if (d === 'no_go') return 'var(--color-risk)'
return 'var(--color-muted)'
}
export function useCaptureReview(includeClosed: boolean) {
return useQuery({
queryKey: ['slatematch', 'capture-review', includeClosed],
queryFn: () =>
request<ReviewBoard>(`/slatematch/capture/review${includeClosed ? '?include_closed=true' : ''}`),
})
}
export function useGateHistory(opportunityID: string) {
return useQuery({
enabled: !!opportunityID,
queryKey: ['slatematch', 'gate', opportunityID],
queryFn: () =>
request<{ decisions: GateDecision[]; count: number }>(
`/slatematch/opportunities/tracked/${opportunityID}/gate`,
),
})
}
// useRecordGate sends the advisory numbers the decider was LOOKING AT, not a fresh computation. The record
// is of a decision made against a number at a moment, and recomputing on the way in would store a number
// nobody saw.
export function useRecordGate(opportunityID: string) {
const qc = useQueryClient()
return useMutation({
mutationFn: (v: {
gate: CaptureGate
decision: 'go' | 'no_go'
rationale: string
readiness_score?: string
readiness_band?: string
win_probability?: string
}) =>
request<GateDecision>(`/slatematch/opportunities/tracked/${opportunityID}/gate`, {
method: 'POST',
body: JSON.stringify(v),
}),
onSuccess: () => {
qc.invalidateQueries({ queryKey: ['slatematch', 'capture-review'] })
qc.invalidateQueries({ queryKey: ['slatematch', 'gate', opportunityID] })
},
})
}
// THE ADVISORY THE DECISION IS MADE AGAINST.
//
// The gate record stores the readiness band and win probability as they stood at the moment somebody
// decided. Nothing could fetch them for a pursuit, so the form recorded a snapshot of nothing. This is the
// read that makes the snapshot mean something.
export interface CaptureReadiness {
answer?: string
score?: string
band?: 'GO' | 'REVIEW' | 'NO_GO'
contributions?: { feature?: string; label?: string; contribution?: string }[]
citations?: string[]
}
export function useCaptureReadiness(opportunityID: string) {
return useQuery({
enabled: !!opportunityID,
// The advisory moves as coefficients retrain, so it is not cached long. It is read when a decision is
// about to be made, and that is the only moment its value matters.
staleTime: 0,
retry: false,
queryKey: ['slatematch', 'capture-readiness', opportunityID],
queryFn: () => request<CaptureReadiness>(`/ai/capture-readiness/${opportunityID}`),
})
}