Commit Graph
84 Commits
Author SHA1 Message Date
fullstackandHermes CI Fix bd4f58aae1 feat(action-plans): COSO 自检未通过条目一键生成改善行动(接通死按钮)
ci/woodpecker/push/main Pipeline failed
- 后端新增 POST /api/cma/action-plans/coso-generate-actions:未通过条目→action_plans,
  账套取 token 绑定(忽略 body entity_id)、幂等键 (entity_id,title)、不写 auto_verify_rule
- 前端 ActionPlanLibrary.vue 按钮接 @click(含未通过条数提示 + 二次确认 + 生成后刷新并跳列表)
- api 客户端补 cosoGenerateActions
- 新增 tests/test_coso_generate_actions_20260921.py(生成/幂等/隔离/未认证/归属校验/空集)
2026-09-23 01:03:19 +08:00
Hermes CI Fix 2c2dbc2b2d fix(cma): 行动计划自动生成补账套 — 第 4 处写入点 + 写前同源校验
ci/woodpecker/push/main Pipeline failed
任务:0-P1-project-20260922-actionplan-entity-write-fullstack
父单:project-20260921-cma-entity-isolation-batch5(§B 治本 0c2e57a)
同族已修(勿重做):alerts.py / deviation_push.py / ai_suggestions.py(d03e322)

缺陷(唯一活写入点):
  scripts/alert_generator.py::run_alert_check 为 red/yellow 预警自动建 ActionPlan 时
  不传 entity_id(模型 default=1 + 生产 DDL `DEFAULT 1` 双默认静默兜底)⇒ 非 e1 账套的
  KPI 触发阈值时「同一次事务里预警归属正确、计划归属落 1」;读侧 dashboard.py 工作台按
  ActionPlan.entity_id 取出计划、KPI 名不做账套过滤渲染 ⇒ 酣客(e1) 工作台显示博海(e2)/
  演示(e6) 的 KPI 名(实测 19/83 行归属不符,09-17/18/22 07:00 各新增 6/7/6 行,每日复发)。

改动:
  1. 写入点补账套:ActionPlan(entity_id=kpi.entity_id, …) —— 归属唯一事实源 = 来源 KPI 账套;
  2. 写前同源校验 action_plan_entity_guard():来源账套(kpi.entity_id) 与目标账套
     (alert.entity_id,即本次遍历账套) 不一致或任一为空 → 不建计划 + OperationLog
     (action="skip_plan", reason=plan_entity_attribution_mismatch) 留痕 + 计入统计输出,
     不 raise、不中断流水线(口径照抄 bot_bridge_v2 R1 账套守卫 / B3(b) skip_alert 留痕姿势);
  3. 统计/汇总:stats 增 skipped_plan_entity_mismatch / skipped_plan_kpis,并在
     generate_and_push 返回值与 coverage_note 中显式输出(不得静默丢弃);
  4. 新增回归 tests/test_alert_generator_entity_attribution_20260922.py(T1/T2/T3 改前 RED、
     改后 GREEN;T4 正对照改前即绿,不构成本单判据)。

边界(零越界):零 DDL(未动列默认、无 alembic/migration/SQL 脚本)、未改 models、
未动同族三处已修文件、未写生产库、未调写端点、未改 cron/前端、未 push、未部署、未重启。

回归:backend `pytest -q` 1374 passed / 0 failed(基线 1370 + 新增 4)
2026-09-22 11:35:01 +08:00
fullstack 30fa745135 fix(cma): 工作台跨实体治本 · DAMA 回查收口 — 审计可归属 + 通知日志口径
DAMA 独立回查 dama-recheck-workbench-entity-isolation-20260922 的 §4b/§5 两处待办:

1. POST /api/cma/tenant/switch 审计行原来不可归属:
   - user_id=None → 改为注入会话用户(user=Depends(require_auth_401))
   - target_type/target_id 从 system_config/None 对齐为 entity/<entity_id>,
     与 /auth/switch-entity 的 switch_entity 共用目标维度 ⇒ 按账套可过滤,零 DDL
   - action 字面 switch_tenant_state 保留不合并(与会话级 switch_entity 语义不同),
     口径登记 docs/audit-and-notification-caliber-20260922.md

