M&A deal workbench → structured JSON deliverable
These tasks all have the same shape: a prompt names one deal, points at a read-only
M&A workbench, and requires only JSON conforming to an answer_template.json.
Output is graded by field-level exact comparison, so a defensible-but-invented value
scores the same as a wrong one. Everything you emit must be traceable to a record you
actually read, or to arithmetic on those records.
The one rule that matters most
Derive, don't infer. The workbench is the whole world. Do not add issues, IDs,
amounts, or classifications from general M&A practice. If a record does not say it,
it is not in the answer.
Its corollary, filter, don't pad, is the single highest-value habit: a list field
named "required", "blocking", or "closing condition" holds only the records whose
own flag says they qualify. Listing every record and tagging each with a type is
measurably worse than listing only the qualifying ones — non-qualifying records belong
in the template's non-blocking / excluded / notice-only field, if it has one, and
nowhere otherwise.
Workflow
1. Read the answer template before touching the API
The template is the contract and the strongest hint about the expected answer. Extract:
- Required top-level keys and their exact spelling. Emit exactly these.
allowed_enums — every enum-typed value must be a verbatim member. Watch case:
workbench records store "High"/"yes", templates usually want "HIGH"/true.
- Pinned ID lists (
possible_issue_ids, stable_issue_ids, stable_redline_ids,
an issue_id "one of:" list). When the template pins IDs, that list is the
expected membership — produce every ID, no more and no fewer. When it does not,
include an entry only where a record evidences it.
- Enums that only make sense nested (
fee_model, tax_allocation_method, forum,
condition_type) — these are telling you the vocabulary for the free-form
*_normalized / must_have_terms objects. Use those exact strings there.
- Units — integer dollars, percent-point decimal places, integer months. The
prompt sometimes overrides the template; the prompt wins.
- Named final positions. A
final_position / recommended_action enum whose values
map 1:1 onto the issue IDs is a decoded answer key — pair them by name and let the
wording drive the row. These names are often self-describing, encoding the target
number, the fallback, and the condition attached to it; read them as instructions.
2. Pull the deal bundle
scripts/fetch_deal.py <base_url> <deal_id> <out_dir> fetches the deal record, follows
its links map, and pulls the governing playbook or policy named on the deal. It
discovers routes from the deal record, so it does not depend on a hardcoded route list.
The workbench holds dozens of decoy deals whose project names differ from the target's by
a syllable or a suffix, with cloned supporting rows carrying similar-looking amounts.
Filter on the exact deal_id every time, including in SQL. Never let a similarly
named project supply a number.
3. Establish the governing standard
The deal record names exactly one:
| Field on deal |
Standard |
Key columns |
playbook_id |
playbook_rules |
preferred_position, fallback_position, limit_value, limit_unit, basis, risk_default |
policy_id |
policy_thresholds |
policy_standard, threshold_value, restricted_flag, approval_required, basis |
Parse the numeric preferred/fallback out of the prose positions — limit_value alone is
ambiguous (it is sometimes the preferred bound, sometimes the fallback).
4. Select the rows
Use only draft_terms with staleness_flag == "current". Then, per template shape:
- Issue register / deviation matrix — one row per template-pinned issue ID. Map each
to the current draft term(s) sharing its
category; a pinned ID with no matching
current term is missing_required_term with empty source_term_ids.
- Committee escalation — include a term only if all hold: current, its category has
a governing threshold, the draft breaches it, and the threshold is in scope for the
requested body (
restricted_flag == "yes" / approval_required matches). Stale,
in-policy, and lower-approval rows are deliberate distractors — put them in the
template's excluded_* fields, not in the escalation list.
reference/classification_rules.md has the status / risk / action decision tables and the
record-flag filters (consents, material contracts, employees).
5. Compute the numbers
- Base = the deal's
headline_value unless the term or rule basis explicitly names
another basis. headline_value also equals upfront_cash + stock_value in these deals;
a draft term that states its own dollar figure confirms the base — check it.
amount = round(base * percent / 100), emitted as an integer.
- Deltas run in the direction that harms your client (see polarity table in
reference/classification_rules.md). Report the gap to the fallback when the template
asks for one gap, and both fallback and preferred gaps when it asks for two.
- Exposure ranges come from
risk_estimates rows, matched by category. Count each
category once in an aggregate even when several issues cite it, and list which
categories you included/excluded if the template has fields for that.
- Every total must equal the sum of the components you actually emitted.
6. Validate, then emit
Run scripts/validate_answer.py <candidate.json> <answer_template.json>. It checks
required keys, enum membership, integer-dollar fields, percent rounding, and pinned-ID
coverage. Emit raw JSON only — no prose, no code fences, no trailing commentary.
Pitfalls
- Padding a required/blocking list with non-qualifying records. The costliest
recurring error.
required_for_closing == "no" and consent_required == "notice only"
are never blockers.
- Inventing IDs. Prefer stable workbench IDs (
CNS_*, MAT_*, EMP_*, TERM_*,
RSK_*, FND_*). Only synthesize one where the template explicitly permits it
(e.g. "synthetic regulatory id").
- Free-styling risk ratings. Inherit
risk_default from the governing rule, or the
record's own risk_rating, before making a judgment call. Normalize to the enum's case.
- Ignoring
client_side. It flips draft_exceeds_playbook vs draft_below_playbook
and flips which party a term protects.
- Double-counting an exposure category across several issues in an aggregate total.
- Trusting
limit_value instead of the preferred/fallback prose.
- Wrong percent precision. Two decimals, one decimal, and four-decimal holder
percentages all appear; re-read the prompt for each task.
- Adding keys the template does not list is harmless; omitting listed keys is not.
1---2name: ma-deal-workbench-33description: Produce structured JSON deliverables (issue registers, deviation matrices, committee escalation memos, closing/transition packages) from an M&A deal workbench API against a supplied answer template. Use whenever a task gives a deal_id, a workbench base URL, and an answer_template.json to conform to.4---56# M&A deal workbench → structured JSON deliverable78These tasks all have the same shape: a prompt names one deal, points at a read-only9M&A workbench, and requires **only JSON** conforming to an `answer_template.json`.10Output is graded by field-level exact comparison, so a defensible-but-invented value11scores the same as a wrong one. Everything you emit must be traceable to a record you12actually read, or to arithmetic on those records.1314## The one rule that matters most1516**Derive, don't infer.** The workbench is the whole world. Do not add issues, IDs,17amounts, or classifications from general M&A practice. If a record does not say it,18it is not in the answer.1920Its corollary, **filter, don't pad**, is the single highest-value habit: a list field21named "required", "blocking", or "closing condition" holds *only* the records whose22own flag says they qualify. Listing every record and tagging each with a type is23measurably worse than listing only the qualifying ones — non-qualifying records belong24in the template's non-blocking / excluded / notice-only field, if it has one, and25nowhere otherwise.2627## Workflow2829### 1. Read the answer template before touching the API3031The template is the contract and the strongest hint about the expected answer. Extract:3233- **Required top-level keys** and their exact spelling. Emit exactly these.34- **`allowed_enums`** — every enum-typed value must be a verbatim member. Watch case:35 workbench records store `"High"`/`"yes"`, templates usually want `"HIGH"`/`true`.36- **Pinned ID lists** (`possible_issue_ids`, `stable_issue_ids`, `stable_redline_ids`,37 an `issue_id` "one of:" list). When the template pins IDs, that list *is* the38 expected membership — produce every ID, no more and no fewer. When it does not,39 include an entry only where a record evidences it.40- **Enums that only make sense nested** (`fee_model`, `tax_allocation_method`, `forum`,41 `condition_type`) — these are telling you the vocabulary for the free-form42 `*_normalized` / `must_have_terms` objects. Use those exact strings there.43- **Units** — integer dollars, percent-point decimal places, integer months. The44 prompt sometimes overrides the template; the prompt wins.45- **Named final positions.** A `final_position` / `recommended_action` enum whose values46 map 1:1 onto the issue IDs is a decoded answer key — pair them by name and let the47 wording drive the row. These names are often self-describing, encoding the target48 number, the fallback, and the condition attached to it; read them as instructions.4950### 2. Pull the deal bundle5152`scripts/fetch_deal.py <base_url> <deal_id> <out_dir>` fetches the deal record, follows53its `links` map, and pulls the governing playbook or policy named on the deal. It54discovers routes from the deal record, so it does not depend on a hardcoded route list.5556The workbench holds dozens of decoy deals whose project names differ from the target's by57a syllable or a suffix, with cloned supporting rows carrying similar-looking amounts.58**Filter on the exact `deal_id` every time**, including in SQL. Never let a similarly59named project supply a number.6061### 3. Establish the governing standard6263The deal record names exactly one:6465| Field on deal | Standard | Key columns |66|---|---|---|67| `playbook_id` | `playbook_rules` | `preferred_position`, `fallback_position`, `limit_value`, `limit_unit`, `basis`, `risk_default` |68| `policy_id` | `policy_thresholds` | `policy_standard`, `threshold_value`, `restricted_flag`, `approval_required`, `basis` |6970Parse the numeric preferred/fallback out of the prose positions — `limit_value` alone is71ambiguous (it is sometimes the preferred bound, sometimes the fallback).7273### 4. Select the rows7475Use only `draft_terms` with `staleness_flag == "current"`. Then, per template shape:7677- **Issue register / deviation matrix** — one row per template-pinned issue ID. Map each78 to the current draft term(s) sharing its `category`; a pinned ID with no matching79 current term is `missing_required_term` with empty `source_term_ids`.80- **Committee escalation** — include a term only if *all* hold: current, its category has81 a governing threshold, the draft breaches it, and the threshold is in scope for the82 requested body (`restricted_flag == "yes"` / `approval_required` matches). Stale,83 in-policy, and lower-approval rows are deliberate distractors — put them in the84 template's `excluded_*` fields, not in the escalation list.8586`reference/classification_rules.md` has the status / risk / action decision tables and the87record-flag filters (consents, material contracts, employees).8889### 5. Compute the numbers9091- Base = the deal's `headline_value` unless the term or rule `basis` explicitly names92 another basis. `headline_value` also equals `upfront_cash + stock_value` in these deals;93 a draft term that states its own dollar figure confirms the base — check it.94- `amount = round(base * percent / 100)`, emitted as an **integer**.95- Deltas run in the direction that harms your client (see polarity table in96 `reference/classification_rules.md`). Report the gap to the *fallback* when the template97 asks for one gap, and both fallback and preferred gaps when it asks for two.98- Exposure ranges come from `risk_estimates` rows, matched by `category`. Count each99 category **once** in an aggregate even when several issues cite it, and list which100 categories you included/excluded if the template has fields for that.101- Every total must equal the sum of the components you actually emitted.102103### 6. Validate, then emit104105Run `scripts/validate_answer.py <candidate.json> <answer_template.json>`. It checks106required keys, enum membership, integer-dollar fields, percent rounding, and pinned-ID107coverage. Emit raw JSON only — no prose, no code fences, no trailing commentary.108109## Pitfalls110111- **Padding a required/blocking list with non-qualifying records.** The costliest112 recurring error. `required_for_closing == "no"` and `consent_required == "notice only"`113 are never blockers.114- **Inventing IDs.** Prefer stable workbench IDs (`CNS_*`, `MAT_*`, `EMP_*`, `TERM_*`,115 `RSK_*`, `FND_*`). Only synthesize one where the template explicitly permits it116 (e.g. "synthetic regulatory id").117- **Free-styling risk ratings.** Inherit `risk_default` from the governing rule, or the118 record's own `risk_rating`, before making a judgment call. Normalize to the enum's case.119- **Ignoring `client_side`.** It flips `draft_exceeds_playbook` vs `draft_below_playbook`120 and flips which party a term protects.121- **Double-counting an exposure category** across several issues in an aggregate total.122- **Trusting `limit_value`** instead of the preferred/fallback prose.123- **Wrong percent precision.** Two decimals, one decimal, and four-decimal holder124 percentages all appear; re-read the prompt for each task.125- Adding keys the template does not list is harmless; omitting listed keys is not.