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.
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)
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
_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).
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.
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).
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)