2. /api/cma/notifications/logs 增加 policy/note 口径字段,前端发送记录表
   empty-text 提示同一口径 —— 避免「B2 上线后 e1/e2 均 0 条」被当成页面故障
   (现库 8 行历史记录全为 alert_id IS NULL 的 OKR 复盘提醒,无账套归属)

回归:tests/test_workbench_entity_isolation_20260922.py 22/22;全量 1370 passed;前端 vite build 通过
2026-09-22 11:21:24 +08:00
wecom-fullstack 0c2e57a4cf fix(cma): 工作台跨实体污染治本 — 全局键拆账套 + 缺列收口 + 预警归属兜底
任务 dama-workbench-entity-isolation-fullstack-20260922(DAMA 诊断 / 项目Bot batch5 §B / 任总 2026-09-22「都批」)

B1 现金基线拆账套(零 DDL)
- cash_forecast_engine: 键 cash.current_balance → cash.current_balance.<entity_id>;
  读只读本账套键、不做旧键回退;set_* 签名 entity_id 必传(漏传 TypeError)
- cash.py: POST /balance 挂 get_entity_id;网银流水联动写回带账套
B3(a) alignment_config 拆账套(零 DDL)
- alignment.py: GET/POST /config、GET /tree 只读 alignment_config.<entity_id>;未配置=configured:false
B3(b) 预警 KPI 归属兜底
- bot_bridge_v2: 去掉 kpi_id or 1 兜底;本账套无 KPI 定义 → 跳过建预警 + OperationLog 留痕
  (kpi_alerts.kpi_id NOT NULL ⇒ 方案A 置 NULL 不可行,走方案B 跳过+审计)
B2 缺列五表分表处置
- notification_logs: 经 alert_id → kpi_alerts.entity_id join 归属(无父键行不返回)
- notification_channels / okr_templates: 集团共享口径写入 docstring(有意不过滤)
- system_configs: 由 B1/B3(a) 拆键覆盖;kpi_data_quality_logs 沿用 batch5 的 join 收口
P0 语义收敛
- tenant.py: current_tenant 收敛为项目Bot A2A 状态记录,GET 仅 it/ceo;POST 补 operation_logs 审计
证据:新增/改写用例 22 条;全量 1370 passed(基线 1348)
2026-09-22 11:06:02 +08:00
Hermes CI Fix f88ed94c15 fix(action-plans): derive overdue filter in list endpoint (single source with stats)
ci/woodpecker/push/main Pipeline failed
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
2026-09-21 15:13:27 +08:00
Hermes CI Fix b4f94a12b3 fix(cma): entity scope for expenses resubmit + cumulative limit check
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
2026-09-21 14:56:43 +08:00
Hermes CI Fix 42ff836309 fix(cma): entity isolation batch5 (budget writes, kpi objectives scope, quality logs join, growth-quality db source, expenses scope)
ci/woodpecker/push/main Pipeline failed
2026-09-21 10:36:02 +08:00
Hermes CI Fix a0762b367f fix(cma): entity isolation batch4 (cash plan writes, budget ownership, read leaks, okr template)
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).
2026-09-21 09:37:38 +08:00
Hermes CI Fix 8c5853f64b fix(alerts): enforce caliber ruling R5-supp (period linkage)
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
2026-09-19 11:12:31 +08:00
Hermes CI Fix 5a99ed87e0 fix(alerts): enforce caliber ruling R5
- 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 任总).
2026-09-19 10:42:09 +08:00
Hermes CI Fix 3beec14d96 fix(alerts): enforce caliber rulings R1-R4 for alert generation
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
2026-09-19 10:05:11 +08:00
Hermes CI Fix 557771a803 fix(cma): scope ai-suggestion action_plan and preview KPI to session entity
_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).
2026-09-18 21:04:30 +08:00
Hermes CI Fix 9cb960da25 fix(cma): scope ai-suggestion dismiss and target KPI to session entity
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).
2026-09-18 20:33:39 +08:00
Hermes CI Fix d2984857a2 fix(cma): scope read-side ai-suggestion detail/preview and map nodes
-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)
2026-09-18 20:23:53 +08:00
Hermes CI Fix d03e322a81 fix(cma): action plan entity_id at the three residual construction sites
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).
2026-09-18 20:12:46 +08:00
Hermes CI Fix b072602191 fix(cma): action plan entity_id on create + okr decomposition entity scope
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).
2026-09-18 20:03:55 +08:00
Hermes CI Fix bab6137aa2 fix(cma): scope action-plan objective_id writes to session entity
create_plan/update_plan now validate Objective.entity_id ownership (404 on cross-entity, no partial row) and update_plan gets an explicit objective_id-vs-KR-derived priority rule. Adds 10 regression cases (batch3 bias: bidirectional + same-entity control + OKR decomposition read-side consequence).
2026-09-18 19:34:59 +08:00
Hermes CI Fix 50baa16f09 fix(cma): scope action-plan KR enrichment to session entity (batch3 follow-up)
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).
2026-09-18 19:03:49 +08:00
Hermes CI Fix 9fb9d14a58 fix(cma): entity isolation batch3 (cost abc write endpoints + allocations/stats reads + plan kr_id scope)
- cost.py: POST /abc/activities 按会话账套写入 entity_id;POST /abc/allocate 校验 activity_id 归属本账套(跨账套 404)并透传 entity_id;GET /abc/allocations 加账套过滤
- cost_engine.py: allocate_cost(entity_id=None) 按账套过滤作业中心 + 显式写入 AbcAllocation.entity_id(不传维持原语义)
- action_plans.py: update_plan 的 kr_id 加账套校验(跨账套 404,对齐 create_plan 既有写法);plan_stats join KPIDefinition 按账套过滤(口径与 list_plans 一致)
- tests: 新增 tests/test_cross_entity_isolation_batch3_20260918.py(10 例;改前 7 failed/3 passed → 改后 10 passed)
- 回归: 1184 passed,1 xfailed → 1194 passed,1 xfailed(无新增失败)
2026-09-18 18:42:38 +08:00
Hermes CI Fix ad953ac759 fix(cma): entity isolation batch2 (roll-forward/check-timeout/by-id/cost-abc)
ci/woodpecker/push/main Pipeline failed
Scope: backend/app/api/{alerts,action_plans,budget,cost}.py + tests

