fix(setup): 되살아난 빈 줄을 잡고, 같은 실수를 검사기가 막게 한다
`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) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7661f08d58
commit
b1a78752df
-1
@@ -906,7 +906,6 @@ replica 를 2 로 되돌리고 예전 이미지를 두 노드에 다시 넣고
|
|||||||
|
|
||||||
**replica 2 에서 다시 로그인이 되는 것이 복구 판정이다.** 그것이 되면 세션이 다시 Redis 를 타고 있다는 뜻이다.
|
**replica 2 에서 다시 로그인이 되는 것이 복구 판정이다.** 그것이 되면 세션이 다시 Redis 를 타고 있다는 뜻이다.
|
||||||
|
|
||||||
|
|
||||||
## 무엇이 관측이고 무엇이 아닌가
|
## 무엇이 관측이고 무엇이 아닌가
|
||||||
|
|
||||||
이 절차의 숫자는 `2026-09-04 13:39–13:46 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
이 절차의 숫자는 `2026-09-04 13:39–13:46 KST` 에 돈 한 번의 실행에서 나왔다(observed).
|
||||||
|
|||||||
Executable
+139
@@ -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:]))
|
||||||
Reference in New Issue
Block a user