feat(m02-df): 36-2 옛 로직 → 흐름도 변환 · 표 찾기 옛 find 와 같은 뜻

- M02_DF_Convert.convert(row, ctx={key, file, master}) → {flow, notes} — 옛 파서 나무 → 함수 상자 시트(칸 주소 · 결과 하나) · 표 찾기 lookup · 로직 줄 비목 셋 · 덧줄 비목 합 · 줄.이름 · 결과 = 값 출력 · 자동 배치
- {입력} 낀 요소(EQ:{기계} · LB:{직종} · MO:경유:{지역} · 재료 검색어) = 조건 찾기 field 「키」(Model CfMaterialRule · Api slot 타입) · 자리 여럿은 글 잇는 함수
- 표 찾기: 빈 조건 칸 = 무엇이든 · 꺼낼 칸 빈 줄 건너뜀(Rule · Compute) · 함수 칸으로 가는 실패는 #N/A 를 넣어 넘김 · 결과가 오류일 때만 끊긴 곳(Eval · Compute 같음)
- 전수 대조 test_m02_df_convert(사본 폴더 1,472 로직 · 시험입력 92.9% · 기본 입력 94.3%) → tmp/convert_report_36.json

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VBToTCGk5dqDKeXCEP3vhk
This commit is contained in:
2026-10-01 23:57:58 +09:00
co-authored by Claude Opus 5.5
parent 9516116af5
commit 52a085eb0c
8 changed files with 837 additions and 36 deletions
+34 -24
View File
@@ -314,7 +314,8 @@ def _rounded(value: Any, r: dict) -> Any:
def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나."""
"""표에서 찾기 — 같음 · 범위(아래 < 값 ≤ 위 · 빈 끝 열림 · 아래 = 위면 같음) · 맞은 줄 하나
· 빈 조건 칸 = 무엇이든 · 꺼낼 칸이 빈 줄은 뺌(옛 엔진 find 와 같음 · Rule lookup 과 같음)."""
if not isinstance(block, list) or len(block) < 2:
return None, "오류"
head = [str(h if h is not None else "") for h in block[0]]
@@ -326,7 +327,7 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
col, want = str(c.get("col")), _cond_of(conds, c.get("from") or {})
if col in head:
i = head.index(col)
tests.append(lambda row, i=i, want=want: _same(row[i], want))
tests.append(lambda row, i=i, want=want: row[i] is None or _same(row[i], want))
elif col + dv.RANGE_LOW in head and col + dv.RANGE_HIGH in head:
lo, hi, x = (
head.index(col + dv.RANGE_LOW),
@@ -347,13 +348,14 @@ def _lookup(rule: dict, block: Any, conds: dict) -> tuple[Any, str | None]:
tests.append(fits)
else:
return None, "오류"
hits = [row for row in block[1:] if all(t(list(row) + [None] * len(head)) for t in tests)]
at = head.index(out)
rows = [list(r) + [None] * len(head) for r in block[1:]]
hits = [r for r in rows if r[at] not in (None, "") and all(t(r) for t in tests)]
if not hits:
return None, "표에 없는 조건"
if len(hits) > 1:
return None, "표에 여럿 맞음"
value = hits[0][head.index(out)]
return (None, "오류") if value is None else (value, None)
return hits[0][at], None
def apply_rule(rule: dict, value: Any, conds: dict) -> tuple[Any, str | None]:
@@ -446,11 +448,8 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
for p in func.get("inputs") or []:
if str(p.get("cell") or "").strip():
port = p.get("port")
_place(
cells,
p["cell"],
arrived[port] if arrived.get(port) is not None else given.get(port),
)
v = arrived[port] if arrived.get(port) is not None else given.get(port)
_place(cells, p["cell"], {"식": "NA()"} if v == NA_VALUE else v)
for v in func.get("variables") or []:
if v.get("cell"):
_place(cells, v["cell"], given.get(v.get("name"), v.get("example")))
@@ -458,6 +457,7 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
REST = "앞 조건이 안 맞아 쉬는 갈래"
NA_VALUE = "#N/A" # 함수 칸으로 가는 표 찾기 실패 — 칸에 NA() · 결과 오류일 때만 끊긴 곳(Eval 같음)
def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict:
@@ -469,6 +469,7 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
errors, breaks, ports, arrived, outputs = [], [], {}, {}, {}
blocked: set[str] = set()
resting: set[str] = set() # 조건이 안 맞아 쉬는 상자 — Eval 과 같은 뜻
late: dict[str, list[dict]] = {} # 함수로 가는 표 찾기 실패 — 결과가 오류일 때만 끊긴 곳
cost: dict[str, Any] = {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}}
def through(ln: dict) -> Any:
@@ -486,23 +487,28 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
conds[f"{p.get('box')}|{p.get('name')}"] = got_v
got, why = apply_rule(rule, v, conds)
if why:
blocked.add(ln["to"].get("box"))
if why == "통과 안 됨":
resting.add(ln["to"].get("box"))
text = f"「{name(src.get('box'))}」 → 「{name(ln['to'].get('box'))}」 {why}"
kind = "오류" if why in ("표에 여럿 맞음", "오류") else why
to = ln["to"]
breaks.append(
{
"kind": kind,
# 무게 — 통과 안 됨 = 쉼 · 나머지 = 문제(Eval CfBreak.level 과 같음)
"level": "쉼" if kind == "통과 안 됨" else "문제",
"box": to.get("box"),
"port": to.get("name"),
"link": ln.get("id"),
"text": text,
}
)
brk = {
"kind": kind,
# 무게 — 통과 안 됨 = 쉼 · 나머지 = 문제(Eval CfBreak.level 과 같음)
"level": "쉼" if kind == "통과 안 됨" else "문제",
"box": to.get("box"),
"port": to.get("name"),
"link": ln.get("id"),
"text": text,
}
if (
rule.get("kind") == "lookup"
and boxes.get(to.get("box"), {}).get("kind") == "function"
):
late.setdefault(to.get("box"), []).append(brk)
return NA_VALUE
blocked.add(to.get("box"))
if why == "통과 안 됨":
resting.add(to.get("box"))
breaks.append(brk)
if why != "통과 안 됨":
errors.append(text)
return None if why else got
@@ -582,6 +588,10 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
if out.get("round") and _num_of(value) is not None:
value = _rounded(value, out["round"])
ports[f["id"]] = {out.get("name"): value}
if value is None or (isinstance(hit, dict) and "오류" in hit):
for brk in late.get(f["id"], []):
breaks.append(brk)
errors.append(brk["text"])
for b in boxes.values(): # 출력 상자 · 원가(Eval 과 같음)
if b.get("kind") != "output":
continue