Task: 0-P1-project-20260918-entity-isolation-batch2
2026-09-18 18:15:22 +08:00
Hermes CI Fix b9c80f836e test(cma): force production Redis off during tests (P1 token leak isolation)
ci/woodpecker/push/main Pipeline failed
2026-09-18 17:33:25 +08:00
Hermes CI Fix a2fedd892a fix(cma): isolate KPI alignment tree by entity (project-20260918-alignment-entity-leak)
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).
2026-09-18 17:18:52 +08:00
Hermes CI Fix 3bb55189b1 feat(cma): 口径裁定 2026-09-17 落地(现金流投资=0 二维判定 / 预警单位+期间口径)
ci/woodpecker/push/main Pipeline failed
依据:wecom-finance/reports/CMA_口径裁定_20260917.md(任总 2026-09-17 23:14「按建议执行」)

裁定1(现金流「投资活动净额=0」参与判定)
- classify_enterprise():0 为合法输入;投资恒为 0 → 经营(±)×筹资(±) 二维映射出 2 个
  候选类型(candidate_type_nos/candidates),不再整体判「未判定」;投资栏 investing_note
  标注「本期无投资活动」;经营/筹资亦为 0 → 回退「未判定(无投资活动)」,不报错不静默
- /analysis 增 classification_dimension / investing_note / nets.investing.note;前端出
  二维横幅 + 候选行高亮 + 投资栏「本期无投资活动」(不给单一类型名)
- import_cashflow_statements.py:打印层修 2D(type_no=None)TypeError 崩溃;NA 措辞改为
  「按 0 参与判定」(机读短语 NA_PHRASE_TMPL/parse_na_activities 未动)

裁定2a(比率类单位统一「百分比(%)」口径,先做)
- 新增 app/utils/caliber_units.py:逐 KPI 对照表 RATIO_CALIBER_TABLE(唯一事实源)+ 判定侧
  唯一入口 resolve_caliber();3 项需归一 ×100(流动/速动/毛利率),12 项取数已百分数不归一;
  表外且命中「疑似错配」窄形态(阈值子句量级≥100 且 0<|实测|<10 且单位属比率类)→
  dimension_suspect:不落预警、不推送(唯一规则,防 586/587 假红灯复发)
