P0-A cash.py: scope api_update_plan / api_delete_plan / api_complete_plan /
api_register_payment to session entity (cross-entity id -> 404, aligning with
the file's existing get_entity_id endpoints).
P0-B budget.py: write entity_id on BudgetPlan creation + require the KPI to
belong to the session entity; scope /comparison, /comparison/kpi/{id} and
/deviation-report reads to the session entity.
P1 notifications.py: explicit field whitelist (name/config/enabled) on
update_channel to close mass-assignment.
P1 read leaks scoped to session entity: bot_kpis list, data_quality check/stats
(KPI calibre only; quality-log table has no entity_id column), kpis causality
chain + associate-map (KPI and map ownership), dashboard my-kpis,
customer_dashboard list, kpi_governance audit, budget_generate candidates,
driver_budget history (+ entity_id on its write path), expenses stats
(+ entity_id on its write path).
P2 okr_templates.py: write entity_id on the StrategicMap created by apply.
tests: tests/test_cross_entity_isolation_batch4_20260921.py (30 cases;
RED before fix = 22 failed, GREEN after = 30 passed; full suite 1302 passed /
1 xfailed / 0 failed).
deviation_engine: link kpi_value_id on deviation/trend alert rows; skip when period not attributable or kpi_values row missing; dedupe key now (entity_id,kpi_id,kpi_value_id,alert_type)
deviation_push: same-source R1/R5 guards, no-period request writes nothing
tests: add test_alert_caliber_r5_residual_20260919 (13 cases incl. negative controls); adapt r1r4/r5 fixtures to the supplemental ruling
- alert_entity_scope: add is_attributable_period / guard_alert_period (R5) and entity_alert_guard (R1, any write path)
- alert_rules.run_all_alert_checks: skip non-YYYY-MM period alert rows (R5)
- bot_bridge / bot_bridge_v2: R1 demo-entity guard + R5 period guard on write paths
- tests: new R5 suite (10 cases)
Not pushed / not deployed (release gate held by 任总).
R1: exclude status=demo entities from alert traversal (alert_generator / deviation_engine / kpi_bep_deriver / alert_rules / cash engine)
R2: only generate alerts within the last 6 calendar months (incl. current)
R3: only regular month periods (YYYY-MM) participate
R4: decouple generation from push - empty notification_channels never blocks
Single source of truth: app/utils/alert_entity_scope.py
GET /api/cma/alerts/check-timeout picked candidates via the join on KPIDefinition.entity_id, so a session could batch-upgrade (red + processing) alert rows whose own entity_id belongs to another account. Switch the filter to the alert row's own entity_id, drop the now-unneeded join and the unused KPIDefinition import. Write path only; no data change, candidate ids on consistent rows are identical before/after.
Derived from dama-20260919-cma-alert-filter-and-cash-baseline-fix (C1/C2 residual in the write endpoint).
_apply_action_plan: constrain the target KPIDefinition lookup to sug.entity_id so a
suggestion whose target_id/suggestion_data.kpi_id points at a foreign-entity KPI now
returns 404 instead of creating an ActionPlan row with a cross-entity kpi_id.
preview_suggestion: same constraint; a foreign-entity KPI now degrades to the existing
placeholder (KPI#<id>, current_target=None) instead of echoing the foreign KPI name and
current_target. Degradation (not 404) keeps the read-side preview usable when the stored
kpi_id is inconsistent.
Regression: backend/tests/test_ai_suggestion_kpi_attribution_residual_20260918.py
(RED 2 failed before, GREEN 5 passed after; full suite 1228+2F -> 1230 passed / 1 xfailed).
dismiss_suggestion: add get_entity_id dependency and filter AISuggestion.entity_id (cross-entity dismiss returned 200 and mutated the foreign row).
_apply_kpi_target/_apply_budget_adjust: constrain target KPIDefinition lookup to sug.entity_id so suggestion_data pointing at a foreign KPI yields 404 instead of rewriting it.
Regression: backend/tests/test_dismiss_suggestion_entity_scope_20260918.py (RED 3 failed before, GREEN 5 passed after; full suite 1220 -> 1225 passed, 1 xfailed).
-m- ai_suggestions.get_suggestion: filter by session entity_id (foreign 404)
- ai_suggestions.preview_suggestion: use the existing entity_id dependency in the query
- deviation_push.get_map_nodes: add entity_id dependency and (entity_id or 1) ownership guard
- tests: 4 regression cases (3 RED -> GREEN + auth baseline 401)
alert-to-plan, deviation-push review topic and AI-suggestion-to-plan built ActionPlan(...) without entity_id, so non-e1 sessions wrote rows with the model default 1 and entity_id stayed untrustworthy on those paths. Write the session entity at all three sites, scope the deviation-push topic dedupe by entity (a foreign same-title topic no longer suppresses the session entity's own topic), and require the applied suggestion to belong to the session entity. Adds 7 regression tests (both directions + cross-entity negative controls + auth floor).
create_plan wrote no entity_id (fell back to model default 1) while the OKR decomposition view filtered weekly_actions by objective_id only, so cross-entity plans leaked into the other tenant view. Write the session entity on create (before adding the read-side condition - order matters) and scope weekly_actions by entity_id. Adds 4 regression tests (both directions + same-entity positive control).
list_plans enriched kr_title via KR.id only -> legacy rows whose kr_id points at another entity's KR echoed the foreign KR title. Add KR.entity_id filter (read-side sibling of batch3 item 4).
Local commit only; no push (push triggers Gitea deploy hook).
GET /api/cma/alignment/tree queried all active KPIs without entity_id filter,
so switching the three alignment modes showed other tenants' KPIs.
Add get_entity_id dependency and filter KPIDefinition by entity_id.
Three mode builders (_build_vertical_tree/_build_horizontal_tree/_build_bsc_chain) unchanged.
Auth semantics unchanged; cross-entity explicit entity_id still returns 403.
Test: tests/test_alignment_entity_isolation_20260918.py (3 modes x both directions, 403, 401).
my_dashboard picked the row with max calculated_at, so batch backfills made stale periods win (revenue/net-profit showed 2026-07, rebate rate 2026-04). Now prefer the latest monthly period, fall back to calculated_at. Frontend adds data-as-of label and stale tag; not deployed yet (push gated by another pending commit).
LOGIN_ENTITY_STATUSES=("active","demo") applied to login / login-entities / my-entities / switch-entity only; batch pipelines stay active-only so the demo entity never enters daily_push or cash alerts.
- MapNode.vue: hover trash icon per KR row emitting delete-kr (inline SVG, no emoji)
- StrategyLayer.vue / MapCanvas.vue: passthrough + MyDialog second confirm + okrApi.deleteKr + whole-map save
- MapsDialog: single-KR policy hint (keep at least 1 KR per objective)
B1: /api/cma/dashboard/predict hand-rolled threshold parsing called float() on the whole
expression -> ValueError on multi-clause thresholds like '<=36|>=124' -> 500. Now routes
through app.utils.threshold_parser.eval_threshold (single source of truth; red then yellow,
invalid/blank expr -> False, no crash).
B2: ReportCenter.vue restatement tab template called restateDiffClass while the script
defines restatementDiffClass (name mismatch since ec6af75) -> TypeError on render.
Verified: predict 200 on new code vs 500 on live code; /reports A/B browser run shows 0 vs 3
occurrences of the TypeError; pytest 1113 passed / 1 xfailed; vite build OK.
api/index.ts: add permissionApi.getModules/getConfig/saveConfig + alignmentApi.config/tree.
views: replace 13 raw fetch calls and 6 local axios.create instances (each hand-building the Authorization header, without the shared 401 redirect / timeout toast) with the shared instance from api/index.ts.
KPIAlignment: fixes a live bug - the header value was the string literal 'Bearer ' + token, so /alignment/tree always returned 401 (reproduced against the production bundle).
Unwrap semantics preserved: the shared response interceptor returns response.data, so call sites that used res.data were adjusted accordingly.
Verified: scan-frontend-tokenless RAW-TOKENLESS=0; vite build ok; headless A/B (new dist vs production bundle, 8 affected pages, real login) -> identical rendered rows and identical pre-existing error profile, /alignment 401->200.
Push not executed: main is ahead 4 with other tickets' gated commits (see cma-main-ahead3-push-gate-notice-20260915).
No production code changed. Root causes were all test-side staleness:
- test_cash TestVoucherImport: fixture filename contained 'test' -> rejected by the intentional import guard (d71c7fe); renamed, and missing-columns case now asserts the column check is what returns 400
- test_budget_tech_improve TestP1ValueCollect: seeded voucher_details source rows; collector deliberately skips empty aggregates since d71c7fe (no more implicit 0), and preview call now passes an explicit period
- test_roadmap_r2r5 / test_budget / test_ai_suggestions: seeds hardcoded 2026-08 (authoring month) while detectors/rule engine use the current month -> replaced with current-period seeds
pytest tests/ -q -rf: 1094 passed, 1 xfailed, 0 failed (baseline was 10 failed, 1084 passed, 1 xfailed)
3 new tables + idempotent DDL migration (prod guard: refuses DB name cma without approval): tax_filing_log (P_TAX_TIMELY), hr_monthly_ledger (L_EMPLOYEE_TURNOVER), licence_ledger (P_LICENCE_OK).
Entry API /api/cma/ext-ledger: create / CSV batch import / query by entity+period / derive trigger. on_time is server-derived only; HR rows rejected when end != begin + hires - separations.
Deriver writes kpi_values only (source_type=derived, idempotent upsert); denominator 0 returns None and writes nothing; never creates or changes kpi_definitions.
Tests: 22 new pytest cases (22 passed); scratch-DB e2e battery 28 assertions PASS with zero production write.
P-6 (M2 gate 2, FIN-CE-REVIEW-20260915-01): kpis.REVERSE_INDICATORS is now the only authoritative direction definition; dashboard.py imports it instead of carrying a literal copy; bot_kpis.py derives REVERSE_BOT_INDICATORS from it (dead import removed).
F_FACTORY_REBATE_RATE (upstream factory rebate rate, higher=better) was wrongly treated as reverse: 278.7% vs target 80 scored 1/danger, now 5/success and workbench three-color red -> green. No DB write, no scoring-engine logic change, no data_level/frontend touch.
require_auth now uses HTTPBearer(auto_error=False) and raises 401 explicitly
when the Authorization header is missing or non-Bearer, matching
require_auth_401. Unauthenticated requests are 401 for every user endpoint;
authenticated-but-forbidden (cross-entity / role) stays 403.
- app/auth_middleware.py: single optional bearer, 401 on missing token
(require_auth_401 kept as a compatibility alias)
- tests: update 4 pre-existing cases that asserted 403 without a token
(test_auth, test_reports, test_kpi_causality, test_expenses,
test_api_auth_surface_20260914) - intentional behavior change
- add tests/test_auth_401_unification_20260915.py: sample + full sweep
(all AUTHED GET routes -> 401), invalid token -> 401, cross-entity and
role-denied still 403, bot channel (X-BOT-KEY) and public paths unchanged
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).