From b1a78752df0db098d5440190925cd798517837e2 Mon Sep 17 00:00:00 2001 From: DongHyeonka Date: Thu, 17 Sep 2026 20:29:27 +0900 Subject: [PATCH] =?UTF-8?q?fix(setup):=20=EB=90=98=EC=82=B4=EC=95=84?= =?UTF-8?q?=EB=82=9C=20=EB=B9=88=20=EC=A4=84=EC=9D=84=20=EC=9E=A1=EA=B3=A0?= =?UTF-8?q?,=20=EA=B0=99=EC=9D=80=20=EC=8B=A4=EC=88=98=EB=A5=BC=20?= =?UTF-8?q?=EA=B2=80=EC=82=AC=EA=B8=B0=EA=B0=80=20=EB=A7=89=EA=B2=8C=20?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `bbd87dc` 에서 Studio 편집기가 다시 쓰는 공백을 맞춰 뒀는데 바로 다음 커밋 `7661f08` 에서 B-0 본문을 보강하며 같은 자리를 되살렸다. 사람이 눈으로 지키는 것으로는 안 되는 종류라 검사기를 만든다. - setup-reproduce-b0-default-session-store.md 의 빈 줄 하나를 지웠다. Studio 저장본과 편집 화면 미러가 1바이트 달랐던 원인이다 - scripts/check-studio-whitespace.py — 이어진 빈 줄과 닫는 펜스 뒤 빈 줄을 studio-body.py 로 변환한 모양에서 찾는다. --fix 로 고친다 검사 범위는 Case·Concept·Setup 뿐이다. Reference·Question·Decision 의 칸은 평문으로 렌더링되므로 블록 편집기를 거치지 않는다 — 범위를 안 좁히면 그 세 종류에서 47곳이 걸려 실제 결함이 묻힌다. 요약 줄이 안 본 기록 수를 따로 센다. STUDIO WHITESPACE: PASS — 본문 있는 기록 193 · 본 것 193 · 어긋난 곳 0 · 못 본 기록 0 · 본문 없는 종류라 안 본 기록 175 `check_body` 는 이 결함을 못 잡는다 — 공백이 달라도 파싱은 성립한다. Co-Authored-By: Claude Opus 5 (1M context) --- ...etup-reproduce-b0-default-session-store.md | 1 - scripts/check-studio-whitespace.py | 139 ++++++++++++++++++ 2 files changed, 139 insertions(+), 1 deletion(-) create mode 100755 scripts/check-studio-whitespace.py diff --git a/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md b/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md index c996984..4b5f5b6 100644 --- a/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md +++ b/docs/keycloak-session-store/tech-log-studio/where-application-state-lives/setup/setup-reproduce-b0-default-session-store.md @@ -906,7 +906,6 @@ replica 를 2 로 되돌리고 예전 이미지를 두 노드에 다시 넣고 **replica 2 에서 다시 로그인이 되는 것이 복구 판정이다.** 그것이 되면 세션이 다시 Redis 를 타고 있다는 뜻이다. - ## 무엇이 관측이고 무엇이 아닌가 이 절차의 숫자는 `2026-09-04 13:39–13:46 KST` 에 돈 한 번의 실행에서 나왔다(observed). diff --git a/scripts/check-studio-whitespace.py b/scripts/check-studio-whitespace.py new file mode 100755 index 0000000..1d7a6e1 --- /dev/null +++ b/scripts/check-studio-whitespace.py @@ -0,0 +1,139 @@ +#!/usr/bin/env python3 +"""Studio 편집기가 다시 쓰는 공백을 저장소에서 미리 잡는다. + +Studio 의 블록 편집기는 본문을 화면에 풀 때 두 가지를 바꾼다. + + 이어진 빈 줄 둘 -> 하나 + 닫는 코드펜스 뒤에 빈 줄이 없으면 -> 하나를 넣는다 + +저장된 값이 저장소와 같아도, 사람이 편집 화면을 열고 `저장` 을 누르는 순간 +그 공백이 바뀌어 양쪽이 갈린다. 내용은 그대로인데 SHA 만 달라지므로 어느 쪽이 +정본인지 알 수 없게 된다. 그래서 저장소를 미리 편집기 모양으로 맞춰 둔다. + +`check_body` 는 이것을 못 잡는다 — 공백이 달라도 파싱은 성립한다. + + python3 scripts/check-studio-whitespace.py # 전부 + python3 scripts/check-studio-whitespace.py <프로젝트> # 한 프로젝트 + python3 scripts/check-studio-whitespace.py --fix # 고친다 + +코드블록 안의 빈 줄과 frontmatter 는 건드리지 않는다. +""" +import glob +import io +import os +import subprocess +import sys + +ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + + +def rendered(path): + """Studio 로 보낼 모양. 저장소의 마크다운 이미지가 :::evidence 로 바뀐다.""" + out = subprocess.run( + ["python3", os.path.join(ROOT, "scripts", "studio-body.py"), path, "--body-only"], + capture_output=True, text=True) + return out.stdout if out.returncode == 0 else None + + +def scan(lines): + """(종류, 줄번호) 목록. 줄번호는 1부터.""" + found = [] + infence = False + for i, line in enumerate(lines): + if line.strip().startswith("```"): + was = infence + infence = not infence + if was and i + 1 < len(lines): + nxt = lines[i + 1].strip() + if nxt and not nxt.startswith("```"): + found.append(("닫는 펜스 뒤에 빈 줄이 없다", i + 1)) + continue + if infence: + continue + if line == "" and i + 1 < len(lines) and lines[i + 1] == "": + found.append(("빈 줄이 둘 이어진다", i + 1)) + return found + + +def fix_file(path): + src = io.open(path, encoding="utf-8").read() + head, sep, rest = src.partition("\n---\n") # frontmatter 는 그대로 둔다 + if not sep: + return 0 + lines = rest.split("\n") + out, infence, changed = [], False, 0 + for i, line in enumerate(lines): + if line.strip().startswith("```"): + was = infence + infence = not infence + out.append(line) + if was and i + 1 < len(lines): + nxt = lines[i + 1].strip() + if nxt and not nxt.startswith("```"): + out.append("") + changed += 1 + continue + if infence: + out.append(line) + continue + if line == "" and out and out[-1] == "": + changed += 1 + continue + out.append(line) + if changed: + io.open(path, "w", encoding="utf-8").write(head + sep + "\n".join(out)) + return changed + + +def main(argv): + do_fix = "--fix" in argv + args = [a for a in argv if not a.startswith("--")] + project = args[0] if args else "*" + + # 본문이 있는 종류만 본다. Reference·Question·Decision 의 칸은 평문으로 + # 렌더링되므로 블록 편집기를 거치지 않고, 그래서 이 공백 규칙의 대상이 아니다. + paths = [] + for kind in ("case", "concept", "setup"): + paths += glob.glob(os.path.join( + ROOT, "docs", project, "tech-log-studio", "*", kind, "*.md")) + paths = sorted(paths) + skipped = len(glob.glob(os.path.join( + ROOT, "docs", project, "tech-log-studio", "*", "*", "*.md"))) - len(paths) + if not paths: + print("STUDIO WHITESPACE: 대상이 성립하지 않는다 — " + f"docs/{project} 에 Case·Concept·Setup 기록이 없다") + return 2 + + unreadable, hits, fixed = [], [], 0 + for path in paths: + body = rendered(path) + if body is None: + unreadable.append(path) + continue + found = scan(body.split("\n")) + if not found: + continue + if do_fix: + fixed += fix_file(path) + else: + for kind, line in found: + hits.append((path, line, kind)) + + for path, line, kind in hits: + print(f" ! {os.path.relpath(path, ROOT)}:{line} {kind}") + for path in unreadable: + print(f" ? {os.path.relpath(path, ROOT)} studio-body.py 가 못 읽었다") + + seen = len(paths) - len(unreadable) + if do_fix: + print(f"STUDIO WHITESPACE: 고침 {fixed}곳 · 기록 {len(paths)} · 본 것 {seen}") + return 0 + verdict = "FAIL" if hits else "PASS" + print(f"STUDIO WHITESPACE: {verdict} — 본문 있는 기록 {len(paths)} · 본 것 {seen} " + f"· 어긋난 곳 {len(hits)} · 못 본 기록 {len(unreadable)} " + f"· 본문 없는 종류라 안 본 기록 {skipped}") + return 1 if hits else 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:]))