Files
cobol-java-v3/hina/classifier.py
T
NB-076 33762ca959 fix: adversarial testing — 4 false positive/negative fixes + comment stripping
COBOL migration expert adversarial testing found 4 real defects:

FIX 1: Comment-stripping in detect_keyword() (FP-2)
- Remove *> inline comments and * comment lines before keyword matching
- Prevents 「マッチング」 from triggering on WS-KEY in comments

FIX 2: KEY comparison context validation (FP-1, FP-6)
- Add _matches_key_comparison() — requires WS-KEY variable to appear
  NEAR an actual comparison operator (= < >), not just as PIC/VALUE decl
- Same check in _path_rule_engine features via has_key_var injection
- Fix regex bug: [=<>\s] vs [=<>] — \s matched whitespace after PIC decl

FIX 3: Old-school naming support (FN-1)
- Add L1 keyword r'[A-Z]\d{0,2}-\w*KEY' with 0.55 confidence
- Matches K01-KEY, KS-KEY etc. (non-WS- prefix naming convention)

FIX 4: mn_output_mode over-matching (FP-6)
- Require IF branches + KEY evidence before returning M:N for file>=3
- matching_vs_keybreak rule 3 now requires has_key_var

New tests: test_adversarial.py — 8 parametrized adversarial tests
Regression: 755 passed (0 new failures)
2026-06-21 15:16:41 +08:00

213 lines
8.2 KiB
Python

