<!-- CURSOR_AGENT_PR_BODY_BEGIN -->
## Summary
Thin read-only GET /board-doc/addon/conformance that exposes the existing B11
conformance engine (detect_template_gaps → ConformanceReport) to the Google
Docs add-on, mirroring the shape of GET /addon/review. The endpoint resolves
the open doc to its Budget Bot session via _resolve_addon_session, gates
access by Klair BU scope / ownership, and returns a freshly-computed
ConformanceReport so the sidebar's "Check structure" coaching block can
render one Accept-able card per ConformanceGap (add / remove / merge /
retype / reorder).
## Why It's Needed
Budget Bot's B11 structure-conformance engine proactively flags missing
required / per-product sections, non-standard sections, retypes, merges, and
reorders — but it is currently only consumed in-app. The /addon/* surface
covers the content review only, so the add-on cannot surface a "Skyvera is
missing a Cloudsense product section" coaching card. Jun-25 testing made the
gap concrete: Claire had to be explicitly *asked* to add product sections that
the conformance engine would have flagged proactively. This slice closes the
gap by giving the add-on the same ConformanceReport the in-app surface
already consumes.
## Changes
- New read-only endpoint GET /board-doc/addon/conformance in
klair-api/routers/board_doc_router.py, mirroring the addon_review GET
exactly: AddonGoogleDocIdQuery + Depends(get_user_from_google_oidc) +
_resolve_addon_session shell, with no persist and no session mutation.
- New response model AddonConformanceResponse (conformance_report:
ConformanceReport | None) — wraps the engine's existing
ConformanceReport verbatim so the FE renders gap cards directly off the
wire shape. None when session.spec is unset.
- Compute-fresh-with-ARR (load-bearing decision): the route runs
detect_template_gaps(session.spec, arr_breakdown) each call rather than
echoing session.conformance_report. invalidate_conformance rewrites the
stash with arr_breakdown=None after any structural mutation, which drops
per-product ADD gaps (e.g. Cloudsense / Kandy — the headline value of the
coaching block); a fresh compute with the ARR breakdown keeps them in the
response. Cost is one ARR fetch (~1–2s), acceptable because the FE gates
the call behind an explicit "Check structure" button click, not
auto-on-load. Pinned by
test_addon_conformance_does_not_echo_stashed_report.
- Best-effort ARR fetch: a tight try/except around *just*
_fetch_arr_breakdown_for_conformance (the orchestrator helper the
refresh-stash path already uses), with an inline comment explaining why
this is the one non-propagating step. Failure logs with context and falls
back to arr_breakdown=None, so the user still gets ADD-required-section /
REMOVE / RETYPE / REORDER coaching when ARR is offline. All other failures
(session resolve 404 / 403, detect_template_gaps bugs) propagate to the
FastAPI handlers per CLAUDE.md.
## Breaking Changes
None — additive read-only endpoint and response model. The conformance engine
(detect_template_gaps / compute_conformance / invalidate_conformance)
and the existing /addon/* routes are unchanged.
## Test Plan
uv run pytest tests/board_doc/test_addon_conformance.py -v
10 tests, all green:
- test_addon_conformance_returns_product_add_gap_when_arr_present —
headline ADD-gap path; BU spec with no product sections + Cloudsense /
Kandy ARR breakdown → response carries ADD PRODUCT_DETAIL gaps with the
expected product_name + arr_current fields.
- test_addon_conformance_arr_failure_returns_structural_only_report — ARR
fetch raises → 200 with a structural-only report; no product ADD gaps,
no surfaced exception.
- test_addon_conformance_spec_none_returns_none_report — spec is None
short-circuits to conformance_report=None and never touches the ARR
fetch.
- test_addon_conformance_is_read_only_no_save — storage.save raises if
called; endpoint returns 200, confirming the GET never persists.
- test_addon_conformance_does_not_echo_stashed_report — seeds a
structural-only stash with no product ADD gaps, then asserts the fresh
response carries the Cloudsense ADD gap (pins the compute-fresh-with-ARR
decision).
- test_addon_conformance_404_when_no_session — _resolve_addon_session
parity.
- test_addon_conformance_outsider_403, …_owner_200,
…_bu_scoped_user_200, …_superuser_200 — access-control parity with
the rest of the /addon/* family.
Existing add-on tests stay green (tests/board_doc/test_addon_*.py, 110
tests passing); the broader conformance suite stays green
(tests/board_doc/ -k conformance, 73 tests passing).
## Verification Artifact
$ uv run pytest tests/board_doc/test_addon_conformance.py -v============================= test session starts ==============================
collected 10 items
tests/board_doc/test_addon_conformance.py::test_addon_conformance_returns_product_add_gap_when_arr_present PASSED [ 10%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_arr_failure_returns_structural_only_report PASSED [ 20%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_spec_none_returns_none_report PASSED [ 30%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_is_read_only_no_save PASSED [ 40%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_does_not_echo_stashed_report PASSED [ 50%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_404_when_no_session PASSED [ 60%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_outsider_403 PASSED [ 70%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_owner_200 PASSED [ 80%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_bu_scoped_user_200 PASSED [ 90%]
tests/board_doc/test_addon_conformance.py::test_addon_conformance_superuser_200 PASSED [100%]
======================== 10 passed, 1 warning in 1.28s =========================
Sample serialized response payload from the headline test — a freshly-computed
ADD gap carrying the load-bearing product_name + arr_current fields the FE
binds to:
{"gap_kind": "add",
"confidence": "high",
"section_type": "product_detail",
"target_section_id": null,
"merge_into_section_id": null,
"product_name": "Cloudsense",
"arr_current": 12000000.0,
"reason": "Skyvera has no dedicated section for Cloudsense (small BU (2 products ≤ 5))",
"suggested_action": "Add a product section for Cloudsense («Cloudsense»)"
}
<!-- CURSOR_AGENT_PR_BODY_END -->
<div><a href="https://cursor.com/agents/bc-5b6ebd5f-7a48-4cb3-b33a-b904db859030"><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-5b6ebd5f-7a48-4cb3-b33a-b904db859030"><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>