<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Adds C3.5 ΓÇö the third per-product benchmark check in the C3.x family, cloned mechanically from C3.3 (engineering_product_benchmark) and its sibling C3.4 (saas_it_ops_benchmark). The novel dimension this check exercises is section-header-row addressing: the Edge metric lives at the Benchmark by Product table's edge section HEADER row, not a category row inside the total section.
### Verdict bands
| Verdict | Range |
| --------- | ------------------------------ |
| Pass | actual Γëñ 5.0% |
| Warning | 5.0% < actual Γëñ 10.0% (+5pp band) |
| Critical | actual > 10.0% |
Trilogy-wide Edge spend benchmark is 5% of revenue. The 5pp warning band tracks C3.3's preliminary calibration ΓÇö Finance review of the per-metric bands is tracked separately and intentionally NOT tuned within this check.
### Reuse of shared _helpers.py primitives
No modifications to _helpers.py ΓÇö C3.5 imports the existing public surface as-is:
- benchmark_product_columns(header) ΓÇö enumerate product columns
- benchmarks_row_for(table, section, category) ΓÇö locate the (edge, Edge) row
- _parse_cell ΓÇö parse percent / blank cells
- BenchmarkColumn ΓÇö NamedTuple for column metadata
- BenchmarkPerProductSupport ΓÇö pinned per-product supporting_data schema
- BenchmarkAggregateSupport ΓÇö pinned BU-level aggregate-pass supporting_data schema
Both Pydantic models are passed through .model_dump() at the call site, matching the C3.3 / C3.4 contract.
### _TARGET_SECTION = "edge" ΓÇö the C3.5 deviation worth flagging
C3.3 / C3.4 (and the planned C3.6 / C3.8 / C3.9) read a CATEGORY row inside the total section (e.g. (section="total", category="Engineering/Product")). C3.5 reads the edge section's HEADER row, where the section name is also the category label ((section="edge", category="Edge")). The shared benchmarks_row_for helper is section-agnostic by design and needs no special-case branch ΓÇö passing (section="edge", category="Edge") works the same way passing (section="total", category="Engineering/Product") does. See tests/board_doc/test_benchmark_by_product.py::test_section_header_row_carries_its_own_section_tag for the parser-side contract that pins this behaviour.
The module docstring and the _TARGET_SECTION drift sentinel test both call this out explicitly so the next sibling (C3.6+) doesn't lose the breadcrumb.
## Why it's needed
C3.5 ships the next per-product benchmark in the Trilogy review scorecard, completing the per-product set alongside C3.3 (Engineering/Product) and C3.4 (SaaS / IT Ops). Unlike its siblings, the Edge metric reads from the (edge, Edge) section-header row rather than a (total, <category>) body row — so the section-row-addressing breadcrumbs and the matching test fixture are the substantive non-clone bits worth reviewing.
## Changes
### C3.5 ΓÇö Edge per-product benchmark check
- klair-api/budget_bot/board_doc/review_checks/edge_benchmark.py (new)
- Reads the (section="edge", category="Edge") row from the Benchmark by Product tab.
- Emits one finding per product column whose actual exceeds 5%.
- Verdict bands: pass Γëñ 5%; warning in (5%, 10%]; critical > 10%.
- Rollup column (col 3 — <BU> Consolidated) → targets SectionType.FINANCIALS; other product columns → SectionType.PRODUCT_DETAIL with product_name (falls through to doc-level when the spec has no dedicated product section).
- All-products-pass emits a single BU-level pass finding so the scorecard reflects the check ran.
- Skip semantics distinguish "tab not loaded" / "Edge section header row missing upstream" / "every product cell blank".
- Narrative tailored to Edge: what names "Edge cost"; why paragraph explains Edge spend (CDN, regional compute, last-mile infrastructure) as variable infrastructure cost that should not exceed 5% of revenue and that exceeding signals an over-distributed footprint or sub-scale per-region revenue; remediation options cover consolidating edge POPs / migrating to a lower-cost CDN tier, repricing to lift revenue, and re-evaluating latency SLAs against per-region revenue.
- klair-api/budget_bot/board_doc/review_checks/__init__.py
- Imports check_edge_benchmark, registers C3.5 next to C3.3 / C3.4 with required_data=(BENCHMARK_BY_PRODUCT,), adds the function to __all__.
### Tests
- klair-api/tests/board_doc/test_edge_benchmark.py (new ΓÇö 20 tests) ΓÇö 1-for-1 mirror of test_engineering_product_benchmark.py / test_saas_it_ops_benchmark.py with the fixture builder parameterising the (edge, Edge) header row instead of a (total, <category>) row, and boundary-case numeric values rescaled to the new (5%, 10%] warning band:
- 5 verdict band + per-product fan-out tests (including the 5pp boundary and just-above-warning critical edge)
- 3 section-id resolution tests (rollup → FINANCIALS, known product → PRODUCT_DETAIL, unknown product → doc-level fallback)
- 4 skip-semantics tests (no tab, missing Edge section header row, all-blank cells, partial-blank cells)
- 1 all-pass aggregate emission test
- 2 supporting-data shape tests
- 1 registry-wiring smoke test
- 2 ragged-row drift WARNING tests
- 2 _TARGET_SECTION / _TARGET_CATEGORY drift sentinel tests (the second one carries an explicit comment about C3.5's deviation from C3.3 / C3.4)
- klair-api/tests/board_doc/test_review_endpoint.py — seeded an Edge row at 4.0% in the populated DataPackage fixture so C3.5 emits a pass alongside C3.3 / C3.4; bumped expected findings count 9 → 10 and added C3.5 to both the happy-path check_ids set, the missing-data skipped_checks set, and the partial-completeness ran_ids set.
## Deviations from the C3.3 pattern
The only structural deviation is the section-row-addressing distinction documented above (_TARGET_SECTION = "edge" rather than "total", and the matching docstring / test breadcrumbs). All other surface ΓÇö module shape, helper imports, supporting-data models, skip-semantics ladder, ragged-row WARNING, drift sentinels ΓÇö is byte-for-byte equivalent to C3.3 / C3.4 except for the documented constant + narrative swaps.
## Breaking changes
None. New check is additive (one more entry in REGISTRY); no model / helper changes; no FE changes (C3.5 findings render through the same FindingCard component as C3.3 / C3.4).
## Test plan
Drone-side checks (the boxes I can verify myself ΓÇö all green):
- [x] cd klair-api && uv run pytest tests/board_doc/test_edge_benchmark.py -q → 20 passed
- [x] cd klair-api && uv run pytest tests/board_doc/test_engineering_product_benchmark.py tests/board_doc/test_saas_it_ops_benchmark.py tests/board_doc/test_benchmark_by_product.py -q → 84 passed (no C3.3 / C3.4 / parser regression)
- [x] cd klair-api && uv run pytest tests/board_doc/test_review_endpoint.py::TestReviewEndpoint::test_skipped_checks_when_data_missing tests/board_doc/test_review_endpoint.py::TestReviewEndpoint::test_partial_completeness_some_run_some_skip tests/board_doc/test_review_endpoint.py::TestReviewEndpoint::test_partial_cached_data_package_is_topped_up -q → 3 passed (the three endpoint tests with assertions touched by this PR)
- [x] cd klair-api && uv run ruff format --check budget_bot/board_doc/review_checks tests/board_doc → no reformat
- [x] cd klair-api && uv run ruff check budget_bot/board_doc/review_checks tests/board_doc → clean
- [x] cd klair-api && uv run pyright budget_bot/board_doc/review_checks/edge_benchmark.py tests/board_doc/test_edge_benchmark.py budget_bot/board_doc/review_checks/__init__.py → 0 errors / 0 warnings
Reviewer-side validation (un-checked ΓÇö please confirm post-merge):
- [ ] Open the Board Doc on a BU whose Benchmark by Product data triggers C3.5 (any BU where the (edge, Edge) row's per-product cell exceeds 5%). Open the Review tab. Confirm the C3.5 finding appears alongside C3.3 / C3.4 with the right severity (warning vs. critical based on the cell value), and the what / why / options text reads sensibly for Edge spend (not a copy-paste of C3.3's engineering rationale or C3.4's SaaS rationale).
- [ ] Address with Claire the C3.5 finding once ΓÇö confirm Claire's regeneration produces a non-trivial change to the affected section (same flow we validated for C3.3 / C3.4). No new behaviour to verify here; C3.5 reuses the same finding shape so the address-with-claire pipeline should "just work".
- [ ] Spot-check the supporting_data JSON in the API response (POST /board_doc/.../review) ΓÇö verify the per-product finding's supporting_data matches the BenchmarkPerProductSupport schema and the BU-level pass finding matches BenchmarkAggregateSupport. The shape is pinned at the model layer; this is "trust but verify on first contact".
Skipped substrate quirk (carried over from the C3.4 PR ΓÇö same gap, same root cause): the populated-data-package endpoint tests (test_returns_findings_for_populated_session and friends in test_review_endpoint.py) hang in the Cursor cloud VM specifically because moto's Redshift stub polls DescribeStatement forever in this sandbox shape. They pass cleanly in production CI via klair-api/conftest.py's real RedshiftHandler mock. The three endpoint tests I touched assertions on (test_skipped_checks_when_data_missing, test_partial_completeness_some_run_some_skip, test_partial_cached_data_package_is_topped_up) all mock the orchestrator and run cleanly here. This is a Klair-side substrate gap, not a regression from C3.5 ΓÇö see the C3.4 PR (#2797) for the broader context and fix path.
## Verification artifact
Sample finding payload C3.5 emits when a non-rollup product (Mobilogy) at 7% trips the warning band against the 5% benchmark:
{"check_id": "C3.5",
"check_area": "Per-Product Benchmarks",
"severity": "warning",
"section_id": "product_detail__mobilogy",
"what": "Mobilogy Edge cost (7.0%) is 2.0pp above the 5.0% Trilogy benchmark.",
"why": "Edge spend (CDN, regional compute, last-mile infrastructure) is a variable infrastructure cost that should not exceed 5.0% of revenue per the Trilogy benchmark; exceeding it signals either an over-distributed footprint or sub-scale per-region revenue.",
"options": [
"Consolidate edge POPs to higher-traffic regions or migrate Mobilogy to a lower-cost CDN tier to bring its Edge spend below the 5.0% benchmark.",
"Reprice or repackage Mobilogy to lift the revenue denominator so the ratio recovers without changing the edge footprint.",
"Defend the gap ΓÇö re-evaluate latency SLAs against revenue per region and document why a 2.0pp overage is acceptable for Mobilogy this quarter (e.g. a deliberate region-expansion cycle that will amortise as new-market revenue ramps)."
],
"preferred_action": null,
"supporting_data": {
"product": "Mobilogy",
"is_rollup": false,
"actual_pct": 7.0,
"benchmark_pct": 5.0,
"gap_pp": 2.0,
"warning_band_pp": 5.0,
"standard_benchmark_pct_in_sheet": 5.0
}
}
(Critical findings on the rollup column carry is_rollup: true, section_id: "financials", and a non-null preferred_action set to the consolidate-POPs option per the C3.3-pattern critical-only nudge.)
Closes KLAIR-2648
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-827ca66c-0772-4c42-95be-e13eb873f2cb"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-web-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-web-light.png"><img alt="Open in Web" width="114" height="28" src="https://cursor.com/assets/images/open-in-web-dark.png"></picture></a> <a href="https://cursor.com/background-agent?bcId=bc-827ca66c-0772-4c42-95be-e13eb873f2cb"><picture><source media="(prefers-color-scheme: dark)" srcset="https://cursor.com/assets/images/open-in-cursor-dark.png"><source media="(prefers-color-scheme: light)" srcset="https://cursor.com/assets/images/open-in-cursor-light.png"><img alt="Open in Cursor" width="131" height="28" src="https://cursor.com/assets/images/open-in-cursor-dark.png"></picture></a> </div>