The voucher-import linkage branch picked write targets by kpi_name LIKE '%cash%' and wrote the monetary-fund ledger balance (yuan) into derived KPIs (F_OP_CFLOW / F_FCF / F_BEP_REVENUE_CASH / F_BEP_COVER_CASH). Replace with an explicit kpi_code whitelist plus a unit/caliber gate that refuses writes on mismatch and reports them via kpi_sync_skipped.
Read-only equivalence probe (real function + intercepted writes + rollback) before/after: before wrote 4 derived KPIs, after writes only F_CASH_SAFETY. tests: +9 new cases; pytest failure set identical to baseline (15/15 same ids).
Single source: app/utils/voucher_guard.py (NOT EXISTS on voucher_void_marks,
key = entity_id+voucher_no+voucher_date and revoked_at IS NULL; same caliber as
the DB view cma.v_voucher_details_active). No hardcoded voucher numbers.
Consumers now guarded: cash.py import linkage (2 fns), reports.py subject/BS
amount (2 sites, also entity_id isolation added), dashboard.py _bep_sum_field,
alert_attribution.py build_subject_attribution. kpi_value_collector.py now
delegates to the shared guard (removes the previous duplicate implementation).
Tests: new tests/test_void_marker_consumers_20260914.py (6 cases).
R2 rule-gate auto-promotion + R3 exception interception in the same collect transaction; merge duplicate rows, keep single verified main per (kpi_id, period); reuse kpi_bep_deriver._is_period_closed; no schema change (data_status/remark only).
Task: finance-20260914-autocollect-reviewflow-impl
Root cause: main.py include_router had no global auth dependency; user routers
(products/cost/predict/...) relied on assert_entity_scope, which treats a
missing session token as the Bot service channel and lets the request through.
Result: 46 user endpoints returned data without any token on a public site.
Changes:
- auth_middleware.py: add require_auth_401 (missing Bearer -> 401, delegates to
require_auth otherwise); 401/403 semantics are explicit.
- router-level Depends(require_auth_401) on products, cost, predict,
notifications, permissions, tenant, alignment; endpoint-level on
/auth/roles and /admin/cache/clear.
- deps.py: document that get_entity_id/assert_entity_scope are NOT auth.
- frontend: add the missing Bearer header to 5 tokenless fetch/axios calls that
hit the newly protected endpoints (KPIAlignment x2, RolePermissions x2,
Dashboard x1).
- tests: new test_api_auth_surface_20260914.py (58 cases: no-token 401,
token 200, cross-entity 403, bot channel unchanged); adapt test_predict /
test_kpi_forecast to send session tokens like production.
- scripts/audit_api_auth_surface.py: reusable FastAPI dependency-graph audit
(423 endpoints; A bot / B gap / C public), exits 1 when a B-class gap exists.
- data_quality.py: remove GET /check-governance (no caller repo-wide; its
entity_id param had no session auth -> cross-tenant read; duplicate of
governance-check) -> the path now returns 404
- reports.py: /reports/dupont resolves the analysis target from the session
account (assert_entity_slug_scope / resolve_session_slug); explicit
cross-entity entity param -> 403, illegal value -> 400; the 2026-08-31
cross-entity exemption is revoked (docstring updated)
- frontend: drop the checkGovernance api definition; DupontAnalysis.vue
entity radio group -> read-only session entity tag, request without param
- tests: TestCheckGovernanceRetired (404 x2), TestDupontSessionScope (3),
TestDupont reworked to account-bound tokens (8 tests)
- docs/api-verification-split.md: drop the retired row, note 334/404 -> 333/403
pytest: 10 failed / 957 passed / 1 xfailed vs HEAD baseline 10 failed /
951 passed / 1 xfailed (identical failure set -> zero regression)
(cma-residual-cross-entity-cleanup-20260914)
- DataQuality / ProductMatrix / AlertList / ActionPlanLibrary /
views.GrowthQuality / PredictAccuracy: drop entity el-select
(including the "all entities" option), show read-only session
entity tag (not clickable), same style as the top bar tag
- drop page-level entity param from those requests; backend resolves
entity from the session token (assert_entity_scope / get_entity_id)
- utils/entity.ts: add sessionEntityName() (cma_entity_name else id map)
- P3: ReportCenter header emoji removed; components/GrowthQuality
detail drill-down el-dialog -> MyDialog
(cma-entity-selectors-converge-6pages-20260914)
- MainLayout/ReportCenter: drop switch-entity switcher, keep read-only entity tag
- KPIList/PredictDashboard/GrowthQuality: entity selectors locked to session entity
- api/index.ts: drop switchEntity/myEntities wrappers (endpoint kept for compat)
- sidebar brand now shows logged-in entity name
(cma-remove-inapp-entity-switcher-20260914)
Defect: _pick_target_period treated "month sales < 30% of 6-month avg" as
"period not closed", so 2026-08 (actually closed: 3103 carry-forward vouchers
present) was skipped because of a low-sales off-season month, pinning the five
F_BEP* KPIs at 2026-07.
Fix: add _is_period_closed() - primary signal voucher_details
(carry_forward=1 AND subject_code='3103'), secondary signal
ZXT_LANDING_DIR/{YYYY.MM}/check.md containing both "issue PASS" and
"P&L carry-forward PASS". _pick_target_period now scans candidate months desc
and returns the first closed one, with no sales filtering; the old sales
threshold path is kept only as a fallback (warns "sales-threshold fallback").
Scope: run_derive() formulas, collector, kpi_definitions and cron untouched.
Verified: period=2026-08; 167.5 / 60.4 / 39.6 / 118.7 / 85.2;
F_BEP_COVER 2026-08 red pending, 2026-07 pending resolved; idempotent on re-run.