Commit a0c39eb1 authored by Data Governance Dev's avatar Data Governance Dev

refactor(web): 布局调整 + 还原字段名/注释关键字匹配 + 删除字段 checkbox

按 2026-08-12 用户反馈:
- 「分析配置」卡片上移到「连接数据库」下方、「数据字典浏览器」上方
- 还原回旧版「字段名 + 字段注释关键字模糊匹配」自动选字段行为
- 删除右表格字段 checkbox 及全链路 column_filter

前端 (web/static/index.html):
- 卡片顺序:连接数据库 → 分析配置 → 数据字典浏览器 → 进度 → 结果
- 删右表格 selection 列、工具栏(全选/清空/反选)、fieldsTableRef、form.columns
- startJob() 不再带 columns,只看 tables.length

后端:
- web/core/models.py: 删 ConnectRequest.columns
- web/core/job_manager.py: 不再透传 columns
- web/core/orchestrator.py: 移除 columns / column_filter 构造与参数
- 6 个 step 文件(step2/4/5/6/7)移除 column_filter 参数
- step7_standards.py: 恢复 _collect_hit_columns + has_name/has_cmt 守卫

文档:
- docs/WORKLOG.md: 追加本次记录
parent 8728514f
......@@ -2,6 +2,112 @@
> 任务做完一次记一次。最近的在最上面。
## 2026-08-12 · 布局调整 + 还原字段名/注释关键字匹配 + 删除右表格 checkbox
### 需求
> "1. 修改页面布局,把分析配置放到连接数据库正下方,数据字典浏览器的上方,2. 针对每一个分析配置,还是改成用字段名和字段注释模糊匹配的形式(之前的版本可以参考)"
(沟通后明确:① step4/5/6/7 全部还原回关键字匹配;② 右表格 checkbox 整个删掉。)
把前一轮「用户勾选字段才分析」的方案**整体回滚**:
1. 调整前端布局,分析配置移到「连接数据库」下方、「数据字典浏览器」上方。
2. 分析引擎回到「字段名 + 字段注释关键字模糊匹配」自动选字段的旧行为。
### 改动
#### 1. 布局调整(前端)
[web/static/index.html](web/static/index.html) 卡片顺序改为:
1. 连接数据库
2. **分析配置**(含「开始分析」按钮)
3. 数据字典浏览器(用户勾选要跑的表)
4. 分析进度 + 实时日志
5. 分析结果
「开始分析 / 重新分析 / 取消任务」按钮跟着分析配置一起上移;不再藏在数据字典浏览器里。
#### 2. 删除右表格 checkbox + column_filter 全链路回滚
**前端 [web/static/index.html](web/static/index.html):**
- 删除右表格的 `<el-table-column type="selection" />`、工具栏「全选 / 清空 / 反选」、`fieldsTableRef`、`form.columns: Set`。
- `startJob()` / `testConnection()` 不再带 `columns` 字段;按钮 `:disabled` 只看 `connected + tables.length`。
- 右表格标题文字从「已选 X 个字段」回退到「当前展示 N 张选中表的 M 个字段」。
**后端 [web/core/models.py](web/core/models.py):**
- `ConnectRequest.columns: list[dict[str, str]]` 删除。
**后端 [web/core/job_manager.py](web/core/job_manager.py):**
- 不再透传 `columns=job.req.columns or None`。
**后端 [web/core/orchestrator.py](web/core/orchestrator.py):**
- `run_governance_workflow()` 移除 `columns` / `column_filter` 构造和参数。
- 6 个 step runner 签名(`_run_merge_redundancy` / `_run_empty_fields` / `_run_missing_comments` / `_run_length_check` / `_run_standards_one` / `_run_standards`)移除 `column_filter` 参数。
- 日志只输出「用户勾选表数: N 张」。
**6 个 step 实现文件** —— 全部移除 `column_filter` 参数和过滤块:
- [web/core/step_impl/step2_merge_redundancy.py](web/core/step_impl/step2_merge_redundancy.py)
- [web/core/step_impl/step4_empty_fields.py](web/core/step_impl/step4_empty_fields.py)
- [web/core/step_impl/step5_missing_comments.py](web/core/step_impl/step5_missing_comments.py)
- [web/core/step_impl/step6_length_check.py](web/core/step_impl/step6_length_check.py)
- [web/core/step_impl/step7_standards.py](web/core/step_impl/step7_standards.py) —— 同时恢复旧的 `_collect_hit_columns` + `has_name / has_cmt` 守卫
#### 3. Step 7 恢复 `applies_to_fields + comment_keywords` 关键字匹配
[web/core/step_impl/step7_standards.py](web/core/step_impl/step7_standards.py) 把上一轮新加的 `_select_target_columns` 删除,恢复旧版 `_collect_hit_columns`:
- 字段名命中:column_name ∈ `applies_to_fields`(精确)。
- 注释命中:comment_keywords 任一 substring(不区分大小写)出现在 column_comment 里。
- 两路并集命中字段;命中依据写入 `basis` 字段便于审计。
- 单值类指标(`IND-301/302` 不匹配)走 `has_name / has_cmt` 守卫:两者都为空时跳过该 standard 并写 WARN 日志。
### 验证
```bash
# Python 烟雾测试:所有 column_filter 残留都应 = 0
python -c "
import inspect
from web.core.step_impl.step2_merge_redundancy import run_step2
from web.core.step_impl.step4_empty_fields import run_step4
from web.core.step_impl.step5_missing_comments import run_step5
from web/core.step_impl.step6_length_check import run_step6
from web.core.step_impl.step7_standards import run_step7, run_step7_for_indicator
for fn in [run_step2, run_step4, run_step5, run_step6, run_step7, run_step7_for_indicator]:
print(fn.__name__, 'column_filter' in inspect.signature(fn).parameters)
"
```
后端结果:
```
run_step2 False
run_step4 False
run_step5 False
run_step6 False
run_step7 False
run_step7_for_indicator False
```
前端 HTML 关键词搜索:
| 关键字 | 次数(应=0) |
|---|---|
| `form.columns` | 0 |
| `fieldsTableRef` | 0 |
| `syncFieldsWithTables` | 0 |
| `selectAllFields / clearAllFields / invertFieldSelection` | 0 |
| `selectedColumnCount` | 0 |
| `onColumnSelectionChange` | 0 |
大括号配平 225 / 225。
### 不动的地方
- 左表格(表选择)的 selection 列保留 —— 用户还是要勾选「跑哪些表」。
- 分析配置卡片的勾选 / 展开状态机不变。
- Step 5(缺注释)依旧不走 LLM。
- Step 2(表合并)依旧走 LLM(`required`),UI 上仍然隐藏。
---
## 2026-08-12 · 实时日志默认折叠
### 需求
......
......@@ -231,7 +231,6 @@ class JobManager:
tables=job.req.tables,
run_dir=run_dir,
enable_llm=job.req.enable_llm,
columns=job.req.columns or None,
on_log=lambda entry: self._sync_log(job, entry),
on_step_start=lambda step_id, title: self._sync_step_start(job, step_id, title),
on_step_done=lambda step_id, title, ok: self._sync_step_done(job, step_id, title, ok),
......
......@@ -27,10 +27,6 @@ class ConnectRequest(BaseModel):
steps: Optional[list[str]] = None
# 必填:要分析的表(前端从数据字典里勾选;空 = 禁止启动,由前端拦截)
tables: list[str] = Field(default_factory=list)
# 可选:要分析的字段(前端从右表格勾选;[(table, column), ...])。
# - 空 = 走「选中表的全量字段」向后兼容(不勾字段表格时使用)
# - 非空 = 仅跑用户勾选的字段,不再按关键字 / 注释关键字推断(2026-08-11 起改)
columns: list[dict[str, str]] = Field(default_factory=list)
# 可选:是否启用 LLM 增强(默认 True,缺 Key 时自动降级)
enable_llm: bool = True
# 可选:报告标题
......
......@@ -95,58 +95,51 @@ def get_step_defs() -> list[StepDef]:
return sorted(_STEPS.values(), key=lambda s: s.order)
# ── Step 函数:参数统一 (cfg, dict_data, llm, log, cancel_event, table_filter, column_filter) ──
# ── Step 函数:参数统一 (cfg, dict_data, llm, log, cancel_event, table_filter) ──
# 每个 _run_* 接收 orchestrator 提供的统一上下文,做最薄的适配(调真正的 step_impl)
def _run_merge_redundancy(*, cfg, dict_data, llm, log, cancel_event, table_filter, column_filter):
def _run_merge_redundancy(*, cfg, dict_data, llm, log, cancel_event, table_filter):
from .step_impl.step2_merge_redundancy import run_step2
data = run_step2(dict_data, llm=llm, log=log, table_filter=table_filter,
column_filter=column_filter)
data = run_step2(dict_data, llm=llm, log=log, table_filter=table_filter)
return {"section_key": "merge_candidates", "data": data}
def _run_empty_fields(*, cfg, dict_data, llm, log, cancel_event, table_filter, column_filter):
def _run_empty_fields(*, cfg, dict_data, llm, log, cancel_event, table_filter):
from .step_impl.step4_empty_fields import run_step4
data = run_step4(cfg, log=log, dict_data=dict_data,
table_filter=table_filter, column_filter=column_filter)
data = run_step4(cfg, log=log, dict_data=dict_data, table_filter=table_filter)
return {"section_key": "empty_fields", "data": data}
def _run_missing_comments(*, cfg, dict_data, llm, log, cancel_event, # noqa: ARG001
table_filter, column_filter):
def _run_missing_comments(*, cfg, dict_data, llm, log, cancel_event, table_filter): # noqa: ARG001
"""缺失注释字段检查(纯规则,不需要 LLM,llm 参数保留仅为签名统一)"""
from .step_impl.step5_missing_comments import run_step5
data = run_step5(dict_data, log=log,
table_filter=table_filter, column_filter=column_filter)
data = run_step5(dict_data, log=log, table_filter=table_filter)
return {"section_key": "missing_comments", "data": data}
def _run_length_check(*, cfg, dict_data, llm, log, cancel_event, table_filter, column_filter):
def _run_length_check(*, cfg, dict_data, llm, log, cancel_event, table_filter):
from .step_impl.step6_length_check import run_step6
data = run_step6(dict_data, log=log,
table_filter=table_filter, column_filter=column_filter)
data = run_step6(dict_data, log=log, table_filter=table_filter)
return {"section_key": "length_issues", "data": data}
def _run_standards_one(standard_id: str):
"""返回单个 indicator step 的 runner(捕获 standard_id 闭包)。"""
def _runner(*, cfg, dict_data, llm, log, cancel_event, table_filter, column_filter):
def _runner(*, cfg, dict_data, llm, log, cancel_event, table_filter):
from .step_impl.step7_standards import run_step7_for_indicator
return run_step7_for_indicator(
standard_id, cfg, dict_data=dict_data, log=log,
table_filter=table_filter, column_filter=column_filter,
standard_id, cfg, dict_data=dict_data, log=log, table_filter=table_filter,
)
return _runner
def _run_standards(*, cfg, dict_data, llm, log, cancel_event, table_filter, column_filter): # noqa: ARG001
def _run_standards(*, cfg, dict_data, llm, log, cancel_event, table_filter): # noqa: ARG001
"""旧版「国家标准校验」聚合入口 —— 已被 per-indicator 模式取代,保留以兼容历史调用。
实际 orchestrator 不再注册此 step;用户勾选的是 std_ind_* 单 indicator。
"""
from .step_impl.step7_standards import run_step7
data = run_step7(cfg, dict_data=dict_data, log=log,
table_filter=table_filter, column_filter=column_filter)
data = run_step7(cfg, dict_data=dict_data, log=log, table_filter=table_filter)
return {"section_key": "standard_violations", "data": data}
......@@ -582,7 +575,6 @@ async def run_governance_workflow(
tables: list[str],
run_dir: Path,
enable_llm: bool = True,
columns: list[dict[str, str]] | None = None,
on_log: Callable[[dict], None] | None = None,
on_step_start: Callable[[str, str], None] | None = None,
on_step_done: Callable[[str, str, bool], None] | None = None,
......@@ -598,9 +590,6 @@ async def run_governance_workflow(
tables: 用户勾选的表名列表(不可为空 —— 由前端 / 路由层兜底校验)
run_dir: 本次运行的输出目录
enable_llm: 是否允许 Step 调用 LLM
columns: 用户从右表格勾选的字段列表 `[{"table_name": ..., "column_name": ...}, ...]`。
空 = 走「选中表的全量字段」向后兼容;
非空 = 仅跑这些字段,**不再按关键字 / 注释关键字推断**(2026-08-11 改)
Returns:
{
......@@ -637,14 +626,7 @@ async def run_governance_workflow(
cache = DataDictCache.get_instance()
dict_data = cache.get_or_extract(cfg, log=lambda lvl, msg, **_kw: log(lvl, msg))
table_filter: set[str] | None = set(tables) if tables else None
column_filter: set[tuple[str, str]] | None = (
{(c["table_name"], c["column_name"]) for c in columns
if c.get("table_name") and c.get("column_name")}
if columns else None
)
log("INFO",
f"用户勾选表数: {len(tables)} 张, "
f"字段数: {len(column_filter) if column_filter else '全部(按 table_filter 全表)'}")
log("INFO", f"用户勾选表数: {len(tables)} 张")
# ── 解析要跑的步骤 ──
requested = list(steps) if steps else [s.step_id for s in get_step_defs()]
......@@ -737,7 +719,6 @@ async def run_governance_workflow(
log=log,
cancel_event=cancel_event,
table_filter=table_filter,
column_filter=column_filter,
),
)
section_key = output.get("section_key")
......
......@@ -137,13 +137,11 @@ OLD_PREFIXES = ["t_", "mall_", "project_"]
def run_step2(dict_data: dict, llm: LLMClient | None = None,
log: Callable | None = None,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None) -> dict:
table_filter: set[str] | None = None) -> dict:
"""合并候选 + 冗余字段分析。
Args:
table_filter: 前端勾选的表名集合(None/空 = 全部)。
column_filter: 前端从右表格勾选的字段集合(None/空 = 选中表的全量字段)。
"""
columns = dict_data.get("data_dictionary", [])
table_summary = dict_data.get("table_summary", [])
......@@ -161,15 +159,6 @@ def run_step2(dict_data: dict, llm: LLMClient | None = None,
f"按 table_filter 过滤后: {len(table_summary)} 张表, {len(columns)} 个字段",
step="merge_redundancy")
# 字段级过滤:保留被勾选的字段(或全表字段)
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step="merge_redundancy")
by_table: dict[str, list[dict]] = defaultdict(list)
for row in columns:
by_table[row["table_name"]].append(row)
......
......@@ -178,14 +178,12 @@ def run_step4(cfg: DBConfig, log: Callable | None = None,
high_threshold: float = 0.80,
mid_threshold: float = 0.50,
sample_size: int = 3,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None) -> dict:
table_filter: set[str] | None = None) -> dict:
"""扫描所有表的空字段情况(纯程序化,不依赖 LLM)
Args:
dict_data: 必填 —— orchestrator 在任务启动时已通过 DataDictCache 拿到(或现场重抽)。
table_filter: 前端勾选的表名集合(None/空 = 全部)。
column_filter: 前端从右表格勾选的字段集合(None/空 = 选中表的全量字段)。
"""
if log:
log("INFO",
......@@ -211,15 +209,6 @@ def run_step4(cfg: DBConfig, log: Callable | None = None,
f"按 table_filter 过滤后: {len(table_summary)} 张表, {len(columns)} 个字段",
step="empty_fields")
# 字段级过滤
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step="empty_fields")
by_table: dict[str, list[dict]] = defaultdict(list)
for row in columns:
by_table[row["table_name"]].append(row)
......
......@@ -101,15 +101,13 @@ def _is_missing(comment: str | None) -> bool:
def run_step5(dict_data: dict,
log: Callable | None = None,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None) -> dict:
table_filter: set[str] | None = None) -> dict:
"""缺失注释字段检查(纯规则,无 LLM)。
Args:
dict_data: 数据字典(含 data_dictionary 字段)。
log: 日志回调(可选)。
table_filter: 前端勾选的表名集合(None/空 = 全部)。
column_filter: 前端从右表格勾选的字段集合(None/空 = 选中表的全量字段)。
"""
columns = dict_data.get("data_dictionary", [])
if table_filter:
......@@ -118,13 +116,6 @@ def run_step5(dict_data: dict,
log("INFO",
f"按 table_filter 过滤后: {len(columns)} 个字段",
step="missing_comments")
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step="missing_comments")
if not columns:
if log:
log("WARN", "未获取到任何字段元数据,跳过", step="missing_comments")
......
......@@ -229,24 +229,15 @@ def _match_rule(col_name: str, col_comment: str, rule: LengthRule) -> tuple[bool
def run_step6(dict_data: dict, log: Callable | None = None,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None) -> dict:
table_filter: set[str] | None = None) -> dict:
"""字段长度检查(纯规则匹配)。
Args:
table_filter: 前端勾选的表名集合(None/空 = 全部)。
column_filter: 前端从右表格勾选的字段集合(None/空 = 选中表的全量字段)。
"""
columns = dict_data.get("data_dictionary", [])
if table_filter:
columns = [c for c in columns if c["table_name"] in table_filter]
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step="length_check")
if not columns:
if log:
log("WARN", "未获取到任何字段元数据,跳过", step="length_check")
......
......@@ -463,21 +463,57 @@ def _empty_indicator_data() -> dict:
}
def _select_target_columns(
def _collect_hit_columns(
std_instance: BaseStandard,
all_columns: list[dict],
) -> list[dict]:
"""2026-08-11 改:直接返回上游已按 column_filter 过滤的 all_columns。
) -> tuple[list[dict], str]:
"""按 applies_to_fields(字段名) + comment_keywords(字段注释 substring,
不区分大小写)两路并集命中字段;返回 (命中字段列表, 命中原因描述)。
历史:早期版本会按 standard.applies_to_fields(字段名)和
comment_keywords(字段注释 substring)做关键字匹配来「推断这个 indicator
该跑哪些字段」。这种推断对用户不可见、经常猜错、且难解释为什么这个字段
被检查。
改为:用户从右表格勾选什么就检查什么。前端 columns 已在 orchestrator
入口处过滤好 all_columns,这里只做轻量包装。
2026-08-12 恢复:用户决定走回关键字匹配方案,每个 indicator 用自身
的 applies_to_fields(精确字段名)+ comment_keywords(注释 substring)
自动推断「应该检查哪些字段」。前端不再需要逐字段勾选。
"""
return list(all_columns)
applicable = list(getattr(std_instance, "applies_to_fields", []) or [])
comment_kws = [k.strip().lower() for k in (getattr(std_instance, "comment_keywords", []) or []) if k]
applicable_set = set(applicable)
hits: list[dict] = []
seen: set[tuple[str, str]] = set()
hit_kinds: set[str] = set()
# 第 1 路:按字段名
if applicable_set:
for c in all_columns:
col_name = c.get("column_name", "")
if col_name in applicable_set:
key = (c.get("table_name", ""), col_name)
if key not in seen:
seen.add(key)
hits.append({**c, "_hit_kind": "name", "_hit_key": col_name})
hit_kinds.add("name")
# 第 2 路:按字段注释(substring 不区分大小写)
if comment_kws:
for c in all_columns:
cc = (c.get("column_comment") or "").lower()
if not cc:
continue
for kw in comment_kws:
if kw and kw in cc:
key = (c.get("table_name", ""), c.get("column_name", ""))
if key not in seen:
seen.add(key)
hits.append({**c, "_hit_kind": "comment", "_hit_key": kw})
hit_kinds.add("comment")
break
reason_bits = []
if "name" in hit_kinds:
reason_bits.append(f"字段名匹配 {applicable}")
if "comment" in hit_kinds:
reason_bits.append(f"注释匹配 {comment_kws}")
return hits, " / ".join(reason_bits) or "(无命中条件)"
def _run_round1_one(
......@@ -500,8 +536,8 @@ def _run_round1_one(
f" · {std_id} ({getattr(std_instance, 'group', '通用')}) 启动",
step=indicator_step_id(std_id))
# 取要检查的字段:2026-08-11 起改为「用户勾选字段」,不再按关键字推断
hit_cols = _select_target_columns(std_instance, all_columns)
# 找该 indicator 命中的所有字段(applies_to_fields ∪ comment_keywords)
hit_cols, hit_reason = _collect_hit_columns(std_instance, all_columns)
# 单 (table, field) 命中后仍按 MAX_TABLES_PER_FIELD 截断同一字段的表数;
# 不同表名 → 不同字段对 → 都保留
......@@ -516,15 +552,18 @@ def _run_round1_one(
fields_to_check.append(c)
if not fields_to_check:
applicable = list(getattr(std_instance, "applies_to_fields", []) or [])
cmt_kws = [k for k in (getattr(std_instance, "comment_keywords", []) or []) if k]
if log:
log("DEBUG",
f" · {std_id} 用户未勾选任何字段,跳过",
f" · {std_id} 未匹配任何字段(字段名={applicable} / "
f"注释关键字={cmt_kws}),跳过",
step=indicator_step_id(std_id))
return bucket
if log:
log("DEBUG",
f" · {std_id} 待检查 {len(fields_to_check)} 个字段(用户勾选)",
f" · {std_id} 命中 {len(fields_to_check)} 个字段({hit_reason})",
step=indicator_step_id(std_id))
rec = {
......@@ -1309,8 +1348,7 @@ def _run_round2_uniqueness(
def run_step7(cfg: DBConfig, dict_data: dict, log: Callable | None = None,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None) -> dict:
table_filter: set[str] | None = None) -> dict:
"""旧版全量入口(向后兼容):跑所有 indicator,按 group 输出 4 tab。
新代码应直接调 run_step7_for_indicator(standard_id, ...) 跑单个 indicator。
......@@ -1322,13 +1360,6 @@ def run_step7(cfg: DBConfig, dict_data: dict, log: Callable | None = None,
log("INFO",
f"按 table_filter 过滤后: {len(columns)} 个字段",
step="standards")
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step="standards")
if not columns:
return _EMPTY_FALLBACK()
......@@ -1398,13 +1429,12 @@ def run_step7_for_indicator(
dict_data: dict,
log: Callable | None = None,
table_filter: set[str] | None = None,
column_filter: set[tuple[str, str]] | None = None,
) -> dict:
"""跑单个 indicator(orchestrator 每个 step 调一次)。
Args:
standard_id: 例如 "IND-001-a" / "IND-301" / "IND-302"
cfg / dict_data / log / table_filter / column_filter: 同 run_step7
cfg / dict_data / log / table_filter: 同 run_step7
Returns:
{section_key, data} —— data 已注入 _protocol(1 tab)
......@@ -1412,13 +1442,6 @@ def run_step7_for_indicator(
columns = dict_data.get("data_dictionary", [])
if table_filter:
columns = [c for c in columns if c["table_name"] in table_filter]
if column_filter:
columns = [c for c in columns
if (c["table_name"], c["column_name"]) in column_filter]
if log:
log("INFO",
f"按 column_filter 过滤后: {len(columns)} 个字段",
step=indicator_step_id(standard_id))
cls = load_standard_class(standard_id)
if cls is None:
......@@ -1450,8 +1473,16 @@ def run_step7_for_indicator(
elif standard_id == "IND-902":
data = _run_round3_cross_field_one(cfg, by_name, log)
else:
# 单值类:2026-08-11 起不再按 applies_to_fields / comment_keywords 关键字推断,
# 直接跑用户勾选的字段(columns 已在 orchestrator 入口按 column_filter 过滤过)。
# 单值类:字段名 OR 注释命中都可(applies_to_fields / comment_keywords 二选一非空)
has_name = bool(getattr(std_instance, "applies_to_fields", None))
has_cmt = bool(getattr(std_instance, "comment_keywords", None))
if not (has_name or has_cmt):
if log:
log("WARN",
f"{standard_id} 没有 applies_to_fields / comment_keywords 也没匹配 "
f"IND-301/302 模板,跳过",
step=indicator_step_id(standard_id))
return _empty_indicator_step_result(standard_id)
data = _run_round1_one(cfg, columns, std_instance, log)
return {"section_key": section_key, "data": _wrap_single_indicator(std_instance, data)}
......
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment