Non-ending stock windows (e.g. ?period=2026-Q2 / 2026-05 / custom) return raw total_assets/equity as None when the window has no subject_balances level-1 rows; identity guard reports status 'unavailable' (not 'identity_broken') and execution continues into the insight detail f-string, where .2f on None raised TypeError -> HTTP 500. Add explicit fmt() ('-' for None) for the four raw columns in detail.
Task: fullstack-20261004-1544 (P1; proposer wecom-fullstack; ASK-20261004-07 follow-up). No DDL. Single file.
Kaliber: wecom-finance 2026-10-02 ruling (Boss 11:59 CST "li; but (1) add real ROI").
- kpi_definitions seed scripts/sql/profitability_kpi_defs_20261003.sql:
F_ROI renamed to F_ROA (same kpi_id=20, all 8 historical values kept), new F_ROI
(real IMA caliber = operating profit / invested capital), F_EVA activated with unit
and thresholds aligned to 万元, new F_RI (residual income).
- parameters are NOT hardcoded in formula text: they live in
kpi_definitions.data_source_config.params (tax_rate / wacc / min_return_rate /
include_factory_subsidy / include_channel_subsidy / include_sales_discount /
annualize / capital_basis); single reader = app/utils/profitability_params.py.
- deriver scripts/kpi_profitability_deriver.py: YTD operating profit from
voucher_details (carry_forward=0 net), invested capital from subject_balances
level-1 rows only, idempotent upsert into kpi_values (source_type=derived, no
data_status downgrade, fail-closed when invested capital is 0).
- read-only endpoint GET /reports/value-management reads the derived rows only
(single source of truth, no recomputation) and exposes the parameter block.
- DupontAnalysis.vue: value management section (ROI / ROA / ROE / EVA / RI).
- caliber_units.py: RATIO_CALIBER_TABLE key F_ROI renamed to F_ROA, new F_ROI row.
- tests: backend/tests/test_profitability_indicators.py (21 cases).
ASK-20261002-06 fix04u: admin(5602%) and depreciation(560213) take net amount in both the dashboard card helper and the BEP deriver; sales (5001) keeps credit_amount. Verbatim per proposal section 2. No DB writes, no kpi_values backfill.
- add missing 'or_' import (NameError on map_id calls)
- union of map_objectives.kpis codes and KPIDefinition.map_id, intersect with entity active KPIs
- cross-entity / unknown map_id -> empty tree (no leak)
- no map_id -> unchanged behaviour
GET /api/cma/alignment/tree ignored the map_id query param (FastAPI silently dropped it) so the board always rendered the full active-KPI set of the account. Add map_id: Optional[int] = Query(None) and filter KPIDefinition.map_id when provided; no param keeps the previous behaviour. Entity scoping unchanged (entity_id from token) - cross-account map_id yields an empty tree, no other-account KPI is returned. Only the KPI selection set is filtered; vertical/horizontal/bsc_chain builders untouched.
Same-class gap as e369508 (alerts list): single-key ORDER BY created_at DESC plus offset/limit lets MySQL filesort return an unstable order inside same-second tie groups, so offset pagination duplicates and skips rows. Append secondary key id DESC, aligning with app/api/alerts.py, app/api/cash.py and app/api/data_quality.py.
Entity-scope caliber untouched: parent-key join attribution, alert_id IS NULL rows still excluded, policy/note wording unchanged. Local commit only - no push (release gated by ASK-20260925-04).
list_alerts ordered by created_at DESC alone; kpi_alerts has many rows sharing the same second timestamp (07:00 batches), so MySQL tie order is unstable and offset pagination both duplicated and skipped rows (entity 1 read-only probe: total=281, 281 rows walked, only 268 unique ids, 13 duplicates; id 587 unreachable in the unfiltered list while visible in ?status=pending).
Add secondary key id DESC so pages form a total order; add regression suite test_alerts_pagination_stability_20260925.py (tie-heavy fixture: unique==total, zero duplicates, strict id DESC inside a tie group, page concat == single-shot ordered query, unfiltered vs status-filtered visibility, entity isolation negative control).
New router app/api/doc_center.py: GET /api/cma/docs (list, ?with_content) and GET /api/cma/docs/{slug}; runtime disk read, role-gated (ceo/finance/business/it), no existing API semantics changed.
Content dir /srv/hermes/shared/cma-content (docs/*.md + categories.json). Placed outside the repo on purpose: the deploy tree equals the committed tree (deploy.sh checkout -f + clean -fdx), so in-repo content would be reset on every deploy and content edits would fail the deploy-tree dirty check.
CMAKnowledge.vue: 20 hardcoded items migrated to md, page now fetches at runtime and renders md with the existing marked dep (no new deps); table borders restored via :deep() since scoped styles never reach v-html content.
Tests: backend/tests/test_doc_center_20260924.py (20 cases). Full suite 1448 passed (baseline 1428 + 20).
task dama-entity-isolation-sweep-20260923 (sections 2-2 4/5, merged by projectBot 2026-09-23 18:2x)
4) operation_logs write side: every audit write carries the account (no DDL, no column added)
- new app/utils/oplog.py::log_op() single wrapper: sets detail.entity_id; scope=global for
by-design non-entity writes (platform dict / role-level); scope=batch + OPLOG_BATCH_NO_ENTITY
sentinel for cross-account batches; no context -> OPLOG_NO_ENTITY sentinel (never guess)
- 16 remaining construction points converted (data/alert_rules/bi_reports/kpi_causality/
bot_bridge x2/budget_generate/driver_budget/kpis _log x3/templates _log x2/erp_sync);
pre-4 state had 20 sites without entity_id, now 0 outside the wrapper
- read side untouched (knowledge_service still user_id fallback); history not backfilled
5) docs/entity-dimension-data-dictionary.md: 8-table three-state registry
(kpi_templates/driver_factor_templates/scenario_suggestions/bot_bridge_config = non-entity;
kpi_subject_map = inherited via kpi_id; analysis_results/subjects = account dimension,
legacy rows still NULL -> other task; operation_logs = non-entity column + detail.entity_id)
Evidence: RED 2 failed/7 passed (reverted call sites) -> GREEN full 1428 passed; 9 new tests
including two read-back rows; app md5 restore verified identical; no cross-account writes
LOCAL commit only - push is gated (ahead contains the P1 entity-dim batch commit fb4609b)
backend: GET /api/cma/data-quality/rule-detail (COUNT(*) total, structured items, server-side paging, scope=manual); governance seven-rule counts use real totals instead of len(LIMIT-ed rows). frontend: clickable anomaly count -> MyDialog detail (no new el-dialog) + full CSV export.
Frontend sends card key status=overdue to GET /api/cma/action-plans, but the
list endpoint compared ActionPlan.status == 'overdue' which is not in the
status domain -> always 0 rows while /stats showed 7 (derived caliber).
Extract _apply_overdue() and use it in both plan_stats and list_plans.
Refs: project-20260921-actionplan-overdue-filter
F1 resubmit_reimbursement now requires session entity_id (cross-entity -> 404).
F2 validate_reimbursement monthly/yearly cumulative sum is entity-scoped,
so other entities' in-flight amounts no longer pollute the limit verdict.
Tests: backend/tests/test_cross_entity_isolation_expenses_residual_20260921.py
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).