Merge remote-tracking branches 'origin/sub_laptop_2', 'origin/sub_laptop_5' and 'origin/sub_laptop_8' into main_laptop_1

This commit is contained in:
2026-10-01 20:49:09 +09:00
7 changed files with 136 additions and 37 deletions
@@ -470,6 +470,8 @@ def run(lg: dict, values: dict, load: Loader, local: list[dict], stack: list | N
breaks.append(
{
"kind": kind,
# 무게 — 통과 안 됨 = 쉼(조건이 안 맞아 쉬는 갈래) · 나머지 = 문제(Eval CfBreak.level 과 같음)
"level": "쉼" if kind == "통과 안 됨" else "문제",
"box": to.get("box"),
"port": to.get("name"),
"link": ln.get("id"),
@@ -284,9 +284,8 @@ export function depInTrace(dep: DepLine, steps: CfStep[]): boolean {
return src && dst;
}
/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — level 이 없으면 「통과 안 됨」 을 쉼으로 */
export const isRest = (k: CfBreak): boolean =>
((k as CfBreak & { level?: string }).level ?? (k.kind === "통과 안 됨" ? "쉼" : "문제")) === "쉼";
/** 끊긴 곳 갈래 — 쉼(조건이 안 맞아 이번엔 안 지나감 · 문제 아님) · 문제 — Eval 의 level 그대로 */
export const isRest = (k: CfBreak): boolean => k.level === "쉼";
/* ── 끌기 손잡이 ─────────────────────────────────────────────────────── */
+38 -4
View File
@@ -43,6 +43,8 @@ export interface CfBreak {
| "조건 겹침"
| "조건 안 맞음"
| "오류";
/** 무게 — 「쉼」 = 조건이 안 맞아 쉬는 갈래(그 아래로 번진 빈 값 포함) · 「문제」 = 고쳐야 할 끊김 */
level: "문제" | "쉼";
box: string;
port?: string;
link?: string;
@@ -112,6 +114,11 @@ const TEXT = {
"「{from}」 → 「{box}」 선 — 표에 맞는 줄이 여럿",
"Line 「{from}」 → 「{box}」 — several table rows match",
],
/** {from} {box} */
Rest: [
"「{from}」 → 「{box}」 — 앞 조건이 안 맞아 쉬는 갈래",
"「{from}」 → 「{box}」 — resting branch (an earlier condition doesn't match)",
],
RuleError: [
"「{from}」 → 「{box}」 선 규칙을 셀 수 없음",
"Line 「{from}」 → 「{box}」 rule can't be worked out",
@@ -251,9 +258,26 @@ export function evalLogic(
const passed = new Map<string, boolean>();
/** 선 규칙에서 막힌 상자 — 그 함수는 셈하지 않고 빈 값을 넘김(갈래가 끊김) */
const blocked = new Set<string>();
/** 조건이 안 맞아 쉬는 상자 — 그 아래로 번짐 */
const resting = new Set<string>();
/** 선 하나로 들어오는 값 — 규칙이 있으면 엔진으로 */
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(
@@ -262,15 +286,17 @@ export function evalLogic(
const res = applyRule(l.rule, v, conds);
if (l.rule.kind === "gate") passed.set(l.id, res.why === null);
const fill = { from: name(l.from.box), box: name(l.to.box), rule: ruleLabel(l.rule) };
if (res.why === "통과 안 됨")
if (res.why === "통과 안 됨") {
resting.add(l.to.box);
found.push({
kind: "통과 안 됨",
level: "쉼",
box: l.to.box,
port: l.to.name,
link: l.id,
text: tl("NotPassed", fill),
});
else if (res.why) {
} else if (res.why) {
const kind = res.why === "표에 없는 조건" ? "표에 없는 조건" : "오류";
const key =
res.why === "표에 없는 조건"
@@ -279,7 +305,7 @@ export function evalLogic(
? "ManyRows"
: "RuleError";
const text = tl(key, fill);
found.push({ kind, box: l.to.box, port: l.to.name, link: l.id, text });
found.push({ kind, level: "문제", box: l.to.box, port: l.to.name, link: l.id, text });
errors.push(text);
}
if (res.why) blocked.add(l.to.box);
@@ -303,7 +329,13 @@ export function evalLogic(
for (const e of b.input?.elements ?? [])
if (e.mode === "rule" && ports[b.id][elementPort(e)] === null) {
const text = tl("NoMaterial", { box: b.name, pill: elementPort(e) });
found.push({ kind: "맞는 재료 없음", box: b.id, port: elementPort(e), text });
found.push({
kind: "맞는 재료 없음",
level: "문제",
box: b.id,
port: elementPort(e),
text,
});
errors.push(text);
}
} else if (b.kind === "input") ports[b.id] = otherLogic(b, arrive(b), given, opts, errors);
@@ -383,6 +415,7 @@ function gates(
const text = tl(n > 1 ? "Overlap" : "NoneMatch", fill);
found.push({
kind: n > 1 ? "조건 겹침" : "조건 안 맞음",
level: "문제",
box: g.from.box,
port: g.from.name,
text,
@@ -402,6 +435,7 @@ export function breaks(lg: CfLogic, values: CfValues = {}, opts: CfEvalOptions =
if (!lg.links.some((l) => l.to.box === b.id && l.to.name === p.name))
out.push({
kind: "빈 받는 자리",
level: "문제",
box: b.id,
port: p.name,
text: tl("EmptyIn", { box: b.name, pill: p.name }),
@@ -116,3 +116,15 @@
color: var(--color-danger);
font-size: var(--text-caption);
}
/* 쉬는 갈래 — 문제가 아니라 회색으로 */
.m02-dfver__rest {
padding: var(--spacing-8) var(--spacing-12);
border-left: 4px solid var(--color-border);
background: var(--color-surface);
color: var(--color-text-muted);
}
.m02-dfver__rest .ui-btn {
color: var(--color-text-muted);
}
+40 -23
View File
@@ -24,6 +24,11 @@ import "./M02_DF_Verify.css";
const TEXT = {
Breaks: ["끊긴 곳", "Broken spots"],
BreaksHint: ["누르면 그 상자 · 선을 고름", "Click to select the box or line"],
Rest: ["쉬는 갈래", "Resting branches"],
RestHint: [
"조건이 안 맞아 쉬는 곳 — 누르면 그 선을 고름",
"Idle because a condition is not met — click to select the line",
],
Design: ["설계 때 정할 것", "To decide at design time"],
DesignHint: ["여기서 바꾸면 그 상자도 바뀜", "Changing here changes the box too"],
Vars: ["변수 예시값", "Variable samples"],
@@ -333,33 +338,45 @@ export function renderVerify(
/* 끊긴 곳 */
const spots = breaks(lg, opts.values ?? {}, { logics: opts.logics });
const spotBlock = spots.length
? [
el("div", {
className: "m02-dfver__block m02-dfver__breaks",
const spotList = (list: typeof spots, hint: TextKey): HTMLElement[] => [
el("ul", {
className: "m02-dfver__list",
children: list.map((s) =>
el("li", {
children: [
el("strong", { text: tv("Breaks") }),
el("ul", {
className: "m02-dfver__list",
children: spots.map((s) =>
el("li", {
children: [
createButton({
label: s.text,
variant: "ghost",
size: "sm",
disabled: !s.box || !on.select,
onClick: () => on.select?.({ box: s.box, port: s.port, link: s.link }),
}),
],
}),
),
createButton({
label: s.text,
variant: "ghost",
size: "sm",
disabled: !s.box || !on.select,
onClick: () => on.select?.({ box: s.box, port: s.port, link: s.link }),
}),
el("p", { className: "m02-dfver__muted", text: tv("BreaksHint") }),
],
}),
]
: [];
),
}),
el("p", { className: "m02-dfver__muted", text: tv(hint) }),
];
const problems = spots.filter((s) => s.level !== "쉼");
const resting = spots.filter((s) => s.level === "쉼");
const spotBlock = [
...(problems.length
? [
el("div", {
className: "m02-dfver__block m02-dfver__breaks",
children: [el("strong", { text: tv("Breaks") }), ...spotList(problems, "BreaksHint")],
}),
]
: []),
...(resting.length
? [
el("div", {
className: "m02-dfver__block m02-dfver__rest",
children: [el("strong", { text: tv("Rest") }), ...spotList(resting, "RestHint")],
}),
]
: []),
];
/* 설계 때 정할 것 — Model `designInputs`(변수 · 재료 자리 · 다른 로직) + 재료 조건 자리 */
const designs = designInputs(lg);
+16 -4
View File
@@ -203,10 +203,22 @@ def test_쉼_가르기():
{
"op": "rest",
"breaks": [
{"kind": "통과 안 됨", "box": "f", "link": "l2", "text": "지반이 암이 아님"},
{"kind": "빈 받는 자리", "box": "f", "port": "품", "text": "비어 있음"},
{"kind": "통과 안 됨", "box": "f", "level": "문제", "text": "level 이 먼저"},
{"kind": "조건 겹침", "box": "f", "level": "쉼", "text": "level 이 먼저"},
{
"kind": "통과 안 됨",
"level": "쉼",
"box": "f",
"link": "l2",
"text": "지반이 암이 아님",
},
{
"kind": "빈 받는 자리",
"level": "문제",
"box": "f",
"port": "품",
"text": "비어 있음",
},
{"kind": "통과 안 됨", "level": "문제", "box": "f", "text": "level 이 정함"},
{"kind": "조건 겹침", "level": "쉼", "box": "f", "text": "level 이 정함"},
],
}
]
+26 -3
View File
@@ -450,16 +450,17 @@ def test_나_조건_통과로_둘로_가르기_표에서_찾기() -> None:
a_out, a_breaks, b_out, b_breaks, far = got
# 토사: 0.5 × 100000 × 1.1 · 암 줄은 통과 안 됨 · 높이 4.5 → 증가율 40
assert a_out == {"b6": 55000, "b7": None, "b8": 40}
assert a_breaks == [["통과 안 됨", "l2"]]
assert a_breaks == [["통과 안 됨", "l2"], ["통과 안 됨", "l5"]] # 암 갈래 + 그 아래로 번진 쉼
assert b_out == {
"b6": None,
"b7": 50000,
"b8": 30,
} # 암 50,000 → 천 원 버림 그대로 · 높이 3.5 → 30
assert b_breaks == ["통과 안 됨"]
assert b_breaks == ["통과 안 됨", "통과 안 됨"] # 토사 갈래 + 번진 쉼
assert far == [
["통과 안 됨", "「입력」 → 「함수 2」 선이 조건(지반 = 암 일 때만)을 통과하지 않음"],
["표에 없는 조건", "「입력 2」 → 「함수 3」 선 — 표에 맞는 줄이 없음"],
["통과 안 됨", "「함수 2」 → 「출력 2」 — 앞 조건이 안 맞아 쉬는 갈래"],
]
@@ -471,7 +472,7 @@ def test_나_조건_선_겹침_안맞음() -> None:
)
overlap, none = got
assert overlap == [["조건 겹침", "「입력」 의 「지반」 에서 나간 조건 선이 둘 이상 통과함"]]
assert none == ["통과 안 됨", "통과 안 됨", "조건 안 맞음"]
assert none == ["통과 안 됨", "통과 안 됨", "통과 안 됨", "통과 안 됨", "조건 안 맞음"]
def test_나_규칙_글과_엑셀_수식() -> None:
@@ -620,3 +621,25 @@ def test_표_범위칸_숫자하나는_같음() -> None:
)
# 4 = 「4」 줄만(같음) · 4 는 「4~6」(초과) 에 안 듦 · 빈 아래 끝은 열림
assert got == [[0.5, None], [0.6, None], [0.4, None], [None, "표에 없는 조건"], [0.6, None]]
def test_쉬는_갈래와_문제_무게() -> None:
got = _run(
SPLIT + "const a = E.breaks(lg, V('토사'));\n"
"const b = E.breaks(lg, V('자갈', 9));\n"
"out([a.map(x => [x.kind, x.level, x.box, x.text]), b.filter(x => x.level === '문제').map(x => x.kind),"
" E.evalLogic(lg, V('토사')).outputs]);"
)
a, b_problems, outs = got
assert a == [
[
"통과 안 됨",
"쉼",
"b4",
"「입력」 → 「함수 2」 선이 조건(지반 = 암 일 때만)을 통과하지 않음",
],
["통과 안 됨", "쉼", "b7", "「함수 2」 → 「출력 2」 — 앞 조건이 안 맞아 쉬는 갈래"],
]
# 하나도 안 맞음 · 표에 없는 조건만 문제 — 쉬는 갈래 넷은 문제로 안 셈
assert b_problems == ["표에 없는 조건", "조건 안 맞음"]
assert outs["b7"] is None # 쉬는 갈래 끝 출력은 빈 값(0 아님)