- alert_generator:判定前归一,文案写「口径归一 ×100(原值 1.29 → 129%)」留审计线索
- 新增 scripts/kpi_unit_crosscheck.py(只读对照表产出器)

裁定2b(期间由「当月硬编码」→「最近可用期间」,后做)
- resolve_latest_period():取该 KPI 自己的最新 YYYY-MM(禁全局 MAX,防演示账套劫持);
  伪期间(2026-H1/H2、2025-FY)在 Python 侧过滤(保「单测=生产」语义)
- 过渡期覆盖度自检扩展:记录回落后的期间分布 + 当月 0 条归因提示
- --dry-run(零写入零推送)作放行前抽查证据;generate_and_push/run_alert_check 支持 dry_run

测试:backend/tests/test_caliber_rulings_20260917.py 17 项;全量 1158 passed/1 xfailed
2026-09-18 00:34:04 +08:00
Hermes CI Fix 06c23927aa fix(cma): KR 名称污染存量清理 + 透传点护栏 (cma-kr-name-pollution-cleanup-20260916)
ci/woodpecker/push/main Pipeline was canceled
- kr_name.py/krName.ts: sanitize_kr_name 判据(尾部≥/≤数字≠目标值→截断;==目标值→保留)
- maps.py/okr.py/okr_templates.py: 模板名/JSON→krs.title 透传点护栏
- test: 28 passed(独立复核)
- 原会话因迭代上限停手,本提交代收口(工作已复核完成)
2026-09-16 14:48:06 +08:00
Hermes CI Fix ae6f71dbc0 feat(cma-report): 建账期初层 + 方向判定可信度 + 权益行科目集(M1/M2/M7,规则表 v1.1)
- M1 时点报表口径:余额 = 期初(report_opening_balances) + 累计发生额;期初只作用于取数层
  (不生成凭证/不写 voucher_details/不改 kpi_values);basis=period 不套期初;受 effective_date 约束
- M2 期初未补 → direction_check_unreliable=true:报表/底稿带"期初未补,方向判定不可信"水印,
  重分类触发只进底稿诊断(diagnostic_only),不落地对外行项目
- M7 权益行科目集 = {3001,3002,3101,3103,3104,3105}(含 4001/4002/4101/4103/4104 别名):补实收资本 3001
- 1901 待处理财产损溢过渡列报「其他流动资产」以保恒等式(M6 归因文案)
- 只读验收脚本 + 幂等导入器(dry-run 默认,--commit 门控)
2026-09-16 01:17:09 +08:00
Hermes CI Fix ba85e03f2f feat(cma-report): 资产负债表取数层重分类 R1~R12(调表不调账)
ci/woodpecker/push/main Pipeline failed
依据《CMA 资产负债表重分类规则表 v1》(wecom-finance,任总 2026-09-16 批准落地)。

新增 backend/app/services/report_reclass.py:12 条规则挂 CMA 真实科目编码的取数层重分类引擎
- 余额方向规范化:资产 sum(借-贷) / 负债·权益 sum(贷-借),正常余额一律为正
- 触发判定全部基于原始余额(单遍、幂等),R3/R4 等互为镜像的规则互不干扰
- 科目取数前缀匹配(1122% 覆盖 112201 明细);备抵科目 1602 反向取数(修正固定资产净额虚增 2 倍折旧)
- 只读 voucher_details + 作废守卫,不生成记账凭证
- 输出:触发条目 / 不变量 / 负数清单 / 模板待核 / 未列报科目 / 损益未结转 诊断

reports.py:
- /balance-sheet 接入重分类;新增 basis 参数(end=期末余额累计为默认 / period=本期发生额兼容回退)
- 有凭证明细时不再混入示例数据(展示口径与落库口径同源);行项目增 raw_value/reclass_adjustment 可追溯
- 规则落位项目(模板未单列)按虚拟行列报;补「长期待摊费用(1801)」行、未分配利润补 3103/3104 别名
- 新增 /balance-sheet/reclass-working-paper(+ /export) 底稿 JSON/Excel;statutory 导出新增「重分类底稿」Sheet

