fix(m02-df): 만나는 상자는 살아 있는 선이 있으면 셈 — 서버 = 화면

- 받는 상자는 들어오는 선이 모두 쉬는 갈래일 때만 쉼 · 하나라도 살아 있으면 셈(쉬는 갈래 값은 빈 칸) · 쉬는 선은 쉼 줄만
- 조건 선 바로 뒤 상자는 그대로 막음 · M02_DF_Compute.py 도 같은 뜻(resting · arrive)
- 시험: 지반 견본 토사 → 1㎥ 경비 2,000 · 암 → 브레이커 갈래 값 · 쉼 줄 · 문제 0 · 서버 = 화면(26개)

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 21:00:14 +09:00
co-authored by Claude Opus 5.5
parent 3eb18bf74b
commit 1dfe6bfefb
3 changed files with 96 additions and 22 deletions
+33 -4
View File
@@ -440,6 +440,9 @@ def _sheet_of(box: dict, arrived: dict, given: dict) -> dict:
return sheet
REST = "앞 조건이 안 맞아 쉬는 갈래"
def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | None = None) -> dict:
"""셈 — {ports, outputs, cost, errors, breaks, filled}(filled = 받는 칸에 값만 되돌려 넣음)."""
ctx = {"load": load, "local": local, "stack": stack or [lg.get("key") or ""]}
@@ -448,6 +451,7 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
name = lambda i: boxes.get(i, {}).get("name") or i # noqa: E731
errors, breaks, ports, arrived, outputs = [], [], {}, {}, {}
blocked: set[str] = set()
resting: set[str] = set() # 조건이 안 맞아 쉬는 상자 — Eval 과 같은 뜻
cost: dict[str, Any] = {**dict.fromkeys(COSTS, Decimal(0)), "값들": {}}
def through(ln: dict) -> Any:
@@ -464,13 +468,15 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
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 과 같음)
# 무게 — 통과 안 됨 = 쉼 · 나머지 = 문제(Eval CfBreak.level 과 같음)
"level": "쉼" if kind == "통과 안 됨" else "문제",
"box": to.get("box"),
"port": to.get("name"),
@@ -483,9 +489,32 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
return None if why else got
def arrive(box: dict) -> dict:
got = {
ln["to"].get("name"): through(ln) for ln in links if ln["to"].get("box") == box["id"]
}
"""받는 값 — 쉬는 갈래 빈 값은 「쉼」 줄만 · 선이 모두 쉬면 상자도 쉼(Eval 과 같음)."""
ins = [ln for ln in links if ln["to"].get("box") == box["id"]]
got, rest = {}, 0
for ln in ins:
src, to = ln.get("from") or {}, ln["to"]
if (
src.get("box") in resting
and ports.get(src.get("box"), {}).get(src.get("name")) is None
):
rest += 1
got[to.get("name")] = None
breaks.append(
{
"kind": "통과 안 됨",
"level": "쉼",
"box": to.get("box"),
"port": to.get("name"),
"link": ln.get("id"),
"text": f"「{name(src.get('box'))}」 → 「{name(to.get('box'))}」 — {REST}",
}
)
continue
got[to.get("name")] = through(ln)
if ins and rest == len(ins):
resting.add(box["id"])
blocked.add(box["id"])
arrived[box["id"]] = got
return got
+26 -18
View File
@@ -263,21 +263,6 @@ export function evalLogic(
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
const through = (l: CfLink): CfValue => {
const v = valueOf(l.from.box, l.from.name);
// 쉬는 갈래에서 온 빈 값 — 받는 상자도 쉼(셈하지 않음 · 문제로 안 셈)
if (v === null && resting.has(l.from.box)) {
resting.add(l.to.box);
blocked.add(l.to.box);
const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) });
found.push({
kind: "통과 안 됨",
level: "쉼",
box: l.to.box,
port: l.to.name,
link: l.id,
text,
});
return null;
}
if (!l.rule) return v;
if (v === null && (l.rule.kind === "scale" || l.rule.kind === "round")) return null;
const conds = Object.fromEntries(
@@ -311,10 +296,33 @@ export function evalLogic(
if (res.why) blocked.add(l.to.box);
return res.why ? null : res.value;
};
/** 받는 값 — 쉬는 갈래에서 온 빈 값은 「쉼」 줄만 남김 · 들어오는 선이 모두 쉬는 갈래면 받는 상자도 쉼
* (하나라도 살아 있으면 셈 · 쉬는 갈래 값은 빈 칸) · 조건 선 바로 뒤 상자는 through 가 막음 */
const arrive = (box: CfBox): Record<string, CfValue> => {
const got = Object.fromEntries(
lg.links.filter((l) => l.to.box === box.id).map((l) => [l.to.name, through(l)]),
);
const ins = lg.links.filter((l) => l.to.box === box.id);
const got: Record<string, CfValue> = {};
let rest = 0;
for (const l of ins) {
if (resting.has(l.from.box) && valueOf(l.from.box, l.from.name) === null) {
rest++;
got[l.to.name] = null;
const text = tl("Rest", { from: name(l.from.box), box: name(l.to.box) });
found.push({
kind: "통과 안 됨",
level: "쉼",
box: l.to.box,
port: l.to.name,
link: l.id,
text,
});
continue;
}
got[l.to.name] = through(l);
}
if (ins.length && rest === ins.length) {
resting.add(box.id);
blocked.add(box.id);
}
arrived[box.id] = got;
return got;
};
+37
View File
@@ -643,3 +643,40 @@ def test_쉬는_갈래와_문제_무게() -> None:
# 하나도 안 맞음 · 표에 없는 조건만 문제 — 쉬는 갈래 넷은 문제로 안 셈
assert b_problems == ["표에 없는 조건", "조건 안 맞음"]
assert outs["b7"] is None # 쉬는 갈래 끝 출력은 빈 값(0 아님)
def _ground() -> dict:
return json.loads((CF / "_sample" / "ground.json").read_text(encoding="utf-8"))
def _server(lg: dict, values: dict) -> dict:
from M02_MasterTemplete.DataFlow import M02_DF_Compute as cp
return cp.run(lg, values, lambda key: {}, [])
def test_만나는_상자는_살아_있는_선이_있으면_셈_서버와_같음() -> None:
lg = _ground()
soil = {"in-ground": {"지반": "토사"}}
rock = {"in-ground": {"지반": "암"}}
got = _run(
f"const lg = {json.dumps(lg, ensure_ascii=False)};\n"
f"const s = E.evalLogic(lg, {json.dumps(soil, ensure_ascii=False)});\n"
f"const r = E.evalLogic(lg, {json.dumps(rock, ensure_ascii=False)});\n"
"out([s.outputs, s.breaks.map(b => [b.kind, b.level, b.link]), r.outputs, r.breaks.map(b => [b.kind, b.level, b.link]),"
" s.ports['fn-sum'], r.ports['fn-sum']]);"
)
s_out, s_breaks, r_out, r_breaks, s_sum, r_sum = got
assert s_out == {"out-exp": 2000} # 굴삭기 갈래만 · 브레이커 갈래는 쉼(빈 칸)
assert s_breaks == [["통과 안 됨", "쉼", "l2"], ["통과 안 됨", "쉼", "l6"]]
assert [b for b in s_breaks if b[1] == "문제"] == []
assert r_breaks == [["통과 안 됨", "쉼", "l1"], ["통과 안 됨", "쉼", "l5"]]
assert r_out["out-exp"] == r_sum["1㎥ 경비"] and r_out["out-exp"] != 2000 # 브레이커 갈래 값
assert s_sum == {"1㎥ 경비": 2000}
# 서버(Compute) = 화면(Eval)
for values, screen_out, screen_breaks in ((soil, s_out, s_breaks), (rock, r_out, r_breaks)):
srv = _server(lg, values)
assert {k: float(v) for k, v in srv["outputs"].items()} == {
k: float(v) for k, v in screen_out.items()
}
assert [[b["kind"], b["level"], b["link"]] for b in srv["breaks"]] == screen_breaks