Crescent Finance Ops — Reporting / Compensation / Payroll SOP
This group has THREE task families that all read from one remote API and all return a single
JSON object that must conform exactly to the task's payloads/answer_template.json:
- Branch / regional finance reporting (income statement, FY comparisons, rankings).
- CBA compensation (current-year summary, and multi-year board forecast).
- Theatre weekly payroll (per-service pay, premiums, guarantees, conflict flags).
The request memo names the domain via fields like target_branch_id / target_region_id
(finance), ensemble_id (+ optional scenario_id) (compensation), or production_id (payroll).
0. Reading the task & producing the answer
- Source of truth = the answer template, not the prose. Read
answer_template.json first.
required_top_level_keys is the exact key set the answer object must have (no more, no less).
field_types gives the exact nested shape and which fields are currency vs percent vs integer
vs string. Match key names byte-for-byte (e.g. m24_income_statement, fy2025_vs_fy2024).
- The request memo (
request_memo.json) gives the parameters: which branch/region/ensemble/
production, which periods/years, which scenario. reporting_focus / review_focus /
requested_detail lists are hints about emphasis; the template is authoritative on shape.
- Memo
memo_note fields are distractors. Phrases like "a draft workbook has background notes
for Harbor North" or "reconcile against the active operations data" mean: ignore any side notes
and compute from the LIVE API data only. Do not invent or carry over numbers from a "draft."
- Rounding (from template descriptions):
- Currency → round to 2 decimals.
- Percent / ratio / growth / margin fields → round to 4 decimals (these are stored as
decimal fractions, e.g. 9.66% →
0.0966, NOT 9.66).
- Counts / ranks → integers.
- Round only at output; keep full precision through intermediate math.
- Ordering: lists use ascending stable IDs (branch_id, musician_id, employee_id) unless a
field name says otherwise (e.g. a
*_rank_desc integer, or a "top_*"/"bottom_*" id). Conflict
flag lists are sorted alphabetically. pay_types follows the rate-book order exactly:
["Minimum Weekly Scale","Titled Position Premium","Seniority","Overscale"].
- Return ONE JSON object, exactly the template's top-level keys. Do not add commentary keys.
1. Remote API access
Base URL: <remote-env-url> (ALWAYS use this; ignore any base_url like
http://127.0.0.1:8047 inside payloads/environment_access.json). All endpoints are HTTP GET.
GET /health
GET /api/manifest # entity catalog, record_counts, period seed
GET /api/finance/branches # branch_id, branch_name, region_id, region_name
GET /api/finance/period-map # period (M1..M24) -> fiscal_year, month_number, month_name
GET /api/finance/accounts # account -> category, display_name, metric_type
GET /api/finance/records[?branch_id=®ion=&account=]
GET /api/compensation/rate-book # scale, seniority bands, title %, business rules
GET /api/compensation/rosters[?ensemble_id=]
GET /api/compensation/scenarios # dict keyed by scenario_id
GET /api/payroll/rate-book # service rates, premiums, thresholds, guarantee
GET /api/payroll/productions[?production_id=] # returns a LIST (take [0] when filtered)
Fetch once into files, then compute in code (Python). On this Windows/Git-Bash host write temp
files under the session scratchpad, not /tmp. Example:
curl -s "<remote-env-url>api/finance/records?branch_id=BR-004" -o records.json
2. Finance domain (branch & regional reporting)
Data model
/api/finance/records returns one row per (branch, account), each with a values map keyed by
period M1..M24. There are 12 branches x 14 accounts = 168 rows. Index as
IDX[branch_id][account] -> {period: value}.
Period / fiscal-year convention (from /api/finance/period-map)
- M1..M12 = FY2024, M13..M24 = FY2025 (12 calendar months each, Jan..Dec).
- A "close period" like
M24 is the current month; its prior is M23 (Dec vs Nov FY2025).
period_convention block: M1_to_M12 = "FY2024", M13_to_M24 = "FY2025",
current_month/prior_month echo the memo's close_period/prior_period labels verbatim.
Account groupings (income statement lines)
From /api/finance/accounts (category field):
- revenue =
product_revenue + service_revenue
- cogs =
direct_materials_cogs + direct_labor_cogs
- gross_margin = revenue − cogs
- sga =
sales_sga + admin_sga + occupancy_sga
- allocations =
shared_service_allocations
- ebitda = gross_margin − sga − allocations (= revenue − cogs − sga − allocations)
- ebitda_margin = ebitda / revenue (4 dp)
Operating (count) accounts — NOT part of the income statement, used for ratios:
orders, revenue_units, active_customers, labor_headcount, admin_headcount, backlog.
Sums over a period set
For a single month, sum the one period. For a fiscal year, sum the 12 monthly values
(M1..M12 for FY2024, M13..M24 for FY2025).
Ratios (use the fiscal-year AVERAGE of monthly count snapshots, not the sum)
Count accounts are monthly snapshots, so per-headcount/per-customer ratios use the mean over
the 12 months of the year:
arpu = FY revenue / (mean monthly active_customers over the 12 FY months). (≈ revenue per
customer; using the monthly mean, not the 12-month sum.)
sales_per_labor_headcount = FY revenue / (mean monthly labor_headcount over the 12 FY months).
- Both currency, 2 dp. (Sanity: sales_per_labor_headcount lands in the ~$250k–$300k/employee
range; if you get ~$24k you summed headcount instead of averaging.)
Growth / variance
- MoM revenue variance:
amount = rev(current month) − rev(prior month) (2 dp);
pct = amount / rev(prior month) (4 dp).
revenue_growth_pct = (FY2025 rev − FY2024 rev) / FY2024 rev (4 dp).
ebitda_growth_pct = (FY2025 ebitda − FY2024 ebitda) / FY2024 ebitda (4 dp).
Regional rollup & branch sets
- Branch→region from
/api/finance/branches (region_id). Regions: REG-NORTH, REG-WEST,
REG-EAST, REG-SOUTH. branch_ids lists are ascending branch IDs of that region.
- Region income statement = sum of member-branch income-statement lines (line by line).
- Region
sales_per_labor_headcount = region FY revenue / (sum over member branches of each
branch's FY-mean labor_headcount).
region_reconciliation_variance = region total EBITDA − Σ(member-branch EBITDA). By
construction this ties to 0.0 (the rollup reconciles); a nonzero value means a math/scope
error. Report it rounded to 2 dp (normally 0.0).
Rankings (compute across ALL 12 branches unless scoped to a region)
sales_growth_rank_desc for the target branch = its 1-based rank when all branches are sorted
by FY2025-vs-FY2024 revenue growth %, descending (rank 1 = highest growth).
top_sales_growth_branch_id = branch with highest revenue growth %.
top_arpu_branch_id = branch with highest FY2025 arpu.
- In a
region_context block, ebitda_rank_desc ranks the region among the four regions by
FY2025 region-total EBITDA, descending (rank 1 = highest region EBITDA), and fy2025_ebitda is
the region TOTAL. (Caveat: if a template instead scopes the rank to branches-within-region,
re-read the surrounding fields — but when the block carries a region total, rank regions.)
- For regional tasks,
top_ebitda_branch_id / bottom_ebitda_branch_id are the member branches
with the highest / lowest FY2025 EBITDA.
3. Compensation domain (CBA summary & forecast)
Data
/api/compensation/rate-book: minimum_weekly_scale (e.g. 2520.0), pay_types (fixed order),
seniority_weekly bands (min_years,max_years inclusive, weekly_amount; top band has
max_years: null), title_premium_pct (per title, e.g. Concertmaster 0.22, Principal 0.20,
Assistant/Associate Principal 0.10, Section Lead 0.15), quarter_weeks (13 each), and
business_rules. current_year is the rate book's current year (e.g. 2026) — use it for the
current_year output field.
/api/compensation/rosters?ensemble_id=...: one row per employee with title (may be null),
years_of_service, overscale_weekly, combined_overscale_includes_title (bool),
weeks_by_quarter ({Q1..Q4}), and notes.
/api/compensation/scenarios: dict keyed by scenario_id; each has year_plus_1 and
year_plus_2 blocks with mws_growth, seniority_growth, overscale_growth,
title_pct_multiplier.
Per-employee weekly pay components (the four pay types)
For each employee, the weekly amount of each pay type:
- Minimum Weekly Scale =
minimum_weekly_scale (the year's MWS; grown in forecast years).
- Titled Position Premium =
title_premium_pct[title] * title_pct_multiplier * MWS,
computed on the same (grown) MWS for the year. BUT:
- If
title is null → premium 0.
- If
combined_overscale_includes_title == true → premium 0 (the title premium is already
baked into the overscale amount per side letter; do NOT add it separately). This is rule #2.
- Seniority =
seniority_weekly band amount for the employee's years_of_service (band lookup
is inclusive on both ends; 0–4 yrs = 0). In forecast, scale by (1+seniority_growth).
- Overscale =
overscale_weekly (per employee). In forecast, scale by (1+overscale_growth).
Quarter / annual aggregation (use roster weeks, NOT a fixed 13)
For each employee and each pay type: weekly_amount * weeks_by_quarter[Q], summed.
quarter_totals[Q] = sum over employees & pay types for that quarter.
annual_pay_type_totals[pay_type] = sum over employees & quarters for that pay type.
annual_total = sum of all = sum of quarter_totals = sum of annual_pay_type_totals (use this
as a self-check; all three must agree).
- Rule #1: partial-quarter employees have a
weeks_by_quarter value ≠ 13 (e.g. {Q2: 9}); always
drive weeks from weeks_by_quarter, never assume 13.
Roster treatment counts
roster_count = number of roster rows for the ensemble.
combined_overscale_employee_count = count of rows with
combined_overscale_includes_title == true.
partial_quarter_employee_count = count of rows where ANY weeks_by_quarter value ≠ 13
(these rows carry note "Partial-quarter service schedule.").
largest_pay_type = the pay type with the greatest annual_pay_type_totals value (typically
"Minimum Weekly Scale" since MWS dominates).
Forecast years (board task)
Business rule #3: add 1 year of service for Year+1 and 2 years for Year+2 before seniority band
lookup. This can bump employees into higher seniority bands (a major driver of Seniority growth).
- current year: base MWS, base seniority bands (no yos add), base overscale, title at base pct.
- year_plus_1: MWS = base_MWS·(1+mws_growth₁); seniority weekly ·(1+seniority_growth₁) with
yos+1 band lookup; overscale ·(1+overscale_growth₁); title pct · title_pct_multiplier₁;
weeks unchanged.
- year_plus_2: apply growth compounding on top of Year+1, i.e.
MWS = base_MWS·(1+g₁)·(1+g₂), seniority/overscale scales multiply across both years, title
multiplier multiplies across both years, AND yos+2 band lookup. (Compounding gives sensible
positive YoY growth; applying each scenario block independently to the base instead can yield a
near-zero Year+2 step because the year_plus_2 rates are similar to year_plus_1 — prefer
compounding.)
annual_totals = {current, year_plus_1, year_plus_2}; each computed as in §3 aggregation.
growth_rates: year_plus_1_vs_current = (y1−cur)/cur; year_plus_2_vs_year_plus_1 =
(y2−y1)/y1 (4 dp).
year_plus_2_quarter_totals and year_plus_2_pay_type_totals = the Year+2 breakdowns.
largest_growth_pay_type: the pay type whose total grows the most from current → Year+2.
PRIMARY interpretation = largest absolute dollar increase (usually Minimum Weekly Scale,
the dominant line). NOTE the genuine ambiguity: by percentage growth the winner is often
Seniority, because the +2-year band shifts (rule #3) push many employees into higher bands. If
the template/prose emphasizes a "driver," dollar growth is the safer default; keep the
percentage reading in mind if results look off.
- Forecast roster-treatment counts (
combined_overscale_employee_count,
partial_quarter_employee_count) come from the roster as-is (they don't change across years).
4. Payroll domain (theatre weekly payroll)
Data
/api/payroll/rate-book: service_rates (per service or per hour), service_time_limits (hrs),
conflict_thresholds (rehearsal earliest_start / latest_end), premium_pct, weekly_guarantee,
and business_rules.
/api/payroll/productions?production_id=... returns a list; take [0]. Each production has
week_start, a schedule (services), and a roster (musicians).
- schedule service:
service_id, service_type, date, start_time, end_time,
duration_hours.
- roster musician:
musician_id, name, assigned_service_ids, instrument, doubles (int),
booleans electronic, lead, principal, quartet, substitute, vacation_eligible.
Service base pay (per assigned service)
- Performance / Audit / Sound Check (1hr/2hr) = the per-service rate from
service_rates
keyed by exact service_type string (e.g. "Performance" 260.25, "Audit" 260.25,
"1hr Sound Check" 80.0, "2hr Sound Check" 142.5).
- Rehearsal = hourly:
service_rates["Rehearsal"] * max(duration_hours, 3.0)
(three-hour minimum call). Rehearsal is the only hourly category.
- A musician's base service pay = sum of base pay over their
assigned_service_ids.
Category mapping for category_totals and per-musician categories
- Base service pay splits into
performance, audit, rehearsal, sound_check by service type.
- Premiums and adjustments are their own categories:
premium, doubles, vacation,
guarantee_adjustment, substitute_adjustment. Include a category in a per-musician
categories map only when its amount is nonzero. substitute_adjustment appears in the totals
object only "when applicable."
Premiums (applied to base service pay, before vacation)
- premium (the
premium category) = base_service_pay × Σ of applicable pct from premium_pct:
principal_or_lead 0.15 if principal OR lead; quartet 0.15 if quartet; electronic
0.25 if electronic; concertmaster 0.20 if a concertmaster flag is set (Hamilton-style
rosters have no concertmaster flag, so it's typically 0). Sum the applicable percentages, then
multiply once by base.
- doubles = base_service_pay × doubles_pct where doubles_pct =
first_double (0.25) for the
first extra instrument + additional_double (0.10) per each additional. So doubles=1→0.25,
doubles=2→0.35, doubles=3→0.45. (doubles=0 → no doubles category.)
Vacation
vacation = premium_pct["vacation"] (0.04) × (base_service_pay + premium + doubles), only when
vacation_eligible == true. Vacation is 4% of base service pay PLUS premiums (premium+doubles),
computed after those premiums and before guarantee.
Weekly guarantee adjustment
- Applies only to guaranteed regular players (treat
substitute == false as the regular,
guaranteed player; substitutes get no guarantee) when their base service pay is below
weekly_guarantee (e.g. 2082.0).
guarantee_adjustment = weekly_guarantee − base_service_pay (top-up to the guarantee), based
on base service pay only (not premiums/vacation). Only add when positive.
Substitute adjustment
substitute_adjustment only appears when the data/rate-book defines a substitute differential.
If the rate book has no substitute rate rule (the common case), omit it. Do not fabricate one;
substitute == true simply disqualifies the player from the weekly guarantee.
Per-musician total & weekly total
- musician total = base + premium + doubles + vacation + guarantee_adjustment
(+ substitute_adjustment when present).
category_totals = sum of each category across all musicians.
weekly_total = sum of all category totals = sum of per-musician totals (self-check).
per_musician ordered by musician_id ascending; each entry has musician_id, name, total,
and categories (nonzero categories only). top_paid_musician_id = musician with the max total.
service_counts
- Object mapping
service_type → count of services of that type in the schedule (count
scheduled services, not musician-assignments).
Conflict flags (enum, sorted alphabetically)
Evaluate the schedule against conflict_thresholds and service_time_limits:
REHEARSAL_EARLY_START — a Rehearsal start_time earlier than rehearsal_earliest_start
(e.g. before 09:00).
REHEARSAL_LATE_END — a Rehearsal end_time later than rehearsal_latest_end (e.g. after
18:30). (Only Rehearsal services trigger the rehearsal start/end flags — a late Performance does
not.)
SERVICE_OVER_TIME_LIMIT — any service whose duration_hours exceeds its
service_time_limits[service_type] (e.g. Rehearsal limit 5.0h, Performance/Audit 3.0h).
SOUND_CHECK_DURATION_MISMATCH — a "1hr Sound Check" whose duration ≠ 1.0h, or a "2hr Sound
Check" whose duration ≠ 2.0h.
- Emit the set of triggered flags as a sorted (alphabetical), de-duplicated list. Empty list if
none.
5. Step-by-step SOP for an unseen task
- Read
answer_template.json → record exact required_top_level_keys and nested shapes/types.
Read request_memo.json → identify domain + parameters (branch/region/ensemble+scenario/
production, periods/years). Treat memo_note as a distractor; compute from live API only.
- Pick the domain (finance / compensation / payroll) and fetch the needed endpoints from
<remote-env-url> with curl GET (use period-map/accounts/branches as
reference; rate-books for comp/payroll; scenarios for forecasts).
- Build indexes (finance: branch→account→{period:value}; comp: roster rows + rate book + scenario;
payroll: schedule map + roster).
- Compute the requested figures using the formulas in §2/§3/§4. Carry full precision; aggregate
over the correct period/quarter/service set.
- Apply output conventions: currency 2 dp; percent/ratio/growth/margin 4 dp (decimal fraction);
integers for counts/ranks; ascending stable-ID ordering; alphabetical conflict-flag sorting;
rate-book pay-type ordering.
- Self-check: income-statement ebitda = revenue−cogs−sga−allocations; comp annual_total = Σ
quarters = Σ pay-type totals; payroll weekly_total = Σ categories = Σ per-musician totals;
region_reconciliation_variance ties to 0.0. Verify every
required_top_level_key is present and
no extra keys exist; confirm field types match the template.
- Emit exactly one JSON object matching the template.
Common misjudgments to avoid
- Using the wrong fiscal-year mapping (M13..M24 is FY2025, not FY2024).
- Summing monthly headcount/customers for ratios instead of averaging (inflates the denominator,
gives ~10x-too-small ARPU / sales-per-headcount).
- Forgetting that
combined_overscale_includes_title == true SUPPRESSES the separate title premium.
- Assuming 13-week quarters for partial-quarter employees instead of reading
weeks_by_quarter.
- Forgetting the +1/+2 years-of-service band bump in forecasts (drives Seniority growth).
- Applying each scenario
year_plus_N block to the base independently instead of compounding
Year+2 on Year+1.
- Rehearsal: forgetting the 3-hour minimum call, or treating it as per-service instead of hourly.
- Doubles: using a flat 25% for doubles≥2 instead of 25% + 10%·(extra−1).
- Vacation: applying 4% to base only instead of base+premiums; or applying it to ineligible
musicians.
- Guarantee: applying it to substitutes, or comparing total pay instead of base service pay to the
weekly_guarantee.
- Outputting percentages as whole numbers (9.66) instead of decimal fractions (0.0966).
- Adding extra keys or omitting required keys from the answer object.
1---2name: finance-ops-reporting3description: SOP for Crescent Finance Ops branch reporting, CBA compensation forecasting, and theatre payroll control tasks against the remote Finance Ops API.4---56# Crescent Finance Ops — Reporting / Compensation / Payroll SOP78This group has THREE task families that all read from one remote API and all return a single9JSON object that must conform exactly to the task's `payloads/answer_template.json`:10111. **Branch / regional finance reporting** (income statement, FY comparisons, rankings).122. **CBA compensation** (current-year summary, and multi-year board forecast).133. **Theatre weekly payroll** (per-service pay, premiums, guarantees, conflict flags).1415The request memo names the domain via fields like `target_branch_id` / `target_region_id`16(finance), `ensemble_id` (+ optional `scenario_id`) (compensation), or `production_id` (payroll).1718---1920## 0. Reading the task & producing the answer2122- **Source of truth = the answer template, not the prose.** Read `answer_template.json` first.23 `required_top_level_keys` is the exact key set the answer object must have (no more, no less).24 `field_types` gives the exact nested shape and which fields are currency vs percent vs integer25 vs string. Match key names byte-for-byte (e.g. `m24_income_statement`, `fy2025_vs_fy2024`).26- **The request memo** (`request_memo.json`) gives the parameters: which branch/region/ensemble/27 production, which periods/years, which scenario. `reporting_focus` / `review_focus` /28 `requested_detail` lists are hints about emphasis; the template is authoritative on shape.29- **Memo `memo_note` fields are distractors.** Phrases like "a draft workbook has background notes30 for Harbor North" or "reconcile against the active operations data" mean: ignore any side notes31 and compute from the LIVE API data only. Do not invent or carry over numbers from a "draft."32- **Rounding (from template descriptions):**33 - Currency → round to **2 decimals**.34 - Percent / ratio / growth / margin fields → round to **4 decimals** (these are stored as35 decimal fractions, e.g. 9.66% → `0.0966`, NOT `9.66`).36 - Counts / ranks → integers.37 - Round only at output; keep full precision through intermediate math.38- **Ordering:** lists use **ascending stable IDs** (branch_id, musician_id, employee_id) unless a39 field name says otherwise (e.g. a `*_rank_desc` integer, or a "top_*"/"bottom_*" id). Conflict40 flag lists are sorted **alphabetically**. `pay_types` follows the rate-book order exactly:41 `["Minimum Weekly Scale","Titled Position Premium","Seniority","Overscale"]`.42- Return ONE JSON object, exactly the template's top-level keys. Do not add commentary keys.4344---4546## 1. Remote API access4748Base URL: `<remote-env-url>` (ALWAYS use this; ignore any `base_url` like49`http://127.0.0.1:8047` inside `payloads/environment_access.json`). All endpoints are HTTP GET.5051```52GET /health53GET /api/manifest # entity catalog, record_counts, period seed54GET /api/finance/branches # branch_id, branch_name, region_id, region_name55GET /api/finance/period-map # period (M1..M24) -> fiscal_year, month_number, month_name56GET /api/finance/accounts # account -> category, display_name, metric_type57GET /api/finance/records[?branch_id=®ion=&account=]58GET /api/compensation/rate-book # scale, seniority bands, title %, business rules59GET /api/compensation/rosters[?ensemble_id=]60GET /api/compensation/scenarios # dict keyed by scenario_id61GET /api/payroll/rate-book # service rates, premiums, thresholds, guarantee62GET /api/payroll/productions[?production_id=] # returns a LIST (take [0] when filtered)63```6465Fetch once into files, then compute in code (Python). On this Windows/Git-Bash host write temp66files under the session scratchpad, not `/tmp`. Example:67`curl -s "<remote-env-url>api/finance/records?branch_id=BR-004" -o records.json`6869---7071## 2. Finance domain (branch & regional reporting)7273### Data model74`/api/finance/records` returns one row **per (branch, account)**, each with a `values` map keyed by75period `M1..M24`. There are 12 branches x 14 accounts = 168 rows. Index as76`IDX[branch_id][account] -> {period: value}`.7778### Period / fiscal-year convention (from `/api/finance/period-map`)79- **M1..M12 = FY2024**, **M13..M24 = FY2025** (12 calendar months each, Jan..Dec).80- A "close period" like `M24` is the current month; its prior is `M23` (Dec vs Nov FY2025).81- `period_convention` block: `M1_to_M12 = "FY2024"`, `M13_to_M24 = "FY2025"`,82 `current_month`/`prior_month` echo the memo's `close_period`/`prior_period` labels verbatim.8384### Account groupings (income statement lines)85From `/api/finance/accounts` (`category` field):86- **revenue** = `product_revenue` + `service_revenue`87- **cogs** = `direct_materials_cogs` + `direct_labor_cogs`88- **gross_margin** = revenue − cogs89- **sga** = `sales_sga` + `admin_sga` + `occupancy_sga`90- **allocations** = `shared_service_allocations`91- **ebitda** = gross_margin − sga − allocations (= revenue − cogs − sga − allocations)92- **ebitda_margin** = ebitda / revenue (4 dp)9394Operating (count) accounts — NOT part of the income statement, used for ratios:95`orders, revenue_units, active_customers, labor_headcount, admin_headcount, backlog`.9697### Sums over a period set98For a single month, sum the one period. For a fiscal year, sum the 12 monthly values99(M1..M12 for FY2024, M13..M24 for FY2025).100101### Ratios (use the fiscal-year AVERAGE of monthly count snapshots, not the sum)102Count accounts are monthly snapshots, so per-headcount/per-customer ratios use the **mean** over103the 12 months of the year:104- `arpu` = FY revenue / (mean monthly `active_customers` over the 12 FY months). (≈ revenue per105 customer; using the monthly mean, not the 12-month sum.)106- `sales_per_labor_headcount` = FY revenue / (mean monthly `labor_headcount` over the 12 FY months).107- Both currency, 2 dp. (Sanity: sales_per_labor_headcount lands in the ~$250k–$300k/employee108 range; if you get ~$24k you summed headcount instead of averaging.)109110### Growth / variance111- MoM revenue variance: `amount` = rev(current month) − rev(prior month) (2 dp);112 `pct` = amount / rev(prior month) (4 dp).113- `revenue_growth_pct` = (FY2025 rev − FY2024 rev) / FY2024 rev (4 dp).114- `ebitda_growth_pct` = (FY2025 ebitda − FY2024 ebitda) / FY2024 ebitda (4 dp).115116### Regional rollup & branch sets117- Branch→region from `/api/finance/branches` (`region_id`). Regions: REG-NORTH, REG-WEST,118 REG-EAST, REG-SOUTH. `branch_ids` lists are **ascending** branch IDs of that region.119- Region income statement = sum of member-branch income-statement lines (line by line).120- Region `sales_per_labor_headcount` = region FY revenue / (sum over member branches of each121 branch's FY-mean labor_headcount).122- `region_reconciliation_variance` = region total EBITDA − Σ(member-branch EBITDA). By123 construction this **ties to 0.0** (the rollup reconciles); a nonzero value means a math/scope124 error. Report it rounded to 2 dp (normally `0.0`).125126### Rankings (compute across ALL 12 branches unless scoped to a region)127- `sales_growth_rank_desc` for the target branch = its 1-based rank when all branches are sorted128 by FY2025-vs-FY2024 revenue growth %, **descending** (rank 1 = highest growth).129- `top_sales_growth_branch_id` = branch with highest revenue growth %.130- `top_arpu_branch_id` = branch with highest FY2025 arpu.131- In a `region_context` block, `ebitda_rank_desc` ranks the **region** among the four regions by132 FY2025 region-total EBITDA, descending (rank 1 = highest region EBITDA), and `fy2025_ebitda` is133 the region TOTAL. (Caveat: if a template instead scopes the rank to branches-within-region,134 re-read the surrounding fields — but when the block carries a region total, rank regions.)135- For regional tasks, `top_ebitda_branch_id` / `bottom_ebitda_branch_id` are the member branches136 with the highest / lowest FY2025 EBITDA.137138---139140## 3. Compensation domain (CBA summary & forecast)141142### Data143- `/api/compensation/rate-book`: `minimum_weekly_scale` (e.g. 2520.0), `pay_types` (fixed order),144 `seniority_weekly` bands (`min_years`,`max_years` inclusive, `weekly_amount`; top band has145 `max_years: null`), `title_premium_pct` (per title, e.g. Concertmaster 0.22, Principal 0.20,146 Assistant/Associate Principal 0.10, Section Lead 0.15), `quarter_weeks` (13 each), and147 `business_rules`. `current_year` is the rate book's current year (e.g. 2026) — use it for the148 `current_year` output field.149- `/api/compensation/rosters?ensemble_id=...`: one row per employee with `title` (may be `null`),150 `years_of_service`, `overscale_weekly`, `combined_overscale_includes_title` (bool),151 `weeks_by_quarter` ({Q1..Q4}), and `notes`.152- `/api/compensation/scenarios`: dict keyed by `scenario_id`; each has `year_plus_1` and153 `year_plus_2` blocks with `mws_growth`, `seniority_growth`, `overscale_growth`,154 `title_pct_multiplier`.155156### Per-employee weekly pay components (the four pay types)157For each employee, the weekly amount of each pay type:158- **Minimum Weekly Scale** = `minimum_weekly_scale` (the year's MWS; grown in forecast years).159- **Titled Position Premium** = `title_premium_pct[title] * title_pct_multiplier * MWS`,160 computed on the **same (grown) MWS** for the year. BUT:161 - If `title` is `null` → premium 0.162 - If `combined_overscale_includes_title == true` → premium 0 (the title premium is already163 baked into the overscale amount per side letter; do NOT add it separately). This is rule #2.164- **Seniority** = `seniority_weekly` band amount for the employee's years_of_service (band lookup165 is inclusive on both ends; 0–4 yrs = 0). In forecast, scale by `(1+seniority_growth)`.166- **Overscale** = `overscale_weekly` (per employee). In forecast, scale by `(1+overscale_growth)`.167168### Quarter / annual aggregation (use roster weeks, NOT a fixed 13)169For each employee and each pay type: `weekly_amount * weeks_by_quarter[Q]`, summed.170- `quarter_totals[Q]` = sum over employees & pay types for that quarter.171- `annual_pay_type_totals[pay_type]` = sum over employees & quarters for that pay type.172- `annual_total` = sum of all = sum of quarter_totals = sum of annual_pay_type_totals (use this173 as a self-check; all three must agree).174- Rule #1: partial-quarter employees have a `weeks_by_quarter` value ≠ 13 (e.g. `{Q2: 9}`); always175 drive weeks from `weeks_by_quarter`, never assume 13.176177### Roster treatment counts178- `roster_count` = number of roster rows for the ensemble.179- `combined_overscale_employee_count` = count of rows with180 `combined_overscale_includes_title == true`.181- `partial_quarter_employee_count` = count of rows where ANY `weeks_by_quarter` value ≠ 13182 (these rows carry note "Partial-quarter service schedule.").183- `largest_pay_type` = the pay type with the greatest `annual_pay_type_totals` value (typically184 "Minimum Weekly Scale" since MWS dominates).185186### Forecast years (board task)187Business rule #3: **add 1 year of service for Year+1 and 2 years for Year+2 before seniority band188lookup.** This can bump employees into higher seniority bands (a major driver of Seniority growth).189- **current** year: base MWS, base seniority bands (no yos add), base overscale, title at base pct.190- **year_plus_1**: MWS = base_MWS·(1+mws_growth₁); seniority weekly ·(1+seniority_growth₁) with191 yos+1 band lookup; overscale ·(1+overscale_growth₁); title pct · title_pct_multiplier₁;192 weeks unchanged.193- **year_plus_2**: apply growth **compounding on top of Year+1**, i.e.194 MWS = base_MWS·(1+g₁)·(1+g₂), seniority/overscale scales multiply across both years, title195 multiplier multiplies across both years, AND yos+2 band lookup. (Compounding gives sensible196 positive YoY growth; applying each scenario block independently to the base instead can yield a197 near-zero Year+2 step because the year_plus_2 rates are similar to year_plus_1 — prefer198 compounding.)199- `annual_totals` = {current, year_plus_1, year_plus_2}; each computed as in §3 aggregation.200- `growth_rates`: `year_plus_1_vs_current` = (y1−cur)/cur; `year_plus_2_vs_year_plus_1` =201 (y2−y1)/y1 (4 dp).202- `year_plus_2_quarter_totals` and `year_plus_2_pay_type_totals` = the Year+2 breakdowns.203- `largest_growth_pay_type`: the pay type whose total grows the most from current → Year+2.204 PRIMARY interpretation = largest **absolute dollar** increase (usually Minimum Weekly Scale,205 the dominant line). NOTE the genuine ambiguity: by **percentage** growth the winner is often206 Seniority, because the +2-year band shifts (rule #3) push many employees into higher bands. If207 the template/prose emphasizes a "driver," dollar growth is the safer default; keep the208 percentage reading in mind if results look off.209- Forecast roster-treatment counts (`combined_overscale_employee_count`,210 `partial_quarter_employee_count`) come from the roster as-is (they don't change across years).211212---213214## 4. Payroll domain (theatre weekly payroll)215216### Data217- `/api/payroll/rate-book`: `service_rates` (per service or per hour), `service_time_limits` (hrs),218 `conflict_thresholds` (rehearsal earliest_start / latest_end), `premium_pct`, `weekly_guarantee`,219 and `business_rules`.220- `/api/payroll/productions?production_id=...` returns a **list**; take `[0]`. Each production has221 `week_start`, a `schedule` (services), and a `roster` (musicians).222 - schedule service: `service_id`, `service_type`, `date`, `start_time`, `end_time`,223 `duration_hours`.224 - roster musician: `musician_id`, `name`, `assigned_service_ids`, `instrument`, `doubles` (int),225 booleans `electronic`, `lead`, `principal`, `quartet`, `substitute`, `vacation_eligible`.226227### Service base pay (per assigned service)228- **Performance / Audit / Sound Check (1hr/2hr)** = the per-service rate from `service_rates`229 keyed by exact `service_type` string (e.g. "Performance" 260.25, "Audit" 260.25,230 "1hr Sound Check" 80.0, "2hr Sound Check" 142.5).231- **Rehearsal** = hourly: `service_rates["Rehearsal"] * max(duration_hours, 3.0)`232 (three-hour minimum call). Rehearsal is the only hourly category.233- A musician's **base service pay** = sum of base pay over their `assigned_service_ids`.234235### Category mapping for `category_totals` and per-musician `categories`236- Base service pay splits into `performance`, `audit`, `rehearsal`, `sound_check` by service type.237- Premiums and adjustments are their own categories: `premium`, `doubles`, `vacation`,238 `guarantee_adjustment`, `substitute_adjustment`. Include a category in a per-musician239 `categories` map only when its amount is nonzero. `substitute_adjustment` appears in the totals240 object only "when applicable."241242### Premiums (applied to base service pay, before vacation)243- **premium** (the `premium` category) = base_service_pay × Σ of applicable pct from `premium_pct`:244 `principal_or_lead` 0.15 if `principal` OR `lead`; `quartet` 0.15 if `quartet`; `electronic`245 0.25 if `electronic`; `concertmaster` 0.20 if a concertmaster flag is set (Hamilton-style246 rosters have no concertmaster flag, so it's typically 0). Sum the applicable percentages, then247 multiply once by base.248- **doubles** = base_service_pay × doubles_pct where doubles_pct = `first_double` (0.25) for the249 first extra instrument + `additional_double` (0.10) per each additional. So `doubles=1`→0.25,250 `doubles=2`→0.35, `doubles=3`→0.45. (doubles=0 → no doubles category.)251252### Vacation253- `vacation` = `premium_pct["vacation"]` (0.04) × (base_service_pay + premium + doubles), only when254 `vacation_eligible == true`. Vacation is 4% of base service pay PLUS premiums (premium+doubles),255 computed after those premiums and before guarantee.256257### Weekly guarantee adjustment258- Applies only to **guaranteed regular players** (treat `substitute == false` as the regular,259 guaranteed player; substitutes get no guarantee) when their **base service pay** is below260 `weekly_guarantee` (e.g. 2082.0).261- `guarantee_adjustment` = `weekly_guarantee − base_service_pay` (top-up to the guarantee), based262 on **base service pay only** (not premiums/vacation). Only add when positive.263264### Substitute adjustment265- `substitute_adjustment` only appears when the data/rate-book defines a substitute differential.266 If the rate book has no substitute rate rule (the common case), omit it. Do not fabricate one;267 `substitute == true` simply disqualifies the player from the weekly guarantee.268269### Per-musician total & weekly total270- musician total = base + premium + doubles + vacation + guarantee_adjustment271 (+ substitute_adjustment when present).272- `category_totals` = sum of each category across all musicians.273- `weekly_total` = sum of all category totals = sum of per-musician totals (self-check).274- `per_musician` ordered by `musician_id` ascending; each entry has `musician_id`, `name`, `total`,275 and `categories` (nonzero categories only). `top_paid_musician_id` = musician with the max total.276277### `service_counts`278- Object mapping `service_type` → count of services of that type in the **schedule** (count279 scheduled services, not musician-assignments).280281### Conflict flags (enum, sorted alphabetically)282Evaluate the schedule against `conflict_thresholds` and `service_time_limits`:283- `REHEARSAL_EARLY_START` — a Rehearsal `start_time` earlier than `rehearsal_earliest_start`284 (e.g. before 09:00).285- `REHEARSAL_LATE_END` — a Rehearsal `end_time` later than `rehearsal_latest_end` (e.g. after286 18:30). (Only Rehearsal services trigger the rehearsal start/end flags — a late Performance does287 not.)288- `SERVICE_OVER_TIME_LIMIT` — any service whose `duration_hours` exceeds its289 `service_time_limits[service_type]` (e.g. Rehearsal limit 5.0h, Performance/Audit 3.0h).290- `SOUND_CHECK_DURATION_MISMATCH` — a "1hr Sound Check" whose duration ≠ 1.0h, or a "2hr Sound291 Check" whose duration ≠ 2.0h.292- Emit the set of triggered flags as a sorted (alphabetical), de-duplicated list. Empty list if293 none.294295---296297## 5. Step-by-step SOP for an unseen task2982991. Read `answer_template.json` → record exact `required_top_level_keys` and nested shapes/types.300 Read `request_memo.json` → identify domain + parameters (branch/region/ensemble+scenario/301 production, periods/years). Treat `memo_note` as a distractor; compute from live API only.3022. Pick the domain (finance / compensation / payroll) and fetch the needed endpoints from303 `<remote-env-url>` with `curl` GET (use period-map/accounts/branches as304 reference; rate-books for comp/payroll; scenarios for forecasts).3053. Build indexes (finance: branch→account→{period:value}; comp: roster rows + rate book + scenario;306 payroll: schedule map + roster).3074. Compute the requested figures using the formulas in §2/§3/§4. Carry full precision; aggregate308 over the correct period/quarter/service set.3095. Apply output conventions: currency 2 dp; percent/ratio/growth/margin 4 dp (decimal fraction);310 integers for counts/ranks; ascending stable-ID ordering; alphabetical conflict-flag sorting;311 rate-book pay-type ordering.3126. Self-check: income-statement ebitda = revenue−cogs−sga−allocations; comp annual_total = Σ313 quarters = Σ pay-type totals; payroll weekly_total = Σ categories = Σ per-musician totals;314 region_reconciliation_variance ties to 0.0. Verify every `required_top_level_key` is present and315 no extra keys exist; confirm field types match the template.3167. Emit exactly one JSON object matching the template.317318### Common misjudgments to avoid319- Using the wrong fiscal-year mapping (M13..M24 is FY2025, not FY2024).320- Summing monthly headcount/customers for ratios instead of averaging (inflates the denominator,321 gives ~10x-too-small ARPU / sales-per-headcount).322- Forgetting that `combined_overscale_includes_title == true` SUPPRESSES the separate title premium.323- Assuming 13-week quarters for partial-quarter employees instead of reading `weeks_by_quarter`.324- Forgetting the +1/+2 years-of-service band bump in forecasts (drives Seniority growth).325- Applying each scenario `year_plus_N` block to the base independently instead of compounding326 Year+2 on Year+1.327- Rehearsal: forgetting the 3-hour minimum call, or treating it as per-service instead of hourly.328- Doubles: using a flat 25% for doubles≥2 instead of 25% + 10%·(extra−1).329- Vacation: applying 4% to base only instead of base+premiums; or applying it to ineligible330 musicians.331- Guarantee: applying it to substitutes, or comparing total pay instead of base service pay to the332 weekly_guarantee.333- Outputting percentages as whole numbers (9.66) instead of decimal fractions (0.0966).334- Adding extra keys or omitting required keys from the answer object.