Task: CMA-20261003 (budget module governance). Local commit only; push is
gated on release approval (see report).
Backend
- app/utils/budget_status.py (new): single source for the effective-version
caliber. EFFECTIVE_STATUSES = approved + active; effective_plan_clause()
enforces entity isolation; pick_effective() priority active > approved >
updated_at desc. Fixes: approved budgets disappeared from the execution
surface because the three queries only read status='active'.
- budget.py: /comparison, /comparison/kpi/{id}, /deviation-check now read the
effective surface; versions list gains is_effective / can_release fields;
submit/approve are fail-closed (draft|active -> submitted -> approved).
- deviation_engine.get_budget_for_kpi: effective surface + deterministic pick.
- 4 missing rings, zero DDL, appended to the same router:
POST /versions/release (approved -> active, idempotent by operation_logs),
POST /adjustments (derive a new draft version, base rows untouched),
GET /control/status (effective budget vs actual -> ok/warn/over),
GET /assessment (achieve rate + A/B/C/D grade).
Audit rows written through app.utils.oplog.log_op with entity_id.
Frontend
- BudgetManagement.vue: top-level tabs reordered to the process order
(strategy, method, driver, input, decompose, versions, release, execution,
deviation, adjust, assess, rolling). Pane name values unchanged, so old
bookmarks keep working; routes unchanged.
- Cash classify rules / unclassified queue moved to /cash-plan
(components/CashClassifyPanel.vue); budget actual-value collection moved to
/data-manage (components/BudgetCollectPanel.vue). Mount points only: no
change to cash_plans data, classify logic, collection logic or kpi_values.
- api/index.ts: 4 ring endpoints exposed.
Docs / tests
- docs/budget-module-governance-design.md: phase-0 design doc (status machine,
tab order, domain boundaries, 4-ring design, cleanup write pack scope).
- tests/test_budget_status_machine.py (new): effective surface, version
guards, release idempotency, adjustment, control and assessment.
- tests/test_budget.py: existing cases adapted to the fail-closed contract.
(cherry picked from commit 4267fc4d25ee45c5675a541af9777be96fe9f9c2)
Task: finance-20261011-strategy-map-frontend-fullstack (P2, read-only integration)
- backend/app/utils/strategy_board.py: single source of truth for the level
caliber (layer colors, inverse-kpi explicit whitelist, status class by
unit, zero guard, period ranking, latest-actual picking). Pure functions.
- backend/app/api/maps.py: GET /api/cma/maps/{map_id}/strategy-board
- nodes straight from map_objectives (sorted by sort_order, id)
- node<->kpi binding with 4-step fallback, binding_source echoed back
- targets from kpi_targets (target_type compatible: yearly + annual),
deduped per (kpi_id, period) with yearly preferred
- actuals from kpi_values actual-only scope, latest period
- causality edges from kpi_causality, out-of-map endpoints dropped+counted
- entity_id taken from the token only (no cross-tenant reads)
- zero writes; nothing is persisted by this endpoint
- frontend/src/views/StrategyMap.vue: /strategy-map live board (4 swim lanes
bottom-up, node cards, 20 causality arrows in SVG, period switch, A3 print).
No emoji, null guards everywhere (no toFixed on null).
- api/index.ts + router/index.ts + permission.ts: route + menu registration
(/strategy-map, zone A read-only, roles ceo/finance/business/it).
- backend/tests/test_strategy_board_20261011.py: 33 cases (spec section 4 /
4.1 / 4.2 / 4.3 + section 7 fixtures + threshold boundaries + period rank).
- backend/tests/test_map_objectives_kpis_form_20261011.py: 14 cases for the
four kpis column shapes (list, json string, object list, NULL/[]/weird).
Not included: push + backend restart (production write, separate gate).
(cherry picked from commit c675bde6b4beaf1d8a9d02a80f5434203a53f348)
Customer revenue/concentration now filters bill_type IN (1,4,14) (XSD/XSTHD/XSHHD).
Non-sales docs (JHD purchase, YFKD prepay, YSKD accept, QTRK, SKD, YBFYD) carry SUPPLIER unit_id and were polluting 'customer concentration' (old Top1/Top2 were our biggest suppliers; root cause of 6.04x base gap).
Unify sign filter to (sum_money+sum_tax_money)>0 (equivalent to money_only on current data, diff 0 rows) and correct the withdrawn 407-row/17.2% wording in cpa_caliber docstring.
Deriver: anchors from cpa_caliber single source; new --concentration overwrite and --only concentration (minimal 9-row write face) with pre-overwrite backup.
Tests: fixture gains bill_type/sum_tax_money; +5 whitelist cases (44 passed targeted).
Task: finance-20261003-cpa-customer-profitability-mvp-fullstack (P0-a / P0-b)
- backend/app/utils/cpa_caliber.py (new): single source of truth for
customer-level revenue aggregation from zxt_masterbill. Reused by
alert_rules._customer_revenue_shares (dedupe: no second implementation).
- backend/scripts/migrate_cpa_snapshot_20261004.py (new): idempotent DDL for
customer_profitability_snapshot (UNIQUE entity_id+period+unit_id),
--check / --apply / --rollback / --status / --show-create + journal.
- backend/scripts/kpi_customer_profitability_deriver.py (new): monthly
snapshot + kpi_values upsert (single transaction, idempotent).
fail-closed: cogs_alloc / chan_sub / contrib / contrib_rate stay NULL and
are never written as 0 or as an apportioned value.
- backend/scripts/sql/cpa_kpi_defs_20261004.sql (new): C_CUST_REVENUE and
C_CUST_COUNT active; 5 P1 codes registered inactive with fail-closed reason.
- backend/app/api/customer_dashboard.py: new read-only endpoint
GET /api/cma/customer-dashboard/standard-indicators returning the 9-card
tri-state (value / no_data / undefined). Reads kpi_definitions via raw SQL
to stay decoupled from the in-flight KPI direction DDL.
- frontend/src/views/CustomerDashboard.vue: 9-card panel rendering the three
states (never renders an empty value as 0).
Anchor check 2026-01..09 (offline sqlite rehearsal, ALL_PASS=true):
73 customers / 4883.52 wan / top1 25.3835 / top2 23.4905 / top4 63.3249 /
top10 81.7675 - all six match the task acceptance anchors.
No production write and no deploy in this commit; release is gated.
Per finance ruling 2026-10-05 (project-20261005-dupont-oob-window-fix):
- R1: no intersection with available month domain => explicit error (endpoint 400), no window fields
- R2: with intersection => intersection window, months>=1 (kills inversion and months<=0)
- R3: quarter/half/... suffix kept only when returned window == nominal window (clipped flag)
- R4: symmetric lower-bound clip note; M2: TTM nominal window = [anchor-11, anchor] (12 months)
- M1: docstring correction (lower-bound overflow is clipped, not 400)
No push / no deploy / no restart / no DB write.
Root cause: resolve_window() matched quarter/half with p[4] == "q" / "h"
(always '-' for "2026-q2"/"2026-h1"), and the branches sat after the
single-month branch -> dead code. Real input fell into the month branch,
so months_between() raised ValueError on int('q2') -> HTTP 500 for all
18 quarter/half options in the period selector.
Fix: judge on p[5] in ("q","h") with a digit guard on p[6], evaluated
before the single-month branch; month branch tightened to p[5:].isdigit().
Existing error-branch wording unchanged.
Tests: backend/tests/test_dupont_period_options.py (18 defect options,
case equivalence, month/year/ytd/ttm/custom non-regression, malformed
input returns error dict instead of raising).
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).
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)
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
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).