chore(rebar): Pier_T_rebar 작업 폴더 초기 구성 및 외부 의존 파일 통합
- Code\rebar_pipeline, review_pipeline, pier-program을 정션에서 실제 폴더로 전환 - Code\rhino_mcp 및 CLAUDE_rhino배경.md 추가, .mcp.json은 PYTHONPATH로 새 위치 사용 - pier-program 내부 .git 제거(이력은 backup\pier-program_20261028.bundle에 보관) - worktree 가이드 반영(CLAUDE.local.md, HANDOFF.md) - D:\Rhino-MCP 경로 참조를 새 위치로 치환 Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
964a841de1
3978 files changed
+58013356
No files matched your search
@@ -0,0 +1,416 @@
|
||||
"""App check ("앱 확인") of the input bundles — stage B of the 입력 묶음 확인·확정 개편 (2026-10-16).
|
||||
|
||||
The app compares every bundle of params.json / expected.json with what it can read independently and reports, per bundle,
|
||||
one of: g 일치 / w 주의 / b 불일치 / n 근거 없음 / x 해당 없음. The app never confirms anything: a person confirms (D1), the app only
|
||||
supplies the evidence. "일치" means "the independent source and the value agree", not "the drawing is right" (기획서 4.3).
|
||||
|
||||
Checks (C1~C8, 기획서 4.3):
|
||||
C1 출처 있음 the bundle (or its `<key>_src` sibling) records where its values come from
|
||||
C2 도면 재대조 table candidates (ELEVATION / 솔플레이트 / 철근재료표) read from the drawing texts vs the current values [strong]
|
||||
C2b 표 밖 치수 재대조 anchors followed into the drawing (engine/anchors.py): A candidates equal / differ, B only warn; own-drawing anchors are no evidence [strong]
|
||||
C3 템플릿 원본 잔존 the bundle still equals the protected template's original and nothing independent supports it
|
||||
C4 내부 정합 bar counts of the generated lists vs the schedule counts in expected [counts only]
|
||||
C5 params ↔ expected 독립 the expected source notes are not copies of the params notes
|
||||
C6 범위·단위 sizes > 0, EL / z in a plausible range, no NaN
|
||||
C7 변수 등급 위반 F bundles / fixed fields unchanged against the template original (or an edit reason is logged)
|
||||
C8 재생성 일치 V3 bundles: regenerated list equals the current one — no regeneration script is connected yet (해당 없음, 독립성 낮음)
|
||||
C9 params↔expected 일관성 values that describe the same quantity in both files agree (engine/consistency.py; 2026-10-20) — finds stale values before a gate is run;
|
||||
not independent evidence (b when they differ, g otherwise, never a strength)
|
||||
C3b 필드 단위 원본 잔존 fields of a CHANGED bundle that still equal the pier's creation snapshot while elevation-like siblings changed (origin_values.json; 2026-10-20)
|
||||
Status per bundle: any failed check -> b; template original without independent support -> n; strong support (C2) -> g (w when a check
|
||||
warns); counts only -> w ("수량만"); F bundle unchanged -> g; nothing -> n; reference-only / human decision records -> x.
|
||||
Everything is derived from the data files and the drawing text index; no gate is run.
|
||||
"""
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
||||
from engine import consistency, reader, variability
|
||||
|
||||
VERSION = "B3"
|
||||
ST_NAME = {"g": "일치", "w": "주의", "b": "불일치", "n": "근거 없음", "x": "해당 없음"}
|
||||
DECISION_KEYS = {("expected", "planned_fail"), ("expected", "rebar_palette"), ("expected", "tol_mm")}
|
||||
SIZE_KEYS = re.compile(r"^(size_[xy]|thickness|diameter|height|length|width_y|plan_length|top_length|half|count_[xy]|nx|ny|dia|"
|
||||
r"half_.*|bottom_flat_half|tip_half|top_half|soffit_rise)$")
|
||||
SIZE_IN = {"d": ("column", "piles"), "dx": ("footing", "lean"), "dy": ("footing", "lean")} # short names only where they are sizes
|
||||
NOT_SIZE = ("translation_checks", "pitch", "negative", "chain") # directional / check values may be negative or 0
|
||||
Z_KEYS = re.compile(r"^(z|z_.*|el_x|z0|seat_z)$")
|
||||
Z_RANGE = (-20000.0, 400000.0) # mm, EL -20 m .. 400 m
|
||||
|
||||
|
||||
def chk(cid, name, st, text, pos="", strength=""):
|
||||
return {"id": cid, "name": name, "st": st, "text": text, "pos": pos, "strength": strength}
|
||||
|
||||
|
||||
def _walk(v, path=()):
|
||||
"""(path, leaf) of every scalar in a nested value."""
|
||||
if isinstance(v, dict):
|
||||
for k, x in v.items():
|
||||
yield from _walk(x, path + (str(k),))
|
||||
elif isinstance(v, list):
|
||||
for i, x in enumerate(v):
|
||||
yield from _walk(x, path + (str(i),))
|
||||
else:
|
||||
yield path, v
|
||||
|
||||
|
||||
def _src_texts(v):
|
||||
"""All source notes (`src`, `*_src`, `sources`) inside a bundle value, flattened to strings."""
|
||||
out = []
|
||||
|
||||
def rec(x, key=""):
|
||||
if isinstance(x, dict):
|
||||
for k, y in x.items():
|
||||
rec(y, k)
|
||||
elif isinstance(x, list):
|
||||
for y in x:
|
||||
rec(y, key)
|
||||
elif isinstance(x, str) and (key == "src" or key.endswith("_src") or key == "sources") and x.strip():
|
||||
out.append(x.strip())
|
||||
rec(v)
|
||||
return out
|
||||
|
||||
|
||||
def _count_bars(value, mark):
|
||||
return sum(1 for b in value.get("bars", []) if b.get("mark") == mark)
|
||||
|
||||
|
||||
def _planned(expected, gate, mark, what="bar count vs schedule"):
|
||||
for pf in expected.get("planned_fail", []):
|
||||
if pf.get("gate") == gate and pf.get("item", "").startswith(f"{mark} {what}"):
|
||||
return pf
|
||||
return None
|
||||
|
||||
|
||||
def count_pairs(params, expected):
|
||||
"""Counts derived from the generated rebar lists vs the schedule counts in expected: {gate: [(label, mark, got, want)]}."""
|
||||
out = {"G2": [], "G3": [], "G4": []}
|
||||
cr = params.get("column_rebar") or {}
|
||||
sn = (expected.get("G2") or {}).get("schedule_n")
|
||||
if cr.get("form") == "ring" and isinstance(sn, dict):
|
||||
try:
|
||||
out["G2"].append(("주철근", cr["main"]["mark"], cr["main"]["count"], sn.get(cr["main"]["mark"])))
|
||||
for z in cr["hoops"]["zones"]:
|
||||
out["G2"].append(("띠철근", z["mark"], len(z["levels"]), sn.get(z["mark"])))
|
||||
t = cr["ties"]
|
||||
out["G2"].append(("크로스타이", t["mark"], len(t["levels"]) * len(t["angles_deg"]), sn.get(t["mark"])))
|
||||
except (KeyError, TypeError):
|
||||
out["G2"] = []
|
||||
for gate, key in (("G3", "footing_rebar"), ("G4", "coping_rebar")):
|
||||
fr = params.get(key) or {}
|
||||
sch = (expected.get(gate) or {}).get("schedule")
|
||||
if not isinstance(sch, dict) or not fr:
|
||||
continue
|
||||
if fr.get("form") == "polylines":
|
||||
for m, v in sch.items():
|
||||
if isinstance(v, list) and len(v) == 2 and m in {b.get("mark") for b in fr.get("bars", [])} | set(sch):
|
||||
out[gate].append(("철근 수", m, _count_bars(fr, m), v[1]))
|
||||
elif fr.get("form") == "mats" and gate == "G3":
|
||||
for g in fr.get("groups", []):
|
||||
out[gate].append(("위치 목록", g["mark"], len(g["pos"]), (sch.get(g["mark"]) or [None, None])[1]))
|
||||
h, v = fr.get("hoops"), fr.get("verticals")
|
||||
if h:
|
||||
out[gate].append(("띠철근 단 수", h["mark"], len(h["levels"]), (sch.get(h["mark"]) or [None, None])[1]))
|
||||
if v:
|
||||
n = len(v["points"]) if "points" in v else sum(1 for i, _ in enumerate(v["grid_x"]) for j, _ in enumerate(v["grid_y"]) if (i + j) % 2 == v["parity"])
|
||||
out[gate].append(("수직근", v["mark"], n, (sch.get(v["mark"]) or [None, None])[1]))
|
||||
return {g: p for g, p in out.items() if p}
|
||||
|
||||
|
||||
def bundles_of_gate(gate):
|
||||
return {"G2": [("params", "column_rebar"), ("expected", "G2")], "G3": [("params", "footing_rebar"), ("expected", "G3")],
|
||||
"G4": [("params", "coping_rebar"), ("expected", "G4")]}[gate]
|
||||
|
||||
|
||||
def anchor_check(rows, missing, key_prefix):
|
||||
"""C2b of one bundle from the anchor rows of its fields (rows of engine.anchors.follow joined with the current values)."""
|
||||
mine = [r for r in rows if r["field"].startswith(key_prefix) and not r["self_source"]]
|
||||
miss = [f for f in missing if f.startswith(key_prefix)]
|
||||
if not mine:
|
||||
return None
|
||||
a_diff = [r for r in mine if r["grade"] == "A" and not r["same"]]
|
||||
b_diff = [r for r in mine if r["grade"] == "B" and not r["same"]]
|
||||
a_same = [r for r in mine if r["grade"] == "A" and r["same"]]
|
||||
b_same = [r for r in mine if r["grade"] == "B" and r["same"]]
|
||||
anchored = len(mine) + len(miss)
|
||||
cover = (len(a_same) + len(b_same)) / anchored if anchored else 0
|
||||
if a_diff:
|
||||
r = a_diff[0]
|
||||
d = "; ".join(f"{x['field'].split(':', 1)[1]}: 현재 {x['current']} ≠ 도면 {x['value']} ('{x['text']}' {x['dwg']})" for x in a_diff[:3])
|
||||
return chk("C2b", "표 밖 치수 재대조", "b", f"A등급 후보 {len(a_diff)}건이 현재 값과 다르다 — {d}", pos=f"{r['dwg']} ({r['x']:.0f}, {r['y']:.0f})", strength="S")
|
||||
if b_diff:
|
||||
r = b_diff[0]
|
||||
d = "; ".join(f"{x['field'].split(':', 1)[1]}: 현재 {x['current']} ≠ 도면 {x['value']}" for x in b_diff[:3])
|
||||
return chk("C2b", "표 밖 치수 재대조", "w", f"B등급 후보 {len(b_diff)}건이 현재 값과 다르다(후보가 틀렸을 수도 있다) — {d}", pos=f"{r['dwg']} ({r['x']:.0f}, {r['y']:.0f})")
|
||||
if a_same and cover >= 0.6:
|
||||
r = a_same[0]
|
||||
return chk("C2b", "표 밖 치수 재대조", "g", f"A등급 {len(a_same)}건·B등급 {len(b_same)}건이 현재 값과 같다 (앵커가 있는 {anchored}개 필드의 {cover:.0%})",
|
||||
pos=f"{r['dwg']} '{r['text']}' ({r['x']:.0f}, {r['y']:.0f})", strength="S")
|
||||
if a_same or b_same:
|
||||
return chk("C2b", "표 밖 치수 재대조", "w", f"앵커 필드 {anchored}개 중 {len(a_same) + len(b_same)}개만 같다 (A {len(a_same)} · B {len(b_same)}) — 대조 범위 {cover:.0%}")
|
||||
return None
|
||||
|
||||
|
||||
HEIGHT_TOKENS = {"z", "z0", "z_bottom", "z_top", "z_flat", "el_x", "datum_el", "step_top", "tier_z", "loop_z", "levels", "hook_leg_z", "straight_from",
|
||||
"c10_bottom", "c15_top", "c16_top", "top_right", "top_left", "tiers"}
|
||||
|
||||
|
||||
def height_like(path):
|
||||
"""True for a leaf that is an elevation (EL in mm): z-like keys, level lists, the z of a point [x, z], the z of an outline point."""
|
||||
last = path[-1]
|
||||
if last in HEIGHT_TOKENS or last.startswith("z_") or last.endswith(("_z", "_top", "_bottom")):
|
||||
return True
|
||||
if any(t in ("step_top", "tier_z", "loop_z", "levels") for t in path):
|
||||
return True
|
||||
if last == "1" and any(t in ("points", "hooks_up", "coping_outline", "top_right", "top_left") for t in path):
|
||||
return True
|
||||
if last == "0" and "c14_levels" in path:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
EL_MIN, EL_MAX = 10000.0, 60000.0 # mm: elevation-like numbers (EL 10 m .. 60 m)
|
||||
STALE_MIN_CHANGED = 3 # at least this many elevation-like siblings changed against the snapshot
|
||||
|
||||
|
||||
def stale_fields_check(which, key, value, origin_values, m):
|
||||
"""C3b: in a bundle that was changed against the pier's creation snapshot, elevation-like numbers that still equal the snapshot value are
|
||||
suspicious (the drawing moved the other heights but this one was left behind, e.g. a P2 coordinate in a P3 point list). Warns (w), never fails."""
|
||||
if not origin_values or which not in origin_values or key not in origin_values[which]:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "x", "교각 생성 시점의 값 기록(origin_values.json)이 없어 필드 단위로 비교하지 못했다")
|
||||
old = dict(_walk(origin_values[which][key]))
|
||||
new = dict(_walk(value))
|
||||
el = [p for p, v in new.items() if isinstance(v, (int, float)) and not isinstance(v, bool) and EL_MIN <= abs(v) <= EL_MAX and p in old and height_like(p)]
|
||||
if not el or old == new:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "x" if old == new else "g", "묶음 전체가 생성 시점 그대로라 C3에서 본다" if old == new else "높이로 보이는 값이 없다")
|
||||
changed = [p for p in el if old[p] != new[p]]
|
||||
kept = [p for p in el if old[p] == new[p]]
|
||||
if len(changed) < STALE_MIN_CHANGED:
|
||||
return chk("C3b", "필드 단위 원본 잔존", "g", f"높이로 보이는 값 {len(el)}개 중 바뀐 것이 {len(changed)}개뿐이라 비교하지 않는다")
|
||||
if kept:
|
||||
names = ", ".join(".".join(p) + f" = {new[p]:g}" for p in kept[:4])
|
||||
return chk("C3b", "필드 단위 원본 잔존", "w", f"바뀐 묶음인데 높이로 보이는 값 {len(el)}개 중 {len(kept)}개가 생성 시점 값 그대로다 — 도면과 대조: {names}")
|
||||
return chk("C3b", "필드 단위 원본 잔존", "g", f"높이로 보이는 값 {len(el)}개가 모두 생성 시점과 다르다")
|
||||
|
||||
|
||||
def verify(params, expected, texts, meta, origin_fixed=None, edit_log=None, current_fixed=None, template_values=None, anchor_rows=None, origin_values=None):
|
||||
"""-> {"params:key": {"st", "checks", "summary"}} for every bundle.
|
||||
meta[bundle] = variability.classify result + "origin" (True / False / None); origin_fixed[bundle] = fixed_view at copy time."""
|
||||
origin_fixed = origin_fixed or {}
|
||||
edit_log = edit_log or []
|
||||
template_values = template_values or {}
|
||||
arows, amiss = (anchor_rows or ([], []))[:2]
|
||||
docs = {"params": params, "expected": expected}
|
||||
# ---- C2: table candidates of the drawing texts vs the current values
|
||||
c2 = {}
|
||||
tables_ok = False
|
||||
if texts:
|
||||
tabs = reader.table_candidates(texts)
|
||||
cands = reader.field_candidates(tabs)
|
||||
tables_ok = bool(cands)
|
||||
for r in reader.proposals(cands, params, expected):
|
||||
c2.setdefault(f"{r['file']}:{r['path'][0]}", []).append(r)
|
||||
# ---- C9: params <-> expected consistency (rules of engine/consistency.py), per bundle of either file
|
||||
cons = consistency.check(params, expected)
|
||||
cons_by = {}
|
||||
for r in cons:
|
||||
cons_by.setdefault("params:" + consistency.bundles_of(r["p"]), []).append(r)
|
||||
cons_by.setdefault("expected:" + consistency.bundles_of(r["e"]), []).append(r)
|
||||
# ---- C4: counts per gate pair
|
||||
pairs = count_pairs(params, expected)
|
||||
out = {}
|
||||
for which in ("params", "expected"):
|
||||
for key, value in docs[which].items():
|
||||
b = f"{which}:{key}"
|
||||
m = meta.get(b) or variability.classify(which, key, value)
|
||||
checks = []
|
||||
if m.get("info"):
|
||||
out[b] = {"st": "x", "checks": [chk("C1", "참고 정보", "x", "게이트가 쓰지 않는 참고 정보라 확인하지 않는다")], "summary": "참고 정보"}
|
||||
continue
|
||||
# C1
|
||||
srcs = _src_texts(value) + (_src_texts(params.get(key + "_src")) if which == "params" and key + "_src" in params else [])
|
||||
if key + "_src" in docs[which] and isinstance(docs[which][key + "_src"], str):
|
||||
srcs.append(docs[which][key + "_src"])
|
||||
if m["grade"] == "F":
|
||||
checks.append(chk("C1", "출처 있음", "x", "고정(F) 항목: 값은 템플릿이 정한다"))
|
||||
elif srcs:
|
||||
checks.append(chk("C1", "출처 있음", "g", f"값의 출처 기록이 {len(srcs)}건 있다", pos=srcs[0][:80]))
|
||||
else:
|
||||
checks.append(chk("C1", "출처 있음", "n", "값의 출처 기록이 없다 (일반 출처: " + " · ".join(m["source"]) + ")"))
|
||||
# C2
|
||||
rows = c2.get(b)
|
||||
if rows:
|
||||
bad = [r for r in rows if not r["same"]]
|
||||
src0 = rows[0]["src"]
|
||||
if bad:
|
||||
d = "; ".join(f"{'.'.join(str(x) for x in r['path'])}: 현재 {r['current']} ≠ 표 {r['candidate']}" for r in bad[:3])
|
||||
checks.append(chk("C2", "도면 재대조", "b", f"표 후보 {len(rows)}건 중 {len(bad)}건이 다르다 — {d}", pos=bad[0]["src"], strength="S"))
|
||||
else:
|
||||
checks.append(chk("C2", "도면 재대조", "g", f"표 후보 {len(rows)}건이 현재 값과 모두 같다", pos=src0, strength="S"))
|
||||
elif not texts and "표" in m["source"]:
|
||||
checks.append(chk("C2", "도면 재대조", "n", "도면 문자 색인이 없어 표와 대조하지 못했다"))
|
||||
elif texts and "표" in m["source"] and not tables_ok:
|
||||
checks.append(chk("C2", "도면 재대조", "n", "도면에서 표를 인식하지 못했다"))
|
||||
else:
|
||||
checks.append(chk("C2", "도면 재대조", "x", "표 후보가 없는 묶음(표 밖 치수는 C단계 몫)"))
|
||||
# C2b
|
||||
ac = anchor_check(arows, amiss, f"{which}:{key}.") if arows else None
|
||||
if ac:
|
||||
checks.append(ac)
|
||||
# C4
|
||||
gate = next((g for g, bl in ((g, bundles_of_gate(g)) for g in ("G2", "G3", "G4")) if (which, key) in bl), None)
|
||||
if gate and gate in pairs:
|
||||
bad, soft, ok_n = [], [], 0
|
||||
for label, mark, got, want in pairs[gate]:
|
||||
if want is None:
|
||||
continue
|
||||
if got == want:
|
||||
ok_n += 1
|
||||
continue
|
||||
pf = _planned(expected, gate, mark)
|
||||
(soft if pf else bad).append(f"{mark} {label} {got} ≠ 재료표 {want}" + (f" (계획 FAIL: {pf.get('approved', '')})" if pf else ""))
|
||||
if bad:
|
||||
checks.append(chk("C4", "내부 정합", "b", "수량이 재료표와 다르다 — " + "; ".join(bad[:3]), strength="C"))
|
||||
elif soft:
|
||||
checks.append(chk("C4", "내부 정합", "w", "수량이 재료표와 다르지만 계획된 예외다 — " + "; ".join(soft[:3]), strength="C"))
|
||||
else:
|
||||
checks.append(chk("C4", "내부 정합", "g", f"생성 목록 수량 {ok_n}건이 재료표 수량과 같다 (위치·좌표는 확인하지 않음)", strength="C"))
|
||||
else:
|
||||
checks.append(chk("C4", "내부 정합", "x", "수량 대조 대상이 아니다"))
|
||||
# C5
|
||||
if which == "expected" and key in ("G1", "G2", "G3", "G4", "G5"):
|
||||
es = _src_texts(value)
|
||||
ps = {t for k in params for t in _src_texts(params[k])}
|
||||
if not es:
|
||||
checks.append(chk("C5", "params와 독립", "n", "expected에 출처 기록이 없어 독립 입력인지 알 수 없다"))
|
||||
elif any(len(t) > 12 and t in ps for t in es):
|
||||
checks.append(chk("C5", "params와 독립", "w", "expected의 출처 문구가 params와 같다 — 독립 입력인지 확인"))
|
||||
else:
|
||||
checks.append(chk("C5", "params와 독립", "g", "expected의 출처 문구가 params와 다르다"))
|
||||
else:
|
||||
checks.append(chk("C5", "params와 독립", "x", "해당 없음"))
|
||||
# C6
|
||||
bad6, warn6 = [], []
|
||||
for path, v in _walk(value):
|
||||
name = path[-1] if path else ""
|
||||
if isinstance(v, float) and (math.isnan(v) or math.isinf(v)):
|
||||
bad6.append(f"{'.'.join(path)} = {v}")
|
||||
elif isinstance(v, (int, float)) and not isinstance(v, bool):
|
||||
is_size = (SIZE_KEYS.match(name) or (name in SIZE_IN and len(path) > 1 and path[-2] in SIZE_IN[name])) and not any(p.startswith(NOT_SIZE) for p in path)
|
||||
if is_size and v <= 0:
|
||||
bad6.append(f"{'.'.join(path)} = {v} (0 이하)")
|
||||
elif Z_KEYS.match(name) and not (Z_RANGE[0] <= v <= Z_RANGE[1]):
|
||||
warn6.append(f"{'.'.join(path)} = {v} (EL 범위 밖)")
|
||||
if bad6:
|
||||
checks.append(chk("C6", "범위·단위", "b", "; ".join(bad6[:3])))
|
||||
elif warn6:
|
||||
checks.append(chk("C6", "범위·단위", "w", "; ".join(warn6[:3])))
|
||||
else:
|
||||
checks.append(chk("C6", "범위·단위", "g", "크기·EL이 상식 범위 안에 있다"))
|
||||
# C7
|
||||
ofix = origin_fixed.get(b)
|
||||
logged = [e for e in edit_log if e.get("section") == b]
|
||||
if m["grade"] == "F":
|
||||
if m.get("origin") is None:
|
||||
tv = template_values.get(b) or {}
|
||||
now = json.dumps(value, sort_keys=True, ensure_ascii=False)
|
||||
same = sorted(t for t, v in tv.items() if v == now)
|
||||
if key == "pier_id":
|
||||
checks.append(chk("C7", "변수 등급 위반", "x", "교각 식별자라 비교하지 않는다"))
|
||||
elif same:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", f"템플릿 복사 기록은 없지만 보호 템플릿 {len(same)}종({', '.join(same[:3])})과 같은 값이다", strength="F"))
|
||||
elif tv:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", "템플릿 복사 기록이 없고 어느 보호 템플릿과도 같지 않다 — 새 형식인지 확인"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "n", "템플릿 원본 기록이 없어 고정 항목을 비교하지 못했다"))
|
||||
elif m["origin"]:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", "고정(F) 항목이 템플릿 원본과 같다", strength="F"))
|
||||
elif logged:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", f"고정(F) 항목이 원본과 다르다 — 사유 기록: {logged[-1]['reason']}"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "b", "고정(F) 항목이 원본과 다르고 사유 기록이 없다"))
|
||||
elif m["fixed_fields"] and ofix is not None and current_fixed is not None:
|
||||
diff = [p for p in set(ofix) | set(current_fixed.get(b, {})) if ofix.get(p) != current_fixed.get(b, {}).get(p)]
|
||||
if not diff:
|
||||
checks.append(chk("C7", "변수 등급 위반", "g", "고정 필드가 템플릿 원본과 같다"))
|
||||
elif logged:
|
||||
checks.append(chk("C7", "변수 등급 위반", "w", f"고정 필드({', '.join(sorted(diff))})가 원본과 다르다 — 사유: {logged[-1]['reason']}"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "b", f"고정 필드({', '.join(sorted(diff))})가 원본과 다르고 사유 기록이 없다"))
|
||||
else:
|
||||
checks.append(chk("C7", "변수 등급 위반", "x", "고정 필드 비교 대상이 없다"))
|
||||
# C8
|
||||
checks.append(chk("C8", "재생성 일치", "x", "생성 스크립트가 앱에 연결되어 있지 않다 (재생성 일치는 독립성이 낮아 근거로 쓰지 않는다)" if m["grade"] == "V3"
|
||||
else "V3 묶음이 아니다"))
|
||||
# C9
|
||||
rel = cons_by.get(b, [])
|
||||
badc = [r for r in rel if not r["ok"]]
|
||||
if badc:
|
||||
d = "; ".join(f"{r['text']}: params {r['p']} = {r['pv']:g} ≠ expected {r['e']} = {r['ev']:g}" if isinstance(r["pv"], float) and isinstance(r["ev"], float)
|
||||
else f"{r['text']}: {r['pv']} / {r['ev']}" for r in badc[:3])
|
||||
checks.append(chk("C9", "params↔expected 일관성", "b", f"서로 같아야 할 값 {len(rel)}건 중 {len(badc)}건이 다르다 — {d}"))
|
||||
elif rel:
|
||||
checks.append(chk("C9", "params↔expected 일관성", "g", f"서로 같아야 할 값 {len(rel)}건이 params와 expected에서 같다 (같은 도면 읽기에서 나왔다면 독립 근거는 아님)"))
|
||||
else:
|
||||
checks.append(chk("C9", "params↔expected 일관성", "x", "연동 값 규칙이 없는 묶음"))
|
||||
# C3b: fields of a changed bundle that still equal the creation snapshot
|
||||
checks.append(stale_fields_check(which, key, value, origin_values, m))
|
||||
# C3 (needs C2 / C4 results)
|
||||
strong = any(c["id"] in ("C2", "C2b") and c["st"] == "g" for c in checks)
|
||||
if m["grade"] == "F":
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "x", "고정(F) 항목은 C7에서 본다"))
|
||||
elif m.get("origin") is None:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "x", "템플릿 복사 기록이 없어 원본과 비교하지 못했다"))
|
||||
elif m["origin"]:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "g" if strong else "n",
|
||||
"원본과 같지만 도면(표·표 밖 치수)과 일치한다" if strong else "현재 값이 템플릿 원본 그대로이고 새 도면 근거가 없다"))
|
||||
else:
|
||||
checks.append(chk("C3", "템플릿 원본 잔존", "g", "원본과 다른 값이 들어 있다"))
|
||||
order = ("C1", "C2", "C2b", "C3", "C3b", "C4", "C5", "C6", "C7", "C8", "C9")
|
||||
checks.sort(key=lambda c: order.index(c["id"]))
|
||||
st = combine(checks, m, (which, key) in DECISION_KEYS)
|
||||
out[b] = {"st": st, "checks": checks, "summary": summarize(st, checks)}
|
||||
return out
|
||||
|
||||
|
||||
def combine(checks, m, decision):
|
||||
by = {c["id"]: c for c in checks}
|
||||
if decision:
|
||||
return "x"
|
||||
if any(c["st"] == "b" for c in checks):
|
||||
return "b"
|
||||
if by["C3"]["st"] == "n":
|
||||
return "n"
|
||||
warn = any(c["st"] == "w" for c in checks)
|
||||
if m["grade"] == "F":
|
||||
c7 = by["C7"]
|
||||
return "w" if warn else ("g" if c7["st"] == "g" else "n")
|
||||
if any(c["strength"] == "S" and c["st"] == "g" for c in checks):
|
||||
return "w" if warn else "g"
|
||||
if any(c["strength"] == "C" and c["st"] in ("g", "w") for c in checks):
|
||||
return "w"
|
||||
return "n"
|
||||
|
||||
|
||||
def summarize(st, checks):
|
||||
"""One sentence for the table: the check that decides the status."""
|
||||
if st == "x":
|
||||
return "사람 결정 기록 또는 참고 정보 — 앱이 도면과 대조하지 않는다"
|
||||
pick = {"b": [c for c in checks if c["st"] == "b"],
|
||||
"w": [c for c in checks if c["st"] == "w"] or [c for c in checks if c["strength"] == "C" and c["st"] == "g"],
|
||||
"g": [c for c in checks if c["strength"] in ("S", "F") and c["st"] == "g"],
|
||||
"n": [c for c in checks if c["id"] in ("C3", "C7", "C2") and c["st"] == "n"]}[st]
|
||||
if pick:
|
||||
c = pick[0]
|
||||
return f"{c['id']} {c['text']}" + (" — 수량만 확인, 위치·좌표는 도면과 직접 대조" if st == "w" and c["strength"] == "C" and c["st"] == "g" else "")
|
||||
return "앱이 독립적으로 대조할 근거가 없다 — 도면과 직접 대조한 뒤 사유와 함께 확정"
|
||||
|
||||
|
||||
def counts(results, required):
|
||||
"""Per-status counts of the required bundles."""
|
||||
c = {"g": 0, "w": 0, "b": 0, "n": 0, "x": 0}
|
||||
for b in required:
|
||||
r = results.get(b)
|
||||
if r:
|
||||
c[r["st"]] += 1
|
||||
return c
|
||||
Reference in new issue
Block a user