测试 tests/test_report_reclass.py 14 例(触发/不变量/幂等/多租户/口径/底稿/虚拟行/阻塞规则)
全量 pytest:1108 passed, 1 xfailed。现网只读验收 scripts/verify_reclass_live_20260916.py(酣客 entity1 2026-08)
2026-09-16 00:38:01 +08:00
Hermes CI Fix fdf805ab8f test(cma): clear 10 pre-existing baseline failures (cma-baseline-tests-cleanup-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)
2026-09-15 18:36:46 +08:00
Hermes CI Fix 9d9a79545a feat(cma): external input ledgers for M2 capability eval (tax/hr/licence)
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.
2026-09-15 18:12:05 +08:00
Hermes CI Fix b796897f8c fix(auth): unify site-wide 401/403 semantics (cma-auth-401-403-unify-20260915)
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
2026-09-15 17:00:52 +08:00
Hermes CI Fix 1f7be6b5ae fix(cash): select cash-balance sync targets by explicit kpi_code whitelist
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).
2026-09-14 20:37:12 +08:00
Hermes CI Fix 8693c8e342 feat(kpi): route all voucher_details consumers through single void-mark guard
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).
2026-09-14 19:05:16 +08:00
Hermes CI Fix a8a82ff837 feat(kpi): 归集侧接作废/撤单标记过滤(voucher_void_marks)
- kpi_value_collector: 所有消费 voucher_details 的归集路径统一排除已作废凭证
  (NOT EXISTS voucher_void_marks, entity_id+voucher_no+voucher_date+revoked_at IS NULL),
  与 DB 视图 v_voucher_details_active 定义一致;标记表不存在时自动跳过(兼容旧库)
- 禁硬编码:凭证号不出现在代码/配置,撤单只需写标记表
- verify_cashflow_direct_caliber: 2026-08 从已知差异放行改为硬断言(8 期逐期一致)
- tests/test_void_marker_filter_20260914.py: 标记表在/不在 两分支 3 例
2026-09-14 18:44:39 +08:00
Hermes CI Fix 8fa6c7f520 feat(kpi): auto_collect review gate (finance ruling 2026-09-14)
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
2026-09-14 18:12:38 +08:00
Hermes CI Fix ec58efa20b fix(budget): 偏差检查同 KPI 多计划去重(deviation-check 补 db.flush)
autoflush=False 下,check_budget_deviation 循环内 existing_alert 去重查询
查不到本次刚 add 未 flush 的行 → 同期间同 KPI 多 active 预算计划会重复告警。
add 后补 db.flush() 使其对下一次循环可见。附回归测试。

Ref: yanxue-20260914-budget-p13p14-activation 遗留②试跑发现的缺陷
2026-09-14 17:56:26 +08:00
Hermes CI Fix 40d522cd34 fix(security): retire default-credential surface (yanxue-20260914-cma-defaultcred-code-fix)
- Login.vue: no prefilled default creds; no anonymous entity enumeration on mount

- deploy.sh: readiness polling (0.5s/30s cap) replaces fixed sleep 3; login smoke reads same-source 600 secret file, missing file = explicit abort

- 19 verify/check/gen_report scripts: shared _admin_creds() loader (CMA_ADMIN_ENV -> ops .secrets -> profile copy); no hardcoded creds

- tests: single TEST_USER_PASSWORD fixture constant; docs: pointer to 600 secret file; removed 4 legacy .bak + pycache residue
2026-09-14 17:39:53 +08:00
fullstack-bot b815b7f623 fix(security): close unauthenticated CMA user API surface + audit tool (cma-api-auth-surface-audit-20260914)
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.
2026-09-14 16:41:15 +08:00
Hermes CI Fix aac1bd1df1 fix(security): retire check-governance + scope /reports/dupont to session entity
- 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)
2026-09-14 12:09:04 +08:00
Hermes CI Fix 115a0ec755 fix(security): 4端点补跨租户鉴权 — 实体1会话读实体2数据(200)→403 (cma-cross-entity-endpoints-auth-20260914)
背景(项目Bot 补充复验 §四#2):以下 4 个只读端点未做会话账套鉴权,
用 entity=1(酣客) 会话 token + 显式 entity 参数可读到 entity=2(博海) 数据(HTTP 200):
  - GET /data-quality/governance-check?entity_id=2   → 200 score=88(对照 entity1=72)
  - GET /products/matrix?entity_id=2                 → 200
  - GET /alerts/risk-matrix?entity=bohai             → 200 博海风险矩阵
  - GET /action-plans/coso-checklist?entity=bohai    → 200 博海 COSO total_score=55
另:risk-matrix / coso-checklist 在非法实体值下静默回落酣客数据,UI 标签与数据不一致。

改动:
1. deps.py 新增统一防护入口 assert_entity_scope / assert_entity_slug_scope /
   get_session_entity / resolve_session_slug(文案对齐 get_entity_id:无权访问企业 entity_id=X(当前账套: Y))
2. governance-check / products/matrix:entity_id 须等于会话账套,否则 403;
   缺省按会话账套(governance-check 原缺省 0=全部 → 登录态下按账套收敛)
3. risk-matrix / coso-checklist:entity 须等于会话账套,否则 403;
   非法值 → 400 显式报错(不再静默回落酣客)
4. P3:POST /auth/switch-entity 加 DEPRECATED 注释(前端已无调用,保留不删)
5. 测试:tests/test_security_multitenant.py 新增 TestCrossEntityEndpointAuth 15 例
   (跨实体 403 / 同实体 200 / 缺省按账套 / 非法显式报错 / 参照端点行为不变)

验证:pytest tests/ -q → 951 passed(10 项失败为改动前既有基线,已 stash 对照确认);
      真实后端冒烟 19/19 PASS(entity=1 会话 4 端点跨实体全 403)。
2026-09-14 11:19:05 +08:00
wecom-fullstack e159db058a fix(alerts): F4b 展示口径与落库口径同源 — summary/suggestions/upcoming_7d 按快照批次 mode 过滤 + raw/excluded/batches 可见 (dama-cashplan-zombie-alert-cleanup-20260913) 2026-09-13 07:38:58 +08:00
Hermes CI Fix b8f4a3d543 chore(security): 凭据治理收口 — 清除硬编码DB账号/口令,脚本与API统一从 backend/.env 读取(无明文兜底);取消跟踪 .pyc 构建产物 (credential-governance-20260912) 2026-09-12 23:34:35 +08:00
Hermes CI Fix 46fc1a4ecc fix(bot-bridge): subjects category 白名单补齐库内实况 revenue/profit_loss — 消除库内合法值被400拒 (dama-cma-subjects-category-whitelist-20260912) 2026-09-12 11:09:16 +08:00
Hermes CI Fix ca2832b698 fix(bot-bridge): subjects 写入端点编码长度收敛到 DB 列宽20 — 21~32字符编码原穿过校验触发 MySQL 1406 整批500, 改为400拒绝 (dama-cma-subjects-write-endpoint-20260912 复核) 2026-09-12 10:46:57 +08:00
Hermes CI Fix 840d98765d feat(bot-bridge): 新增会计科目写入端点 POST /api/cma/bot/subjects — 行级全量校验+按subject_code幂等UPSERT(dry_run/update_existing开关)+L3留痕, 绝不DELETE (dama-cma-subjects-write-endpoint-20260912) 2026-09-12 10:42:46 +08:00
Hermes CI Fix af541e9043 feat(bot-bridge): 新增智享通科目字典写入端点 POST /api/cma/bot/dict/zxt-account — 幂等UPSERT(JSON/CSV)+L3分级+audit留痕, 解除 ops 管道 API 迁移阻塞 (ops-cma-dict-write-endpoint-20260912) 2026-09-12 10:07:20 +08:00
Hermes CI Fix e22093256b fix(kpi): suggest 保留历史默认口径 middle_best — 修复未知编码被误判为 higher_better(C_REBATE_RATE/BH_SALES_10 建议退化) (project-20260911-threshold-fix) 2026-09-11 17:27:12 +08:00
Hermes CI Fix 4c6fb85cf5 fix(kpi): 阈值红黄绿读写/填写规则修复 — 统一解析器+红灯优先+建议可解析+前端校验+存量修正 (project-20260911-threshold-fix)
① 新建 app/utils/threshold_parser.py 统一解析/判定(单一事实源):
   - 支持负数(>=-100000)、闭区间 A~B、多条件 C1|C2;空串=未设置
   - 判定顺序 red→green→yellow:红灯优先(消除宽黄灯遮蔽红灯),
     green 先于 yellow(green 是主条件,yellow 是更宽兜底带)
② kpis.py / alert_generator.py 双实现收敛到统一解析器(消除重复维护)
③ thresholds.py suggest 输出可解析表达式(原黄色/适中型生成 `52.33~107.67`、`偏离55%` 等
   区间/文字 → 用户点"自动建议"填入后静默失效;另修复大数目标生成 1.6e+06 科学计数法)
④ KPIDetail.vue 三个阈值输入加 placeholder/tooltip/帮助文案 + 保存前格式校验(非法即拒)
⑤ 存量 4 个不可解析表达式修正(id=46 C_REBATE_RATE / id=327 BH_SALES_10),
   改前 mysqldump 备份 backend/scripts/backups/kpi_definitions.bak-20260911.sql +
   threshold_rows_before_20260911.json(可回滚);不可解析 5→0
⑥ 单测 97 条 + 红黄绿判定回归 367 判定点(旧→新差异 14 条,含差异说明)
2026-09-11 17:25:24 +08:00
Hermes CI Fix f61b6ce604 fix(cash): 智享通凭证行级幂等键剔除派生period + period_conflict留痕 + 批内行序防吞行 (dama-zxt-idemkey-period-20260911)
缺陷: 行级幂等键含 period(日期派生属性),summary口径切主表VoucherDate后判重失效→重复入库(实测V17756 6行)。修: 幂等键=(entity_id,voucher_no,subject_code,debit,credit,summary);命中且period不同→period_conflict计数仅留痕不覆盖;同键多行按载荷内出现次序编号防跨批次吞行;返回体/日志补计数。回归测试10项 + 真实快照41830行复推实证(测试库)。
2026-09-11 17:13:07 +08:00
Hermes CI Fix 9822fbd9ba feat(alerts): 新增客户集中度预警规则 customer_concentration + 预测偏差规则启用/联动每日预警生成
- alert_rules: 新增 rule_type=customer_concentration(单一/前N客户收入占比超阈值触发,黄=超阈值/红=阈值x倍数)
  数据源 zxt_masterbill 客户(往来单位)收入聚合,unit_id<=0 占位剔除;无客户数据可回退 KPI 口径
  复用现有 KPIAlert 生成链路:check-all 内联分支 + 专用端点 POST /run-customer-concentration
- alert_generator(每日9:00): 新增规则引擎步骤,复用上述检查函数跑 forecast_deviation/customer_concentration
- AlertList.vue: 规则类型筛选/编辑项 + 参数表单 + 客户集中度检查按钮
- 测试: tests/test_customer_concentration_rule.py 17 条(规则注册/聚合/双数据源/黄红灯/去重/check-all)
- 验证脚本: scripts/verify_forecast_deviation_rules_e2e.py / verify_customer_concentration_rule_e2e.py
2026-09-10 09:56:50 +08:00
Hermes CI Fix 397c365263 feat(maps): 战略地图版本管理完整实现(方案B)— versions API列表/详情/回滚带自动备份/备注 + MapVersions独立版本页 + 路由注册 + 版本弹窗接真实数据 (project-20260908-map-versions-page) 2026-09-08 20:11:59 +08:00
Hermes CI Fix 193061cf4d fix(data): 数据管理模块脱节修复 — Excel导入/sources端点 entity 隔离 + ERP直连通道诚实化(410) + 真实zxt链路展示
- import_excel/import_excel_smart KPI字典按 entity_id 过滤(防跨账套匹配串写,P0安全)
- import_excel_smart 自动建KPI编码防冲突(_next_code 循环取号)
- sources list/create/update/delete 全部按 entity_id 隔离(create 写入当前账套)
- sync-kpis + admin/erp-sync 下线返回410(erp-gateway DB凭据2026-08-31已删,真实通道=zxt智享通注入)
- 新增 GET /data/channel-status:展示真实zxt取数通道状态+遗留ERP数据源清单(诚实化)
- DataManage.vue:数据源tab顶部展示真实通道(最近注入period/时间/行数/落盘目录mtime)、ERP遗留源标注'非当前通道'、实际取数映射(kpi_value_sources)表格
- 新增 test_data_entity_isolation_20260908.py(8条:双账套导入隔离/CRUD隔离/sync-kpis 410/channel-status)
2026-09-08 11:19:38 +08:00