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.
Two of the three keys are trivial matter-owned columns; the third is not.
client_name - business.clients.name via
matters.client_id (NOT NULL, ondelete=RESTRICT - a matter cannot
exist without a client, so no NULL sentinel).representing_side - matters.representing_side verbatim
(principal|opposing|advisory). This is the load-bearing field
code and prompts branch on; advisory alone carries "no single adverse side".who_we_represent - the procedural label counsel picked for our side
("Claimant" / custom). OPTIONAL; byte-verbatim when present, else
null. Never a fallback string, never derived, never fabricated -
an invented label is a lie; a null that validates is honest (RFD D1, founder 2026-07-21).
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.
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
who_we_represent is null - passed through unchanged,
never coerced to a string.
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.
| Case | Assertion |
|---|---|
| Query shape | The matter read JOINs business.clients, selects
c.name AS client_name + m.representing_side, and reads
who_we_represent off mp.side = 'ours'. |
| Verbatim carry | A non-ASCII client + a custom label ("Respondent No. 2") ride
byte-verbatim onto FramerInput. |
| Null label | who_we_represent is None (not a fallback
string) while client_name / representing_side stay populated. |
| Advisory | representing_side == "advisory" carries verbatim; label is
None. |
| Full-payload equality | The existing EB-02 ordering test now asserts the three keys
are present in the invoke payload's input. |
| Q | A |
|---|---|
| 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. |
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
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.