- 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>
108 lines
6.3 KiB
Python
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))
|