Merge remote-tracking branch 'origin/dev' into sub_laptop_8

- 충돌 시험 셋(717-4 · 719-2 · hopyo build) = 740-1 펼침 단언 + 742-1 저장 수량 단언 합침
- 740-2 일위대가표 풍선 줄의 body(742-1 이 걷은 휘발 셈 몸) → cur

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-11-02 21:47:51 +09:00
co-authored by Claude Opus 5.5
66 changed files with 2663 additions and 244 deletions
+60 -4
View File
@@ -42,6 +42,7 @@ _OWNER = {"company": re.compile(r"^\d+$"), "personal": re.compile(r"^\d+/\d+$")}
Ref = dict[str, Any]
Here = tuple[str, str | None]
COPY = "사본" # 740-1 하위 호표 사본 블록 — 참조 꼴 {층: 사본, 주인: 몸 지문, 키: 본 SC, 몸}
def _bad(text: str, status: int = 400) -> store.StoreError:
@@ -103,7 +104,28 @@ def ref_path(ref: Ref) -> Path:
return folder_of(ref["층"], ref.get("주인")) / kind_of(ref["키"]) / f"{ref['키']}.json"
def is_copy(ref: Any) -> bool:
"""사본 블록(740-1) — 일위대가 안 일위대가 몸을 줄 안에 담음(원본 연결 없음 · 키 = 출처)."""
return isinstance(ref, dict) and ref.get("층") == COPY and isinstance(ref.get("몸"), dict)
def copy_ref(body: dict[str, Any], key: str) -> Ref:
"""사본 블록 참조 — 몸(종류 · 이름 · 단위 · 담은로직 · 자재 · 본뜬) · 주인 = 몸 지문."""
import hashlib
keep = {k: body[k] for k in ("종류", "이름", "단위", "담은로직", "자재", "본뜬") if body.get(k)}
raw = json.dumps(keep, ensure_ascii=False, sort_keys=True, default=str)
return {
"층": COPY,
"주인": hashlib.sha256(raw.encode()).hexdigest()[:12],
"키": key,
"몸": keep,
}
def read_ref(ref: Ref) -> dict[str, Any] | None:
if is_copy(ref):
return ref["몸"]
try:
got = json.loads(ref_path(ref).read_bytes())
except (OSError, ValueError):
@@ -114,6 +136,8 @@ def read_ref(ref: Ref) -> dict[str, Any] | None:
def readable(at: Here, ref: Ref) -> bool:
"""`at` 층이 `ref` 를 가리켜도 되나 — 위 층만(프로젝트는 아무것도 · 굳힌 사본만 씀)."""
layer, owner = at
if ref.get("층") == COPY: # 사본 블록 = 줄 안 몸(어느 층도 읽음)
return True
if layer == "project":
return False
if ref["층"] == "system":
@@ -127,6 +151,8 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref:
"""참조 꼴 · 위 층만 · 있는 문서 — 아니면 400."""
if not isinstance(raw, dict):
raise _bad(f"{at} · 참조는 {{층, 주인, 키}}")
if raw.get("층") == COPY:
return _check_copy(raw, at)
layer, owner, key = raw.get("층"), raw.get("주인"), str(raw.get("키") or "")
if layer == "system":
owner = None
@@ -142,6 +168,20 @@ def check_ref(raw: Any, at: str, here_: Here) -> Ref:
return ref
def _check_copy(raw: Ref, at: str) -> Ref:
"""사본 블록 검사 — 키 = SC · 몸 줄 · 자재 = 일위대가 검사 그대로 · 지문 다시 셈."""
from M02_MasterTemplete import M02_Labor_Doc as labor
body, key = raw.get("몸"), str(raw.get("키") or "")
if not isinstance(body, dict) or not _KEY.match(key) or kind_of(key) != "precombo":
raise _bad(f"{at} · 사본 블록은 {{층: 사본, 키: SC-…, 몸}}")
try:
labor.clean({"담은로직": body.get("담은로직") or [], "자재": body.get("자재") or []})
except store.StoreError as e:
raise _bad(f"{at} · 사본 블록 · {e.detail}") from e
return copy_ref(body, key)
# ── 서고 문서 정리 ─────────────────────────────────────────────────────
def check_key(kind: str, name: str) -> None:
found = _KEY.match(name)
@@ -155,6 +195,10 @@ def clean_doc(kind: str, doc: Any) -> dict[str, Any]:
raise _bad("서고 문서가 묶음 아님")
key = str(doc.get("키") or "")
check_key(kind, key)
if here()[0] != "project": # 740-1 — M02 참조 줄 = M01 키 줄로 펼침(못 펼친 줄은 그대로)
from M02_MasterTemplete import M02_Library_Flat as flat
doc = flat.flat_doc(kind, doc, me={"층": here()[0], "주인": here()[1], "키": key})
name, unit = str(doc.get("이름") or "").strip(), str(doc.get("단위") or "").strip()
if kind == "sangeun":
from M02_MasterTemplete import M02_Sangeun_Calc as calc
@@ -245,13 +289,14 @@ def _combo_lines(doc: dict, key: str) -> list[dict[str, Any]]:
일위대가와 같은 길(`labor.clean` · 같은 로직 · 같은 참조 두 번 · 입력값) · 수량 기본 {손: 1}."""
from M02_MasterTemplete import M02_Labor_Doc as labor
lines = []
for one in labor.clean({"담은로직": doc.get("담은로직") or []})["담은로직"]:
lines, raw = [], doc.get("담은로직") or []
for one, sent in zip(labor.clean({"담은로직": raw})["담은로직"], raw):
head = {k: one[k] for k in ("참조", "고정", "패치") if k in one} # 패치 = 줄 덮개(715-3)
if "로직" in one:
head.update(로직=one["로직"], 입력값=one["입력값"])
head.update({k: one[k] for k in ("켜쌓기율", "고름", "자재표시") if k in one})
head.update({"별칭": one["별칭"]} if one["별칭"] else {}) # 실무 품명(324-3)
head.update(_mark(sent)) # 넣은 줄 출처(740-1 · 셈에 안 씀)
lines.append({**head, "수량": one["수량"], "메모": one["메모"]})
at_ = here()
me = {"층": at_[0], "주인": at_[1], "키": key}
@@ -269,13 +314,20 @@ def _combo_mats(doc: dict, lines: list[dict[str, Any]]) -> list[dict[str, Any]]:
로직 자재와 같은 품목 400(`labor.no_twice`)."""
from M02_MasterTemplete import M02_Labor_Doc as labor
mats = labor.clean({"자재": doc.get("자재") or []})["자재"]
raw = doc.get("자재") or []
mats = [{**m, **_mark(s)} for m, s in zip(labor.clean({"자재": raw})["자재"], raw)]
if mats:
got = {"담은로직": lines, "자재": mats}
labor.no_twice(got, labor.calc(got, {}))
return mats
def _mark(sent: Any) -> dict[str, Any]:
"""줄 `본뜬`(740-1 · 펼쳐 넣은 출처 {층, 주인, 키, 이름, 때}) — 묶음만 · 셈에 안 씀."""
got = sent.get("본뜬") if isinstance(sent, dict) else None
return {"본뜬": got} if isinstance(got, dict) else {}
def _no_loop(ref: Ref, trail: list[Ref]) -> None:
"""저장 때 순환 · 깊이 — 아래로 따라가 처음 문서를 다시 만나면 422 「A → B → A」."""
if len(trail) > DEPTH:
@@ -412,8 +464,12 @@ def _reused(kind: str, doc: dict[str, Any], ref: Ref) -> str | None:
def carry(doc: dict[str, Any], source: Here) -> dict[str, Any]:
"""복사(원본 층 `source` → 지금 층) — 지금 층이 못 읽는 참조는 그 문서를 지금 층에 새 키로
같이 복사(`본뜬` = 원래 참조) · 참조를 새 키로 바꿈 · 같은 원본 같은 내용 사본이 이미 있으면
그 키(733-2). 원본 층 읽기 권한은 부르는 쪽 몫."""
그 키(733-2). 원본 층 읽기 권한은 부르는 쪽 몫 · 단가산출 · 일위대가 = 먼저 펼침(740-1)."""
target, done = here(), {}
if doc.get("종류") in (TITLE["sangeun"], TITLE["precombo"]):
from M02_MasterTemplete import M02_Library_Flat as flat
doc = flat.flat_doc("sangeun" if doc["종류"] == TITLE["sangeun"] else "precombo", doc)
def move(ref: Ref, depth: int) -> Ref:
if readable(target, ref):