Files
Pier_v1/Code/교각모델링앱/engine/variability.py
T
ahnyongjooandClaude Sonnet 5.5 964a841de1 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>
2026-10-28 11:20:18 +09:00

108 lines
6.3 KiB
Python

"""Variability classes of the input bundles (stage A of the 입력 묶음 확인·확정 개편, 2026-10-16).
Every top-level item of params.json / expected.json is a bundle. Grades (기획서 4.4):
V1 value variable numbers / text that change from drawing to drawing within the same form
V2 list variable position lists, dimension chains, outline points
V3 generated variable long lists made by a reading script (hand editing is blocked)
F fixed defines the form; changing it means "another template" (edit needs a reason)
`fixed_fields` are paths inside a non-F bundle that behave like F (e.g. coping.form); "*" matches every child of an object.
`source` lists where the values normally come from: 표 / 표 밖 치수 / 판독 스크립트 / 사람 입력 / 템플릿 고정.
The result is a draft until the template's owner confirms it (variability.json of each protected template).
"""
import json
SOURCES = ["표", "표 밖 치수", "판독 스크립트", "사람 입력", "템플릿 고정"]
GRADES = {"V1": "값 변수", "V2": "목록 변수", "V3": "생성 변수", "F": "고정"}
V3_MIN_BARS = 100 # a rebar bundle with at least this many explicit bars is a generated list
# key -> (grade, [sources], [fixed_fields], note); the same table serves params and expected (file-specific rows below)
PARAMS = {
"pier_id": ("F", ["템플릿 고정"], [], "교각 식별자"),
"scope": ("F", ["템플릿 고정"], [], "모델링 게이트 범위(실행 설정)"),
"builders": ("F", ["템플릿 고정"], [], "게이트별 생성기(실행 설정)"),
"display": ("F", ["템플릿 고정"], [], "표시·색 설정"),
"lean_concrete": ("V1", ["표 밖 치수"], [], "버림콘크리트 치수·EL"),
"footing": ("V1", ["표 밖 치수"], [], "기초 치수·EL (rot_deg = 기초 회전 각도 값)"),
"column": ("V1", ["표 밖 치수"], ["shape"], "기둥 지름·높이 (단면 형식 shape는 고정)"),
"materials": ("V1", ["표 밖 치수"], [], "재료 강도 주기"),
"piles": ("V1", ["표 밖 치수"], ["kind"], "말뚝 개수·간격·길이 (말뚝 유무는 템플릿 형식)"),
"pile": ("V1", ["표 밖 치수"], ["kind"], "단일 말뚝 구간"),
"coping": ("V2", ["표 밖 치수"], ["form"], "코핑 외곽·치수 (형식 form은 고정)"),
"bearing_seats": ("V1", ["표", "표 밖 치수"], ["pedestal", "pedestals"], "받침 표 값 + 평면 위치 (대좌 구조는 고정)"),
"sole_plates": ("V1", ["표"], ["bearing_size"], "솔플레이트 표 값 (받침 크기 규칙은 고정)"),
"rebar_marks": ("V1", ["표"], ["*.shape"], "재료표 직경·형상 (마크 체계의 형상은 고정)"),
"column_rebar": ("V2", ["표 밖 치수"], ["form"], "기둥 철근 사슬·위치"),
"footing_rebar": ("V2", ["표 밖 치수"], ["form"], "기초 철근 위치 사슬"),
"shaft_rebar": ("V2", ["표 밖 치수"], ["form"], "말뚝 철근"),
"coping_rebar": ("V2", ["표 밖 치수"], ["form", "host", "group"], "코핑 철근"),
}
EXPECTED = {
"pier_id": ("F", ["템플릿 고정"], [], "교각 식별자"),
"tol_mm": ("V1", ["사람 입력"], [], "허용 오차"),
"checkers": ("F", ["템플릿 고정"], [], "게이트별 검사기(실행 설정)"),
"negative": ("F", ["템플릿 고정"], [], "음성 시험 대상(실행 설정)"),
"rebar_palette": ("V1", ["사람 입력"], [], "철근 직경별 색 (사람 결정 기록)"),
"planned_fail": ("V1", ["사람 입력"], [], "계획된 FAIL (사람 결정 기록)"),
**{g: ("V2", ["표", "표 밖 치수"], [], "독립 기대값 (params와 따로 도면에서 옮긴 값)") for g in ("G1", "G2", "G3", "G4", "G5")},
}
def is_info(key):
return key.startswith("_") or key.endswith("_src") or key.startswith("hitl_decisions")
def _bars(value):
"""Number of explicit per-bar entries of a rebar bundle (list `bars`), 0 when the bundle is rule based."""
return len(value.get("bars", [])) if isinstance(value, dict) and isinstance(value.get("bars"), list) else 0
def classify(which, key, value):
"""-> {grade, source, fixed_fields, note, info}."""
if is_info(key):
return dict(grade="F", source=["템플릿 고정"], fixed_fields=[], note="참고 정보(게이트가 쓰지 않음)", info=True)
grade, source, fixed, note = (PARAMS if which == "params" else EXPECTED).get(key, ("V1", ["사람 입력"], [], "미분류 — 담당자 확인"))
if which == "params" and key in ("coping_rebar", "footing_rebar") and _bars(value) >= V3_MIN_BARS:
grade, source, note = "V3", ["판독 스크립트"], note + " — 철근별 중심선 목록(판독 스크립트로 생성)"
return dict(grade=grade, source=list(source), fixed_fields=list(fixed), note=note, info=False)
def build(params, expected):
"""variability.json content for a params / expected pair (draft)."""
sections = {}
for which, d in (("params", params), ("expected", expected)):
for k, v in d.items():
sections[f"{which}:{k}"] = classify(which, k, v)
return {"_schema": "variability/1", "status": "초안 — 템플릿 담당자 확정 전", "grades": GRADES, "sections": sections}
def _walk(value, parts):
"""Values at a path with "*" wildcards, as a list of (path, value); a missing path yields (path, None)."""
if not parts:
return [("", value)]
head, rest = parts[0], parts[1:]
if head == "*":
if not isinstance(value, dict):
return []
out = []
for k, v in value.items():
out += [(f"{k}.{p}".rstrip("."), x) for p, x in _walk(v, rest)]
return out
if not isinstance(value, dict) or head not in value:
return [(head, None)]
return [(f"{head}.{p}".rstrip("."), x) for p, x in _walk(value[head], rest)]
def fixed_view(value, fixed_fields):
"""{path: json text} of the fixed fields of a bundle value (what must not change silently)."""
out = {}
for f in fixed_fields:
for p, x in _walk(value, f.split(".")):
out[p] = json.dumps(x, sort_keys=True, ensure_ascii=False)
return out
def changed_fixed(old, new, fixed_fields):
"""Paths of fixed fields whose value differs between two bundle values."""
a, b = fixed_view(old, fixed_fields), fixed_view(new, fixed_fields)
return sorted(p for p in set(a) | set(b) if a.get(p) != b.get(p))