Story 2778: framer_trigger sends client_name + representing_side + participant label

Deploy step 1 of RFD-AGT-011 D3. Today Vega receives only title + matter_type and must guess which party is "us" from documents - a wrong guess is the Critic's wrong_identity finding class. The DB already knows the answer. This Story adds the counsel-declared matter context to FramerInput in src/workers/framer_trigger/processor.py _build_framer_input: client_name, representing_side, and who_we_represent. It is additive only - the currently-deployed Vega brain ignores unknown keys, so this ships first and safely (RFD D3); the pramaan-agents sibling (#2777) makes the two required fields load-bearing and deploys second. Reversing the order would break every build. No migration (both columns already NOT NULL), no board change, no deploy from here.

Why this shape (the D1 resolution rule)

Two of the three keys are trivial matter-owned columns; the third is not.

There is no matter-level participant-label column. The label counsel typed at creation lives only in business.proceeding_participant_labels, linked to the ours-side party through proceeding_participant_roles → matter_participants.side='ours' on the Main proceeding. Founder decision A (2026-07-22): reading counsel's declared side off matter_participants is a structural read of a fact counsel explicitly chose in the Add-Matter form - it is the declared-context the RFD wants Vega to stop guessing, not derivation. The verbatim-or-null rule governs the label name (the free text counsel typed), which stays byte-exact or null.

1. The query

One read, already firm-scoped (_set_firm_scope, EB-06). The two columns are a JOIN business.clients plus a verbatim column select; the label is a correlated scalar subquery so the main matter row is never multiplied:

SELECT m.matter_number, m.title, mt.name AS matter_type,
       c.name AS client_name, m.representing_side,
       (SELECT ppl.name
          FROM business.matter_participants mp
          JOIN business.proceeding_participant_roles ppr ON ... AND ppr.participant_id = mp.id
          JOIN business.proceedings p ON ... AND p.is_main
          JOIN business.proceeding_participant_labels ppl ON ... AND ppl.id = ppr.participant_label_id
         WHERE mp.firm_id = m.firm_id AND mp.matter_id = m.id AND mp.side = 'ours'
         ORDER BY mp.created_at, mp.id LIMIT 1) AS who_we_represent
FROM business.matters m
JOIN business.matter_types mt ON mt.firm_id = m.firm_id AND mt.id = m.matter_type_id
JOIN business.clients c ON c.firm_id = m.firm_id AND c.id = m.client_id
WHERE m.firm_id = :firm_id AND m.id = :matter_id
The subquery is firm-and-matter sealed on every join leg and pins the Main proceeding. When counsel picked no label for the ours side (advisory and blank matters, typically), the subquery returns no row and who_we_represent is null - passed through unchanged, never coerced to a string.

2. The payload

Three keys added to the framer_input dict, beside the existing title, matter_type, documents, previous_frame, locked_fields (and the CE-21 context_bundle when a cache hits). The run-input audit INSERT (matter_frame_run_inputs) is untouched - it stores per-document rows, not the input payload; no DDL.

3. Tests

CaseAssertion
Query shapeThe matter read JOINs business.clients, selects c.name AS client_name + m.representing_side, and reads who_we_represent off mp.side = 'ours'.
Verbatim carryA non-ASCII client + a custom label ("Respondent No. 2") ride byte-verbatim onto FramerInput.
Null labelwho_we_represent is None (not a fallback string) while client_name / representing_side stay populated.
Advisoryrepresenting_side == "advisory" carries verbatim; label is None.
Full-payload equalityThe existing EB-02 ordering test now asserts the three keys are present in the invoke payload's input.

4. The six review-gate answers

QA
New cloud capability?None. A wider read of existing tables over the existing firm-scoped worker DB path.
Who grants it / where?N/A - business.clients, proceeding_participant_labels, proceeding_participant_roles, matter_participants are already readable under the consumer's firm scope; no new grant.
What check goes red if absent?test_framer_trigger_processor.py - the query-shape and verbatim/null/advisory cases fail if the keys or the join are dropped.
Observed red?The new cases fail against the pre-change assembler (keys absent / KeyError); the full-payload equality assertion failed before the payload carried the keys.
What legacy is deleted?Nothing - this is a strictly additive contract growth (RFD D3); nothing is retired.
Deploy smoke exercises the change?The unit suite exercises the exact assembler path; the RFD-mandated staged live Vega build (the orchestrator's post-merge step) confirms the three keys land in the run's FramerInput.

5. Verify

ruff check src/workers/framer_trigger/ tests/unit/test_framer_trigger_processor.py   # clean
pytest tests/unit/test_framer_trigger_processor.py tests/unit/test_framer_trigger_handler.py  # 25 passed
Not in this Story (RFD D3): the pramaan-agents FramerInput contract + the D2 evidence hierarchy in all four role prompts (#2777, deploys second), and the four eval cases (#2780). This side must land and be verified live before #2777's artifact deploys.