"""
HINA 程序分类器 — L1 关键字规则 + 确信度计算。
通过 COBOL 源码中的关键字匹配进行程序分类,支持多级确信度判定。
"""
from __future__ import annotations
import re
from typing import Any
# ── L1 规则 ──────────────────────────────────────────────────────────────
# 格式: (分类名称, [关键字列表], 置信度阈值)
L1_RULES: list[tuple[str, list[str], float]] = [
("DB操作", ["EXEC SQL"], 0.95),
("子程序调用", ["CALL", "LINKAGE SECTION"], 0.90),
("IS INITIAL", ["IS INITIAL"], 0.99),
("SYSIN", ["SYSIN"], 0.90),
("编码转换", ["ALPHABETIC", "ASCII", "EBCDIC"], 0.85),
("online", ["DFHCOMMAREA", "MAP"], 0.95),
("SORT", ["SORT ON KEY"], 0.95),
("MERGE", ["MERGE ON KEY"], 0.95),
("编辑输出", ["WRITE AFTER", "WRITE BEFORE"], 0.80),
("文件编成", ["ORGANIZATION IS"], 0.99),
("替代索引", ["ALTERNATE RECORD KEY"], 0.99),
("マッチング", ["re:WS-[\\w-]*KEY"], 0.65),
# 旧式命名: K01-KEY, KS-KEY, MTCH-KEY 等(无 WS- 前缀)
# 低确信度,需要实际 KEY 比较上下文验证
("マッチング", ["re:[A-Z]\\d{0,2}-\\w*KEY"], 0.55),
]
# ── 冲突解决规则 ─────────────────────────────────────────────────────────
# 当 L1 匹配到多个分类时的消歧策略:
# value = "file_count" → 取测试数更多的分类
# value = "has_accumulator" → 取包含累加器的分类
CONFLICT_RULES: dict[tuple[str, str], str] = {
("マッチング", "キーブレイク"): "file_count",
("編集処理", "項目チェック"): "file_count",
("キーブレイク", "項目チェック(重複)"): "has_accumulator",
}
# ── 关键字检测 ───────────────────────────────────────────────────────────
def _strip_cobol_comments(source: str) -> str:
"""剥离 COBOL 注释,避免注释中的关键词触发 L1 匹配。
处理两种注释:
- 固定格式列 7: 行首 `*` (comment line)
- 自由格式/内联: `*> ...` 到行尾
"""
lines = source.split('\n')
cleaned = []
for line in lines:
# 自由格式/内联注释: *>
idx = line.find('*>')
if idx >= 0:
line = line[:idx]
# 固定格式注释行: 如果第一个非空字符是 *
stripped = line.strip()
if stripped.startswith('*') and not stripped.startswith('*/'):
continue # 跳过整个注释行
cleaned.append(line)
return '\n'.join(cleaned)
def _matches_key_comparison(source_upper: str) -> bool:
"""检查源码中是否包含实际的 KEY 变量比较(而非仅声明)。
匹配 KEY 变量在比较上下文中的使用:
WS-KEY = / WS-KEY > / WS-KEY <
IF WS-MAST-KEY
KEY = WS-...
"""
# 模式 1: KEY 变量出现在比较上下文中(= < > 后跟变量)
# 注意: 不能用 \s 代替 [=<>],否则「WS-KEY PIC」中的空格也会误匹配
if re.search(r'WS-[\w-]*KEY[A-Z0-9-]*\s*[=<>]', source_upper):
return True
# 模式 2: 非 WS- 前缀的 KEY 变量(旧式命名 K01-KEY 等)
if re.search(r'\b[A-Z]\d{0,2}-[\w-]*KEY\s*[=<>]', source_upper):
return True
# 模式 3: 源码中含有 READ INTO + KEY 变量
if re.search(r'READ\s+\w+\s+INTO\s+\w+.*KEY', source_upper, re.DOTALL):
return True
return False
def _get_procedure_division(source_upper: str) -> str:
"""只提取 PROCEDURE DIVISION 部分用于关键词匹配。"""
idx = source_upper.find('PROCEDURE DIVISION')
if idx >= 0:
return source_upper[idx:]
return source_upper
def detect_keyword(source: str) -> list[tuple[str, float, str]]:
"""在 COBOL 源码中搜索 L1_RULES 定义的关键字,返回匹配结果。
处理步骤:
1. 剥离注释,避免注释中的关键词触发匹配
2. 对需要程序上下文的关键词(マッチング),检查 KEY 变量是否在比较中使用
关键字前缀 "re:" 表示正则表达式匹配。
Args:
source: COBOL 程序源码文本。
Returns:
list[tuple[str, float, str]]:
每个元素为 (分类名称, 置信度, 匹配到的关键字原文)。
"""
cleaned = _strip_cobol_comments(source)
source_upper = cleaned.upper()
results: list[tuple[str, float, str]] = []
for category, keywords, confidence in L1_RULES:
matched = False
for kw in keywords:
if kw.startswith("re:"):
pattern = kw[3:]
if not re.search(pattern, source_upper):
continue
# マッチング 关键词需要额外上下文验证:KEY 变量必须在比较中使用
if category == "マッチング":
if not _matches_key_comparison(source_upper):
continue
results.append((category, confidence, kw))
matched = True
break
else:
if kw in source_upper:
results.append((category, confidence, kw))
matched = True
break
return results
# ── 确信度计算 ───────────────────────────────────────────────────────────
def compute_confidence(
source: str,
structure: dict[str, Any] | None = None,
llm_result: dict[str, Any] | None = None,
) -> dict[str, Any]:
"""计算程序分类的确信度。
优先级:
1. L1 关键字命中,且最高置信度 >= 0.90 → 直接返回 L1 结果。
2. LLM 结果存在 → 使用 LLM 的分类结果。
3. 否则 → 返回 unknown。
Args:
source: COBOL 程序源码文本。
structure: 可选的程序结构信息(暂未使用,保留扩展)。
llm_result: 可选的 LLM 分类结果。
预期格式: {"category": str, "confidence": float, ...}
Returns:
dict:
- "category": str — 分类名称或 "unknown"
- "confidence": float — 确信度 (0.0 ~ 1.0)
- "source": str — 结果来源 ("l1" / "llm" / "unknown")
- "matches": list — 匹配到的关键字详情
"""
# ── 1. L1 关键字检测 ──
matches = detect_keyword(source)
# 找出最高置信度的 L1 匹配
if matches:
best = max(matches, key=lambda m: m[1]) # (category, confidence, keyword)
category, confidence, _ = best
if confidence >= 0.90:
return {
"category": category,
"confidence": confidence,
"method": "keyword",
"source": "l1",
"features": [best[2]],
"required_tests": [],
"strategy_params": {"special_boundaries": [], "coverage_requirements": {"branch": 0.95, "paragraph": 1.0}},
"matches": matches,
}
# ── 2. LLM 结果 ──
if llm_result is not None:
llm_category = llm_result.get("category", "unknown")
llm_confidence = llm_result.get("confidence", 0.0)
return {
"category": llm_category,
"confidence": llm_confidence,
"method": "hybrid",
"source": "llm",
"features": [],
"required_tests": [],
"strategy_params": {"special_boundaries": [], "coverage_requirements": {"branch": 0.95, "paragraph": 1.0}},
"matches": matches,
}
# ── 3. 未知 ──
return {
"category": "unknown",
"confidence": 0.0,
"method": "none",
"source": "unknown",
"features": [],
"required_tests": [],
"strategy_params": {"special_boundaries": [], "coverage_requirements": {"branch": 0.95, "paragraph": 1.0}},
"matches": [],
}