diff --git a/skills/ir_generation_skill/step1_semantic_index.py b/skills/ir_generation_skill/step1_semantic_index.py index 68879f7..4e34b1a 100644 --- a/skills/ir_generation_skill/step1_semantic_index.py +++ b/skills/ir_generation_skill/step1_semantic_index.py @@ -553,28 +553,67 @@ def _quick_validate( f"未覆盖: {uncovered[:5]}" ) - # Count table rows + # Count table rows — only from functional sections with content total_rows = sum( len(b.get("rows", [])) for s in doc.get("sections", []) + if _is_functional_section(s.get("source", "")) + and _has_section_content(s) for b in s.get("blocks", []) if b.get("type") == "table" ) - covered_rows = sum( - 1 for fu in units - for src in fu.get("sources", []) - if src.get("type") == "table" and src.get("row") - ) - row_cov = covered_rows / max(total_rows, 1) - # When there are no table rows to cover, skip the check (not a coverage failure) + covered_set: set[tuple] = set() + for fu in units: + for src in fu.get("sources", []): + if src.get("type") == "table" and src.get("row"): + covered_set.add((src.get("section", ""), src.get("row"))) + covered_rows = len(covered_set) + # When there are no table rows to cover, skip check if total_rows == 0: row_cov = 1.0 + else: + row_cov = covered_rows / total_rows print(f" 表格行覆盖率: {row_cov:.0%} ({covered_rows}/{total_rows} rows)", flush=True) if row_cov < SECTION_COVERAGE_TARGET: + # Collect specific missing rows with content for targeted feedback + missing_rows: list[dict] = [] + for s in doc.get("sections", []): + if not _is_functional_section(s.get("source", "")): + continue + if not _has_section_content(s): + continue + sec_name = s.get("source", "").split()[0] if s.get("source") else "?" + for b in s.get("blocks", []): + if b.get("type") != "table": + continue + for row in b.get("rows", []): + rn = row.get("row") + if (sec_name, rn) not in covered_set: + key_col = "" + val_col = "" + for col in row.get("columns", []): + cn = col.get("name", "") + ct = col.get("text", "")[:100] + if cn in ("功能", "三级功能", "一级功能", "功能名称"): + key_col = ct + elif cn in ("功能详细说明", "详细说明", "四级功能", "说明"): + val_col = ct + if not key_col: + # Use first column as key + for col in row.get("columns", []): + key_col = col.get("text", "")[:60] + break + missing_rows.append({ + "section": sec_name, + "row": rn, + "key": key_col, + "value": val_col, + }) gaps["coverage_warnings"].append( f"表格行覆盖率 {row_cov:.0%} < {SECTION_COVERAGE_TARGET:.0%}, " - f"({covered_rows}/{total_rows} rows)" + f"({covered_rows}/{total_rows} rows from functional sections)" ) + gaps["missing_table_rows"] = missing_rows # Coverage warnings are non-blocking (depend on LLM prompt quality) if gaps["coverage_warnings"]: @@ -595,19 +634,34 @@ def _build_coverage_feedback(gaps: dict) -> str: parts = [] for item in gaps.get("coverage_warnings", []): parts.append(f"- {item}") + + # Include specific missing table rows with their content + missing_rows = gaps.get("missing_table_rows", []) + if missing_rows: + parts.append(f"\n### 以下具体表格行缺少对应 function_unit(共 {len(missing_rows)} 行):\n") + for mr in missing_rows: + sec = mr.get("section", "?") + rn = mr.get("row", "?") + key = mr.get("key", "") + val = mr.get("value", "") + parts.append( + f"- **章节 {sec}, 行 {rn}**: {key}" + + (f" — {val}" if val else "") + ) + if not parts: return "" return ( - "\n## 关键覆盖反馈(上一轮 LLM 输出了以下缺口,请重新处理)\n\n" + "\n## 关键覆盖反馈(上一轮 LLM 输出存在缺口,请重新处理)\n\n" + "\n".join(parts) + "\n\n" "### 修复动作(必须执行)\n\n" - "1. **重新扫描上述每个缺失章节**,从文字和表格中提取所有可被测试的功能行为\n" - "2. **为每个缺失的表格行创建独立的 function_unit**,不得合并不同行的规则\n" + "1. **重新扫描上述每个缺失章节和表格行**,从文字和表格中提取所有可被测试的功能行为\n" + "2. **为上述每个缺失表格行创建独立的 function_unit**,不得合并不同行的规则\n" "3. **每个 function_unit 必须引用具体的 section 号和 row 号**作为 source\n" "4. **非功能章节可以跳过**(如背景、术语、变更日志),但行为规则章节必须覆盖\n" - "5. 输出中必须包含针对上述缺口的新 function_unit\n" + "5. 输出中必须包含针对上述缺口的新 function_unit,**尤其是列出具体缺失的表格行**\n" ) diff --git a/tests/acceptance/test_main_health.py b/tests/acceptance/test_main_health.py index 6a027af..3a86d89 100644 --- a/tests/acceptance/test_main_health.py +++ b/tests/acceptance/test_main_health.py @@ -137,16 +137,22 @@ def _extract_content_units(parsed_data: dict) -> dict: for sec in sections: name = sec.get("source", "") - if _is_functional_section(name) and _has_section_content(sec): + is_func = _is_functional_section(name) and _has_section_content(sec) + if is_func: functional_sections.append({ "name": name, "number": _section_number(name), }) - for block in sec.get("blocks", []): - if block.get("type") == "table": - rows = block.get("rows", []) - total_table_rows += len(rows) + # Only count table rows from functional sections + # (non-functional sections like changelog, glossary, references + # cannot be covered by function_units — counting them inflates + # the denominator and yields misleadingly low coverage.) + if is_func: + for block in sec.get("blocks", []): + if block.get("type") == "table": + rows = block.get("rows", []) + total_table_rows += len(rows) # Diagram-type images from image_analysis diagram_rids: list[str] = []