Skip to content

05 — Q3.8 + Q4.6.Q8 investigation — Bid response → corpus feedback loop (3 UC mapping)

05 — Q3.8 + Q4.6.Q8 investigation — Bid response → corpus feedback loop (3 UC mapping)

Section titled “05 — Q3.8 + Q4.6.Q8 investigation — Bid response → corpus feedback loop (3 UC mapping)”

Status: Findings (S233 Wave 1B-4). Foreground investigation per feedback doc §1 lines 39-42. Q3.8 reframed from PENDING-ARCHRESOLVED-DIRECTION after this finding.

Couples to: 1B-1 source_documents (binary-source manifest for prior-bid-response uploads), 1B-2 Q&A write-back + bid_question_matches (drafting loop consumer + match cache), 1B-3 historical extraction + coverage (bid_response as 4th coverage gap source), 1B-5 workspace types (bid is a workspace type; outcome JSONB column promotion question).

Audit date: 12/05/2026 Branch: content-items-investigation Method: schema introspection (bid_questions, bid_responses, bid_response_history, workspaces.domain_metadata) + live prod row counts + tracing of lib/bid/bid-state-machine.ts, app/api/bids/[id]/{questions/extract,questions/match,responses/draft-all,outcome,outcome/integrate}/route.ts, lib/validation/schemas.ts (BidMetadataSchema, KBIntegrationBodySchema, BidOutcomeBodySchema).


  • All three UCs are partially or fully designed; the architecture is largely in place. Two of the three feedback-loop directions are wired today (UC-B in-platform bid creation; UC-C outcome capture + integrate-to-KB). The third (UC-A external prior-bid-response import) has no first-class path — closest existing surface is /api/upload landing DOCX as content_items (content_type='q_a_pair') via Pattern A/B detection, which loses bid-context provenance. Q3.8 resolves to RESOLVED-DIRECTION with three follow-up tasks (one per UC), not RESOLVED-COMPLETE.
  • UC-B (client creates response in platform) is the platform’s happy path — 11-state bid state machine (draft → questions_extracted → matching → drafting → in_review → ready_for_export → submitted → won|lost|withdrawn) with full edit-flow §6.5 UC5 promotion semantics. Drafting writes bid_responses; revisions append to bid_response_history. Schema-impact when 1B-2 lands: bid_responses.source_content_ids UUID[] (inline citation array) ports to the new citations table (1B-2 Rec 3); bid_questions.matched_content_ids UUID[] ports to bid_question_matches (1B-2 Rec 4). Both deprecate-with-history-preserved per 1B-2.
  • UC-C (outcome → corpus) exists as POST /api/bids/[id]/outcome/integrate — admin/editor-driven, won-bids-only, dedup-checked (checkExactDuplicate from lib/dedup), per-response opt-in ({question_id, action: 'new_entry' | 'update_existing' | 'skip', target_content_id, content_type}). Three gaps: (i) lost/withdrawn bids carry zero learning today (no signal capture); (ii) writes to content_items not q_a_pairs (because S16 q_a_pairs table doesn’t exist yet — migration follows S16); (iii) BidMetadataSchema parks outcome+evidence in JSONB (16 fields including outcome, outcome_notes, outcome_recorded_at/by, tender_document_ids, deadline) — Liam’s S232 “lift JSONB to typed columns” framing applies. Schema-impact: promote 6-9 outcome columns from JSONB to typed columns on workspaces OR introduce a bid_workspaces satellite (per 1B-5 Shape B pattern).
  • UC-A (external prior bid response import) has no first-class path. Today: drop DOCX into /api/upload, Pattern A/B detects Q&A tables → content_items with content_type='q_a_pair'. Loses bid-context (which bid was this from? won/lost? what client?), loses confidence signal (was this response approved? what was the evaluator’s feedback?). New work needed (small): new content_items.ingest_source value 'prior_bid_response' + prior_bid_metadata JSONB (client name + buyer + outcome-if-known + import date) OR — preferred — extension of the new q_a_extractions table per S16 §6.3 with prior_bid_context typed columns. No new top-level table.
  • No new top-level table needed for Q3.8. All three UCs land on existing primitives (workspaces+bid_questions+bid_responses+bid_response_history+content_items / q_a_pairs / q_a_extractions / citations / bid_question_matches) plus targeted column additions. The 1B-2 changes (citations + bid_question_matches) ARE the schema upgrades that the bid feedback loop needs.

2. Live state — feedback-loop surfaces (12/05/2026)

Section titled “2. Live state — feedback-loop surfaces (12/05/2026)”

2.1 Schema map (workspaces + bid descendants)

Section titled “2.1 Schema map (workspaces + bid descendants)”
workspaces (4 prod rows, ALL type='intelligence', 0 bid rows)
├── domain_metadata JSONB (BidMetadataSchema — 16 fields when type='bid')
│ ├── buyer, status, deadline, reference_number, estimated_value
│ ├── tender_source, tender_document_ids[], submission_date
│ ├── outcome (won|lost|withdrawn|null), outcome_notes,
│ │ outcome_recorded_at, outcome_recorded_by
│ └── notes
└── bid_questions (0 prod rows; FK project_id → workspaces — renames to workspace_id per 1B-5)
├── question_text, section_name, section_sequence, question_sequence
├── word_limit, evaluation_weight, confidence_posture
├── matched_content_ids UUID[] (inline cache — ports to bid_question_matches per 1B-2)
├── template_requirement_id (FK → template_requirements)
├── status, has_variants, assigned_to
└── bid_responses (0 prod rows; FK question_id → bid_questions)
├── response_text, response_text_advanced
├── source_content_ids UUID[] (inline citations — ports to citations per 1B-2 Rec 3)
├── review_status, drafted_by, last_edited_by, approved_by
├── overall_score numeric
├── version
└── bid_response_history (0 prod rows; FK response_id → bid_responses)
├── version, response_text, response_text_advanced
├── source_content_ids UUID[]
├── review_status, edited_by, change_reason
RouteMethodPurposeTouches
/api/bids/[id]/questions/extractPOSTTender PDF/DOCX → bid_questions rowslib/ai/extract-questions.ts (Anthropic tool-use); extractPDFQuestions / extractDOCXQuestions / extractTenderMetadata
/api/bids/[id]/questions/matchPOSTMatch each bid_question against KB → matched_content_ids[]Inline array cache (1B-2 ports to bid_question_matches)
/api/bids/[id]/responses/draftPOSTSingle-question 3-pass draft (Sonnet analysis / Opus drafting / Haiku quality)bid_responses (new version row)
/api/bids/[id]/responses/draft-allPOSTBatch-draft all questionsMultiple bid_responses writes
/api/bids/[id]/responses/draft-streamPOSTStreamed draft (SSE)Same
/api/bids/[id]/responses/[rId]PATCHUser-edits a draftbid_responses updated + bid_response_history versioned
/api/bids/[id]/responses/[rId]/regeneratePOSTRe-run drafting on a single responseSame
/api/bids/[id]/responses/[rId]/restorePOSTRevert to a prior bid_response_history rowSame
/api/bids/[id]/responses/estimatePOSTCost estimation pre-draft-alllib/coverage/cost-estimation.ts
/api/bids/[id]/export/docxGETExport submission-ready DOCXlib/bid/bid-export-docx.ts
/api/bids/[id]/export/xlsxGETExport XLSX summarylib/bid/bid-export-xlsx.ts
/api/bids/[id]/outcomePOSTRecord won/lost/withdrawn → workspaces.status + JSONBBidOutcomeBodySchema; state-machine transition guard
/api/bids/[id]/outcome/integratePOSTPromote won-bid responses to KB. Per-response selection (action: 'new_entry' / 'update_existing' / 'skip'); 5 target content_types (q_a_pair / case_study / policy / methodology / capability).KBIntegrationBodySchema; checkExactDuplicate; writes content_items
/api/bids/[id]/tenderGET/POST/DELETETender doc upload + replaceworkspaces.domain_metadata.tender_document_ids[]
/api/bids/[id]/templates/*variousTemplate selection + auto-map + fillUses template_requirements from 1B-3

Per lib/bid/bid-state-machine.ts:48-58:

draft ─┬─ questions_extracted ── matching ── drafting ── in_review ── ready_for_export ── submitted ─┬─ won
│ ├─ lost
└─ withdrawn (from any state) └─ withdrawn
  • won / lost / withdrawn are terminal. No transitions out.
  • outcome/integrate route gates on status === 'won'. Lost / withdrawn bids cannot integrate today.

3. UC-A — Client supplies prior bid responses in various formats

Section titled “3. UC-A — Client supplies prior bid responses in various formats”

No first-class path. Three indirect options exist:

PathWhat happensWhere provenance lands
/api/upload route (DOCX)Mammoth → HTML → lib/quality/qa-detection.ts Pattern A/B/C → content_items rows with content_type='q_a_pair'content_items.ingest_source='manual'; bid-context entirely lost
Python pipeline (scripts/kb_pipeline/)Same fork — DOCX → markdown → classify → content_itemsSame
Manual Q&A pair authoringUser types pairs into the Q&A editorcontent_items with ingest_source='manual'; no bid-context

Implications:

  • Prior responses become indistinguishable from authored Q&A pairs once landed.
  • Outcome / evaluator-feedback information from the source bid is dropped.
  • Confidence signal is identical to fresh manual authoring (no “validated-by-winning-bid” boost).
  • Cross-client provenance (this came from Client X’s bid for Buyer Y, response approved on date Z) is unrecoverable from the row.

Two parallel work-streams, both small.

(i) New ingest source classificationcontent_items.ingest_source (or post-S16, q_a_extractions.extractor_kind) gets a new value 'prior_bid_response'. The upload route accepts a new optional body field {prior_bid: { client_name?, buyer?, outcome?, source_bid_date?, evaluator_notes? }} that lands in the typed columns (post-S16) or in metadata JSONB (pre-S16). One new ingest path, zero new tables.

(ii) Confidence-signal boost from prior outcomeq_a_pairs.confidence (per S16 §6.2) reads the prior-bid outcome at promotion time: won → confidence bonus; lost → confidence penalty; withdrawn → neutral. Surfaces in coverage’s tier scoring (1B-3 §4.3 — bid_response as 4th gap source).

Both changes are additive — no migration of existing rows required.

ChangeWhereNotes
New 'prior_bid_response' value in ingest-source / extractor-kind enumcontent_items.ingest_source (pre-S16) OR q_a_extractions.extractor_kind (post-S16)Single enum extension
New typed columns prior_bid_client, prior_bid_buyer, prior_bid_outcome, prior_bid_dateq_a_extractions (post-S16)4 columns; only populated when extractor_kind = ‘prior_bid_response’
Upload-route field prior_bid_metadata (Zod-validated)app/api/upload/route.ts + lib/validation/schemas.tsNew optional body field
Confidence-signal bonus rulePromotion path in UC5 (per edit-flow §6.5)App-side calculation; no schema

4. UC-B — Client creates response in-platform with existing Q&A + new pairs

Section titled “4. UC-B — Client creates response in-platform with existing Q&A + new pairs”

4.1 Today’s state (largely WORKING — per edit-flow §6.5)

Section titled “4.1 Today’s state (largely WORKING — per edit-flow §6.5)”

The platform’s happy path. State sequence:

1. Workspace (type='bid') created with buyer + deadline metadata.
2. Tender doc upload → /api/bids/[id]/tender — stored, tender_document_ids JSONB updated.
3. /api/bids/[id]/questions/extract — Anthropic tool-use extraction from PDF/DOCX → bid_questions rows.
State → questions_extracted.
4. /api/bids/[id]/questions/match — match each question against KB → matched_content_ids[].
State → matching.
5. /api/bids/[id]/responses/draft-all (or per-question drafts).
3-pass Sonnet+Opus+Haiku per cost-estimation.ts. Writes bid_responses + creates bid_response_history v1.
State → drafting.
6. User edits draft in TipTap editor → PATCH /responses/[rId] → bid_response_history v2..N.
State → in_review when ready.
7. Export DOCX/XLSX → /export/docx /export/xlsx.
State → ready_for_export → submitted.
8. POST /outcome → workspaces.status + domain_metadata.outcome.
State → won|lost|withdrawn.

Promotion of novel responses to Q&A pairs (UC5 per edit-flow §6.5): RATIFIED — user-initiated, never auto, draft → in_review (curator queue) → published. Not yet implemented as a discrete UI surface — the existing /outcome/integrate route is the closest primitive but is won-bid-only. UC5 broadens it to “promote at any point during drafting/review, not just at outcome”.

4.2 Gaps + schema-impact (1B-2 dependencies)

Section titled “4.2 Gaps + schema-impact (1B-2 dependencies)”
TodayAfter 1B-2 lands
bid_questions.matched_content_ids UUID[] (inline cache)bid_question_matches table (per 1B-2 Rec 4) — adds per-match score, match_method, recompute_run_id, rank_within_run. Drop inline column post-migration.
bid_responses.source_content_ids UUID[] (inline citations)Rows in citations table (per 1B-2 Rec 3 — renamed from content_citations). Add bid_response_id FK + chunk-level offsets. Drop inline array post-migration.
bid_response_history.source_content_ids UUID[] (inline citations, per-version)Tricky: citation history per response version. Recommendation: keep inline on bid_response_history (it’s a versioned snapshot) — only the live bid_responses row’s citations port to the new table. Re-evaluate at WP3 05-qa-flow.md.
No promotion-from-draft UIUC5 surface — promote-to-Q&A button on each bid_response row → opens promotion dialog (scope_tag picker + intent vocabulary + duplicate-check). Per edit-flow §6.5.1 4.6.Q1-Q10.
KB Integration Review (KBIntegrationReview) currently won-bid-onlyPer 4.6.Q8: “Same surface as broader bid integration review” — extend to support pre-outcome promotion.
ChangeWhereSource
bid_question_matches new table1B-2 Rec 4Already specced
citations rename + extend (q_a_pair_id, chunk-level)1B-2 Rec 3Already specced
Promotion-from-draft UINew components/bid/promote-response-to-qa-dialog.tsxNew work
bid_responses.source_content_idscitations.* migration1B-2 §10 sequenceSequenced after citations rename
bid_questions.matched_content_idsbid_question_matches.* migration1B-2 §10 sequenceSequenced after new table land

Net: NO new tables for UC-B beyond what 1B-2 already proposed.


5. UC-C — Bid outcome feeds back as Q&A data informing future bids

Section titled “5. UC-C — Bid outcome feeds back as Q&A data informing future bids”

5.1 Today’s state — /outcome/integrate route

Section titled “5.1 Today’s state — /outcome/integrate route”

Active for won bids only (line 65 of route: if (bidStatus !== 'won') { return 400 }). Body schema (KBIntegrationBodySchema):

{
integrations: Array<{
question_id: UUID,
action: 'new_entry' | 'update_existing' | 'skip',
target_content_id?: UUID, // required if action='update_existing'
title?: string,
content_type?: 'q_a_pair' | 'case_study' | 'policy' | 'methodology' | 'capability'
}>,
skip_dedup?: boolean // admin-only; silent-ignored for non-admin
}

Flow:

  1. Admin/editor reviews won bid responses one-by-one.
  2. Per response: skip / update_existing (with content_id) / new_entry.
  3. checkExactDuplicate runs pre-insert; admin can override via skip_dedup=true.
  4. new_entry writes a content_items row with the chosen content_type + embedding generated via lib/ai/embed.ts.
  5. Plain text extracted from response HTML via htmlToPlainText.

(i) Lost / withdrawn bids carry zero learning. State machine blocks transition out of lost/withdrawn; /outcome/integrate rejects non-won. Yet lost bids contain signal: which questions did we lack content for? Which responses scored low? Was there evaluator feedback? None captured today.

(ii) Writes to content_items, not q_a_pairs. Post-S16 q_a_pairs lands; integrate must update to target q_a_pairs (when content_type='q_a_pair') or content_items (other 4 types: case_study / policy / methodology / capability). Polymorphic target — route picks table based on content_type.

(iii) Outcome data in JSONB. workspaces.domain_metadata carries 16 fields when type='bid' (BidMetadataSchema): buyer / status / deadline / reference_number / estimated_value / tender_source / tender_document_ids[] / submission_date / outcome / outcome_notes / notes / outcome_recorded_at / outcome_recorded_by. Per Liam S232 “lift JSONB to typed columns” framing: at least 6 of these are first-class queryable values that should be typed columns (buyer, deadline, submission_date, outcome, outcome_recorded_at, outcome_recorded_by).

For (i) — lost / withdrawn learning:

AddWhereWhy
bid_response.evaluator_score (numeric 0-100, nullable)New column on bid_responsesCapture evaluator’s per-question score when available (often from debrief feedback)
bid_response.evaluator_feedback (text, nullable)New column on bid_responsesFree-text feedback per response from debrief
bid_response_history.outcome_signal (text, nullable)New column on bid_response_history”approved-pre-submission” / “approved-with-edits” / “rejected-by-internal-review” / “submitted-final”
New outcome_signal_type enum value 'lost_bid_learning' (post-S16)q_a_extractions.extractor_kindWhen promoting a lost bid response to corpus, mark its provenance
Route extension: /outcome/integrate accepts lost AND won bids (with different default actions)app/api/bids/[id]/outcome/integrate/route.tsLost-bid promotion is a smaller use case (mostly “this question had no good answer — author one for next time”) but valuable signal

For (ii) — q_a_pairs target post-S16:

ChangeWhere
/outcome/integrate route: when content_type='q_a_pair', write to q_a_pairs not content_itemsapp/api/bids/[id]/outcome/integrate/route.ts (post-S16 migration)
KBIntegrationBodySchema: split content_type into target_table ∈ {q_a_pairs, content_items} + target_subtype ∈ {case_study, policy, …}lib/validation/schemas.ts
Citations write: through new citations table (1B-2) with q_a_pair_id or content_item_id depending on targetSame

For (iii) — JSONB → typed columns:

Two options:

OptionShapeProsCons
A — promote 6-9 outcome columns to workspacesDirect typed columns on workspaces: outcome, deadline, submission_date, outcome_recorded_at, outcome_recorded_by, buyer. Leave intelligence-specific fields in domain_metadata.Simpler; queries against workspaces.outcome are direct.workspaces becomes type-aware (columns only relevant when type=‘bid’).
B — bid_workspaces satellite table (per 1B-5 Shape B pattern)New table bid_workspaces (workspace_id PK FK → workspaces, buyer, deadline, outcome, ...)Clean separation; matches 1B-5 satellite pattern; mirrors sales_proposals_workspaces future shape.Joins required for every bid query.

Recommendation: Option B for symmetry with 1B-5 Shape B per-type-satellite pattern. Sales proposals will need a proposal_workspaces table; competitor research a competitor_workspaces; etc. Bid is the precedent. Land in WP4 04-workspace-types.md.

ChangeNewModifiedSource
bid_workspaces satellitenew table1B-5 Shape B pattern; this finding
bid_responses.evaluator_score (numeric)new columnThis finding
bid_responses.evaluator_feedback (text)new columnThis finding
bid_response_history.outcome_signal (text)new columnThis finding
lost_bid_learning extractor_kindenum extensionThis finding
/outcome/integrate route: accept lost + won; polymorphic targetroute changeThis finding
q_a_pairs integration target (vs content_items)route changeCoupled to S16 §10 sequence

6. Recommendations (binding for downstream work)

Section titled “6. Recommendations (binding for downstream work)”
  1. Q3.8 — Update decision-graph status from PENDING-ARCHRESOLVED-DIRECTION. Three UC-specific follow-ups carry forward as separate items (see Recs 2-4 below). Wire WP2 status sync (Wave 2 this session).
  2. UC-A — New 'prior_bid_response' ingest path. Add as q_a_extractions.extractor_kind value (post-S16) + 4 typed prior-bid context columns. Touchpoint: app/api/upload/route.ts + lib/validation/schemas.ts. Small; lands alongside S16 migration sequence.
  3. UC-B — Promotion-from-draft UI surface. Implement UC5 (per edit-flow §6.5.1) as a promote-response-to-qa-dialog.tsx component on bid_responses rows. Uses 1B-2’s new citations + bid_question_matches tables. Gates on S16 q_a_pairs + 1B-2 schema land.
  4. UC-C — Lost-bid learning + JSONB-to-typed promotion. (a) Extend /outcome/integrate route to accept lost AND won bids with different default actions. (b) Add evaluator_score + evaluator_feedback columns to bid_responses. (c) Add outcome_signal column to bid_response_history. (d) Promote 6-9 bid-specific JSONB fields to a new bid_workspaces satellite table (per 1B-5 Shape B pattern). All sequenced after S16 q_a_pairs lands.
  5. Coverage 4th gap source bid_response (per 1B-3 §4.3): wire into lib/coverage/gap-scoring.ts post-1B-2 bid_question_matches land. Connects UC-B and UC-C feedback loops back into platform-wide coverage signal.
  6. No new top-level table for Q3.8 beyond what 1B-2 + this finding already propose. The bid-response feedback loop lands cleanly on existing primitives + 1B-1/1B-2/1B-5 schema upgrades.

Downstream itemCoupling
1B-1 source_documents Option α/βPrior-bid-response uploads (UC-A) ingest as DOCX/PDF binary sources. Under Option α: land in source_documents with mime_type + content_hash. Under Option β: cocoindex source-key. Same path as Phew Q&A library DOCXs today.
1B-2 citations + bid_question_matchesUC-B drafting loop consumes bid_question_matches; UC-B citations write through new citations table. Both UC-B schema deps are 1B-2 deliverables. No additional 1B-2 work needed.
1B-3 historical extraction + coverageCoverage’s 4th bid_response gap source (1B-3 §4.3 Rec 4) consumes bid_question_matches rows. Feedback loop output (1B-3) is bid loop input (this finding).
1B-5 workspace types + Q3.11 templatesUC-C JSONB-to-typed promotion creates bid_workspaces — the first satellite-table for the bid workspace type per 1B-5 Shape B. Establishes precedent for sales_proposals_workspaces, competitor_workspaces, training_workspaces.
S16 q_a_pairs + q_a_extractionsUC-A q_a_extractions.extractor_kind='prior_bid_response'; UC-C polymorphic integration target (q_a_pairs vs content_items). Both gate on S16 §10 migration land.
Edit-flow §6.5 UC5 promotionRATIFIED. UC-B Rec 3 implements the UI surface.
Edit-flow §6.6 UC6 revisionWhen integrated Q&A pairs (from UC-C) need revision (e.g. regulatory update), UC6 user-direct flow applies.
WP3 architecture/08-new-features.mdHosts: bid-feedback loop diagram (3 UCs); lost-bid learning capture; bid_workspaces satellite; outcome JSONB promotion.
WP3 architecture/04-workspace-types.mdHosts: bid_workspaces as Shape B satellite precedent.
WP3 architecture/05-qa-flow.mdHosts: promotion-from-draft (UC5) + promotion-from-outcome (UC-C) UI surfaces.

QFor
OQ-Q38-ALost-bid promotion default action — skip (current won-bid default for null actions) or new_entry with confidence penalty? Recommendation: skip (lost bids surface fewer high-value novel answers; opt-in only).
OQ-Q38-BEvaluator feedback capture UX — free-text on /outcome form, or per-response inline in the review surface, or both? Recommendation: both (top-level outcome notes + per-response evaluator_feedback).
OQ-Q38-CCitation provenance from bid_response_history — port versioned citations into citations (large rows) or keep inline source_content_ids[] on history (skewed read pattern)? Recommendation: keep inline on history; only current bid_responses row’s citations port to the new table.
OQ-Q38-DConfidence-signal weighting for prior-bid-outcome — won → +0.1, lost → -0.05, withdrawn → 0? Floor at 0.0; cap at 1.0. Recommendation: calibrate post-launch; ship with conservative defaults.
OQ-Q38-Ebid_workspaces satellite column set — minimum 6 (buyer / deadline / submission_date / outcome / outcome_recorded_at / outcome_recorded_by) or extended to all 16 BidMetadataSchema fields? Recommendation: 6-column minimum; remaining 10 (reference_number, estimated_value, tender_source, tender_document_ids[], outcome_notes, notes) stay in JSONB. Promote only the queryable / RLS-relevant ones.
OQ-Q38-FWhen UC-C integrates a response to q_a_pairs (post-S16), does the citation chain back to the original bid_response (provenance trace)? Recommendation: yes — q_a_pairs.provenance_source carries {kind: 'bid_response', bid_response_id, bid_workspace_id, integrated_at}.
OQ-Q38-GState-machine extension — should won / lost exit to a new learning_captured state after /outcome/integrate completes? Recommendation: no; integration is asynchronous + per-response. Don’t lock state on it. Track via bid_response.integrated_at column instead.

End of finding 05.