Compare commits

...
68 Commits
Author SHA1 Message Date
DongHyeonkaandClaude Opus 5 c93cdea150 feat(studio): 반입에서 빠졌던 setup 두 편을 Studio 에 만든다 — 35편 전부 자리를 갖는다
virtualization 의 setup 9편은 같은 커밋(2109f72)에서 한꺼번에 생겼는데 그때
Studio 반입이 7편만 덮고 둘을 빠뜨렸다. 둘 다 readiness 가 READY 이고 관문도
통과하는데 frontmatter 에 id 도 studio 도 없었다 — Studio 문서가 아예 없었다는
뜻이다. 빼놓을 이유가 없어서 만들었다.

- setup-tear-down-the-lab-and-know-what-survives  -> 87e0138d-…
- setup-power-cycle-the-lab-and-reallocate-guest-memory -> 8a9ca3d4-…

주제는 새로 만들지 않고 같은 폴더의 기존 편에서 topicId·projectId 를 읽어
같은 값을 썼다. 둘 다 저장까지만 하고 게시하지 않았다(currentPublication: null).

pinnedVersions 의 40자 상한
- power-cycle 의 「기준 배치」 값이 51자여서 서버가 422 로 거절했다
  (size must be between 1 and 40)
- 그 칸에 문장이 들어가 있었다. CLAUDE.md 는 Setup 에 대해 「명령이 본문에
  들어가고 버전만 pinnedVersions 에 남는다」고 적는다
- 잘라내기 전에 그 설명이 본문에 있는지 먼저 봤다. 셋 다 있었다 — 63행이
  호스트 RAM 증설, 176행이 2026-09-03, 215행이 2026-09-10 과의 날짜 엇갈림.
  그래서 날짜만 남겼고 잃은 내용이 없다
- 저장소 전체를 훑어 40자를 넘는 값은 이것 하나뿐이었다

기록 두 편의 diff 는 frontmatter 뿐이고, tech-log-tree.json 은 파생 칸
(publication · studioId) 넷만 바뀌었다. 「게시됨」은 Studio 에 있다는 뜻이지
공개됐다는 뜻이 아니다.

관문: check_body PASS · check_prose error 0 · check_evidence 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 ·
check-studio-whitespace PASS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:58:10 +09:00
DongHyeonkaandClaude Opus 5 862e502af3 docs(skill): GET 을 그대로 되보내면 400 이라는 것을 PUT 설명 옆에 적는다
`references/studio-api.md` 는 「서버가 주지만 보내지 않는 것: id · version ·
updatedAt」을 이미 적고 있었는데, 그 문장이 PUT 요청 모양을 설명하는 절이
아니라 한 절 아래 「종류마다의 칸」에 있었다. 2026-09-17 에 서로 다른 두
작업이 같은 자리에서 400 VALIDATION_FAILED / UnrecognizedPropertyException
을 맞았다 — 둘 다 GET 으로 받은 document 를 그대로 되보냈다.

내용을 더한 것이 아니라 PUT 을 설명하는 자리에서 그 표를 가리키게 했다.
서버 상태는 안 바뀌므로 셋을 빼고 다시 보내면 된다는 것도 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:34:56 +09:00
DongHyeonkaandClaude Opus 5 b1a78752df 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>
2026-09-17 20:29:27 +09:00
DongHyeonkaandClaude Opus 5 7661f08d58 feat(b0): B-0 을 주입부터 원상복구까지 끝까지 밟는다 — setup 35편 완주
남아 있던 한 편이다. keycloak-pattern 에 이 실험과 무관한 변경이 39개(미추적
3개 더) 있어 따로 떼어낸 작업 트리에서 파일 넷을 고치고 이미지를 구워 두
노드에 넣었다. 원래 작업 트리는 한 글자도 안 바뀌었다.

자동구성이 고른 것
- 빈 321개 — 이 편이 예측한 숫자와 같다
- authorizedClientService -> InMemoryOAuth2AuthorizedClientService
- SessionRepository 빈이 하나도 없다. Spring Session 이 아예 안 걸렸고
  세션을 톰캣이 자기 메모리의 HttpSession 에 직접 들고 있다
- 타입에 Redis 나 Jdbc 가 든 빈 0개

로그인이 replica 개수에서 갈린다
- 원 가이드는 브라우저로 한 번 해 보라고 적는데 그러면 한 번의 결과만 남는다.
  쿠키 병을 쓰는 curl 로 같은 흐름을 여섯 번씩 돌려 양쪽을 같은 수로 견줬다
- replica 2 — 여섯 번 다 /login?error
- replica 1 — 여섯 번 다 200. 이쪽이 대조군이라, 거기서도 실패했으면 가설이
  아니라 시험 방법을 의심해야 한다
- 토큰 경계는 원래 실행과 한 글자도 다르지 않았다

원상복구를 같은 명령으로 확인했다
- 빈 321 → 437, InMemory → Jdbc, Redis/Jdbc 빈 0 → 58, Redis 세션 키 6
- replica 2 에서 로그인이 다시 되는 것이 복구 판정이다

그리고 ⑤ 의 `git diff --stat` 점검은 작업 트리가 깨끗할 때만 성립한다는 것을
적었다. git worktree 로 떼어내면 그 점검이 다시 살아난다.

관문: check_body PASS · check_prose error 0 · check_evidence 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:16:45 +09:00
DongHyeonkaandClaude Opus 5 bbd87dc9ab fix(setup): Studio 편집기가 다시 쓰는 공백을 저장소에서 미리 맞춘다
Studio 의 블록 편집기는 본문을 화면에 풀 때 공백을 정규화한다. 이어진 빈 줄
둘을 하나로 줄이고, 닫는 코드펜스 뒤에 빈 줄이 없으면 하나를 넣는다. 저장된
값은 저장소와 바이트가 같지만, 사람이 편집 화면을 열고 저장을 누르는 순간
그 공백이 저장소와 갈린다 — 내용은 그대로인데 SHA 만 달라져서 어느 쪽이
정본인지 알 수 없게 된다.

setup 35편을 훑어 9편 11곳을 찾아 저장소 쪽을 편집기와 같은 모양으로 맞췄다.
빈 줄만 움직였고 내용 줄은 하나도 바꾸지 않았다(추가 2 · 삭제 9).

- 이어진 빈 줄 둘 → 하나: d4 · d4a(3곳) · a7 · b7a · c2 · b0 · tear-down
- 닫는 펜스 뒤 빈 줄 추가: c1 · create-three-guests

코드블록 안의 빈 줄과 frontmatter 는 건드리지 않았다.

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:03:53 +09:00
DongHyeonkaandClaude Opus 5 da774648dc fix(b0): 주입 1~2 절을 실제로 밟고 ⑤ 의 점검이 언제 성립하는지 적는다
B-0 의 주입 1 절(파일 넷 편집)과 2 절의 이미지 빌드를 2026-09-17 에 밟았다.
두 노드에 이미지를 밀어 넣는 단계부터는 안 밟았고 그대로 unknown 으로 적었다.

- ⑤ 의 `git diff --stat` 점검은 작업 트리가 깨끗할 때만 성립한다. 그 줄은
  덜 지운 것을 잡으려는 것인데, keycloak-pattern 에 이 실험과 무관한 변경이
  39개(미추적 3개 더) 있으면 아무것도 못 가린다. 따로 떼어낸 작업 트리에서
  고치면 점검이 다시 살아난다 — git worktree 한 줄을 적었다
- 넷을 다 뺀 뒤 무엇이 남는지를 숫자로 확인해 적었다. pom.xml 은 XML 로
  유효하고, SecurityConfig 에는 bffSecurity 하나만 남고, application.yml 의
  spring 아래는 둘뿐이며, 매니페스트의 Redis Deployment·Service·PVC 는 남는다
- 빌드는 한 번에 통과했다. 이 편이 적어 둔 processDuplicateKeys 실패는
  재현되지 않았다 — Tests run: 4, Failures: 0 · exit=0
- 앞서 「남은 걸림돌은 인증서 하나」라고 적힌 것을 해결됨으로 고쳤다.
  인증서는 엣지로 옮기고 강제 갱신까지 끝났다

관문: check_body PASS · check_prose error 0 · check_evidence 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 20:00:13 +09:00
DongHyeonkaandClaude Opus 5 32e39e20aa fix(setup): 실험대에서 35편을 끝까지 밟고 어긋난 명령·결과 31건을 고친다
test-server 를 비우고 다시 세운 뒤 Setup 기록 35편(virtualization 9 ·
keycloak-session-store 26)을 문서에 적힌 명령 그대로 쳤다. 어긋난 자리를
기록과 SSOT 양쪽에 실측과 함께 넣었다.

막히던 것
- 04 의 인증서 경로가 live/hyeonworks.com 이라 nginx 가 [emerg] 로 안 떴다.
  실제 계보는 live/auth.hyeonworks.com 이고 「문제가 생기면」은 진단이 거꾸로였다
- 인증서가 와일드카드가 아니다. SAN 이 auth·app1·app2 셋뿐이라 그 밖의 이름은
  TLS 에서 끊기고 curl 이 exit 60 · %{http_code} 000 을 낸다. SSOT 안에서
  두 문단이 서로 어긋나 있었다
- A-7 14번 ①이 kc-lab-1 에서 여섯 줄 다 실패하는데 마지막 date 만 「차단」을 찍는다

검사가 실패할 수 없던 자리
- B-1 의 세션 키 고르기는 앞 단계가 $KEY 를 채워 둬서 루프가 한 건도 못 맞혀도
  통과한다. KEY= 로 비우고 키마다 1/0 을 찍게 바꿨다
- k3s-agent 유닛의 sed -i 는 패턴에 $HOME 이 들어 있어 아무 줄도 안 바꾼 채 성공한다

certbot
- renew --dry-run 의 종료 코드는 성공도 0, 실패도 0, 다른 사유의 실패는 1 이다.
  본문의 renew failure(s) 로만 판정할 수 있다
- --dry-run 은 staging 서버를 쓰는데 renewal/*.conf 의 account= 는 운영 계정을
  가리킨다. 실패한 dry-run 이 staging 계정을 하나 더 만들어 다음 실행이 계속 멎는다
- 훅을 755 로 놓고 시뮬레이션이 성공해도 Running deploy-hook command 는 안 나온다.
  certbot 2.1.0 에는 --run-deploy-hooks 도 없다
- 강제 갱신은 실제로 쳤고 서빙까지 닿았다. serial 06F3E0EF…1373 → 065547…3DF1,
  notAfter Dec 3 → Dec 16, nginx worker 2629 4712 → 4745 4754

독자가 칠 수 있는 형태로
- 안 되는 형태가 번호 붙은 단계에 앉아 있던 8곳을 뒤집고, 되는 형태를 ①로 올렸다
- 랩 안에서 공개 이름을 치는 curl 65줄에 --resolve 를 붙였다. 붙인 형태를 실제로
  쳐서 문서가 적은 값과 같은지 확인했다
- 힙독·sed -i·echo >>·&&·|| 를 편집기 + 파일 리스팅 + 분할 형태로 바꿨다
- 닫는 코드펜스가 빠져 뒤 200여 줄의 블록 종류가 뒤집혀 있던 곳을 포함해 3곳을 고쳤다

관문: check_body PASS · check_prose error 0 · check_evidence 두 프로젝트 문제 없음 ·
verify-tech-log-tree error 0 · verify-project-layout error 0 · 코드펜스 전수 0건

남은 것: B-0 주입은 keycloak-pattern 저장소의 소스를 고치고 이미지를 다시 구워야
해서 안 했다(unknown).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 19:38:12 +09:00
DongHyeonkaandClaude Opus 5 4a457afde9 fix(ssot): 기록에만 있던 2026-09-17 실측 넷을 SSOT 에도 넣는다
기록과 SSOT 를 대조해 한쪽에만 들어간 실측을 찾았다. 넷이 기록에만 있었다.

- A-0 시험 0c 의 `9 + 5 = 14` 와 DB 총계 대조
- A-8 의 `created_on` 은 그대로이고 `last_session_refresh` 만 117초 뒤로 간 것
- 토큰 길이가 판마다 다르다는 것 (`612자`·`758자` 대 `1187자`·`2043자`)

나머지 실측은 양쪽에 다 들어가 있는 것을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 16:05:39 +09:00
DongHyeonkaandClaude Opus 5 024362d096 fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과
keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지
밟았고 밟은 범위를 편마다 적었다.

명령이 못 도는 것을 고쳤다.

- kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다.
  라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다
- `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표
- busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다
- `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을
  못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다
- 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v`
- `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어
  그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다
- B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다
- D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는
  `kc-lab-edge` 의 `certbot.service` 다
- `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로
- `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다

결과가 조건부인 것을 갈랐다.

- readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다
- 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다
- A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다
- A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다

증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은
길이만 적었고 화면에 찍힌 토큰은 가렸다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 15:59:42 +09:00
DongHyeonkaandClaude Opus 5 ab59130196 chore: 이전 세션이 남긴 변경을 커밋한다
이번 파이프라인 작업과 무관하게 작업 트리에 남아 있던 것을 그대로 올린다.
사용자가 「전부 커밋」으로 정했고, 이번 작업과 섞이지 않게 커밋만 나눴다.

대부분은 clean-architecture-backend-template 의 그림 정본 재배치다 —
final/assets/diagrams/<이름>/ 에 있던 것이 CLAUDE.md 가 적은 배치인
final/assets/<이름>/ 로 옮겨졌고 .techviz/<이름>/ 이 함께 들어왔다.
삽입 줄의 대부분(3.15M)이 그 .techviz context.json 이다.

그 밖에 ca-tmpl·document-haness 의 정리, .claude/agents/ 열한 개,
writing-practitioner-guides 스킬, .playwright-mcp 세션 산출물,
scripts/check-ssot-facts.py 와 그 시험이 들어 있다.

이 커밋의 내용은 내가 만든 것이 아니라 이전 세션이 남긴 것이고 검증하지 않았다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:02:02 +09:00
DongHyeonkaandClaude Opus 5 2109f726fe feat(pipeline): keycloak-session-store 25편·virtualization 59편을 S3→S5→S6 으로 돌린다
기록 84편을 계약 에이전트로 다시 썼다. 기존 71편(kss 25 · virt 46)과, 계약에만
있고 안 쓰여 있던 새 글감 13편이다. 원장 84개를 열어 단계마다 스킬 영수증과 관문
종료 코드를 적었고 verify-pipeline-run.py 가 error 0 으로 닫는다.

SSOT 결함 둘을 고쳤다.

- kss 의 `약 58일` 이 반입 중 `약 59일` 로 바뀌어 있었다. 원 증거 파일이
  「남은 일수: 88일 … 실제 갱신까지 약 58일」로 산수를 직접 적는다. D-4a 쪽
  `약 59일` 은 강제 갱신 뒤(`VALID: 89 days`)라 맞는 값이라 그대로 뒀다.
- virt §198 의 `11.6GB` 는 §178 의 원 측정 `Mem: 11648`(MiB)과 어긋나는데
  원 가이드의 표기 그대로라 고치지 않고 쓰이는 자리에 대조를 적었다.

기록의 수치 오류 셋을 고쳤다 — CASE 요약의 「게스트 셋에 8240MB」(5120+3120 은
둘이다), k3s 편이 같은 것을 여섯·일곱·여덟로 세던 것, no-docker 편의 「셋을 더
든다」(§281 의 표는 네 행이고 디스크 행이 빠져 있었다).

계약을 셋 고쳤다.

- kss 의 sourceRepository 리비전이 cdac9b8 이었는데 그 커밋에는 docs/guides/**
  28개가 아예 없다. 9465582b 로 바꾸고, 반입한 바이트가 어느 커밋과도 같지 않다는
  것을 측정값과 함께 적었다 — 반입은 커밋이 아니라 그 시점의 작업 트리에서 떠 온
  것이다(kss 297/306 · virt 12/14 가 작업 트리와 같고, 200 커밋을 거슬러 전수
  대조했을 때 가장 가까운 커밋도 28개가 어긋났다).
- virt 계약이 「2026-09-11 재배분」이라고 적는데 SSOT 는 재배분 날짜를 적지 않고
  재배분 뒤 값은 이미 2026-09-10 측정에 찍혀 있다.
- kss 후보 대장이 지나친 절 아홉에 처분을 적었다(warn 9 → 0). 새 글감은 0건이고
  넷은 앵커가 h3 슬러그의 접두가 아니라 중간 토막이라 검사기가 못 본 것이었다.

style_profile.mjs 의 결함 둘을 고쳤다 — frontmatter 가 문장으로 세어져
(실측 398자짜리 「문장」 하나) 평균 길이를 기준 안으로 밀어 올리고 있었고,
engPerSent 의 분자는 목록을 포함한 글에서, 분모는 목록을 걷어낸 글에서 세고
있었다(Question 기록에서 11.94 → 3.86).

verify-pipeline.py 전 항목 PASS · error 0 · unittest 334건 OK.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-17 11:01:55 +09:00
DongHyeonkaandClaude Opus 5 d473609e0a fix(studio-save): 응답 껍데기를 벗기는 자리를 코드가 갖고, 계획마다 새 키가 나게 한다
① GET /documents/{id} 는 문서를 data.document 아래에 둔다(studio-v1.yaml:1831). A 가
   B-011 에서 data 를 문서로 보고 견줘 칸이 전부 「다르다」로 나왔다 — 벗기는 자리를
   코드가 갖고 있지 않아서 난 일이라 unwrap_document 를 둔다. 모양이 다르면 조용히
   넘기지 않고 거절한다. 잘못된 층을 견주는 것보다 멈추는 편이 낫다.

   그리고 updatedAt 을 서버가 붙이는 칸으로 옮겼다. 매 저장마다 「서버가 더 줬다」로
   나오면 그 칸을 아무도 안 읽게 된다. variantIds 는 안 옮겼다 — 계약이 입력으로 받는
   칸인데 이 어댑터가 안 보내므로 보여야 한다.

② CR-007 때문에 같은 키를 두 번 못 쓴다. 런 식별자를 계획 파일에 적고 키에 섞는다.
   같은 계획을 두 번 보내면 같은 키, 계획을 새로 만들면 다른 키다. 보낼 때 만들면
   재시도가 곧 중복 생성이 되므로 만드는 자리는 계획을 짜는 순간 하나뿐이다.
   --run-id 를 안 주면 예전 키 모양 그대로라 옛 계획을 안 깬다.

python3 -m unittest discover -s scripts/tests — Ran 291 · OK (skipped=13)
PIPELINE CONTRACT: PASS

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 13:18:30 +09:00
DongHyeonkaandClaude Opus 5 1be1c8ea6d fix(studio-save): 항목 id 를 다시 매기는지는 종류마다 다르다
A 가 B-011 운영 실행에서 봤다. PROJECT_DECISION 은 보낸 uuid5 여섯이 글자 그대로
돌아온다. QUESTION 은 서버가 uuid4 로 다시 발급한다(C · V-009 열 번째). 둘 다
OrderedText 배열인데 서버가 다르게 다룬다.

한 벌로 묶어 빼면 DECISION 에서 볼 수 있는 것을 안 보게 된다 — 서버가 언젠가
DECISION 도 재발급하기 시작해도 아무도 모른다. 그래서 종류로 갈랐다.

  QUESTION          다시 매긴다 → id 를 뺀다 (쟀다)
  PROJECT_DECISION  다시 안 매긴다 → 그대로 본다 (쟀다)
  그 밖              안 쟀다 → 빼지 않고 그대로 보고, 안 쟀다는 것을 값에 적는다

안 잰 채로 빼면 「안 봐도 되는 것」으로 굳는다. 「못 보는 것」과 「안 봐도 되는 것」은
다르다 — itemIdBehaviourUnmeasured 가 어느 칸을 왜 그대로 견줬는지 적는다.

python3 -m unittest discover -s scripts/tests — Ran 286 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:19:55 +09:00
DongHyeonkaandClaude Opus 5 325b6008ab fix(studio-save): 선택지 절의 세 가지 모양을 전부 읽는다
A 가 전수로 재서 알려 줬다. 내가 목록형 기록 하나를 보고 「이미 잰 값은 안 바뀐다」로
일반화한 것이 틀렸다 — 문단형인 openquestion-* 계열이 다섯 칸을 통째로 잃고 있었다.

`## 선택지` 를 쓰는 모양이 셋이다. `### N. 제목` · `**굵은 한 줄**` · 표시 없이 첫 줄이
제목인 문단. 첫째만 읽고 있었고, `_ordered` 의 「글이 있는데 항목 0 개면 거절」이 이 칸에는
안 걸려서 읽지도 않고 막지도 않는 상태였다.

전수로 다시 쟀다 (0d58881 대 지금):

  배열 칸이 있는 기록 150건 · 값이 달라진 것 83건 · 거절 0건
  종류별 — decision 29 · question 26 · reference 28

고친 뒤에도 0 인 칸이 24건 남는데 전부 REFERENCE 의 rules 다. 그것은 다음 배치 16번이고
설계 조건(「없는 규칙」과 「다른 모양으로 쓴 규칙」을 가른다)이 붙어 있어 안 건드렸다.

options 의 거절 가지는 이제 안 닿는다 — 세 번째 모양이 글이 있으면 언제나 항목을 하나는
만든다. MAX_KEY_LENGTH 와 같은 자리라 지우지 않고 그렇게 주석에 적었다.

python3 -m unittest discover -s scripts/tests — Ran 284 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:11:17 +09:00
DongHyeonkaandClaude Opus 5 252f14c88e fix(studio-save): 양쪽이 빈 칸을 「같다」에 섞지 않는다 (C 의 B-006 ② 수정안)
C 가 V-009 에서 걸렸다 — P-QUESTION-01 의 「되읽기 차이 0」이 배열 다섯이 전부 비어
견줄 것이 없어서 나온 값이었다. same: true 가 「13칸이 다 맞았다」로 읽히는데 실제로는
찬 칸만 맞은 것이다. R14 와 같은 모양이 대조기 안에 한 번 더 있었다.

- _is_empty 로 양쪽이 빈 칸을 갈라 emptyBoth 로 낸다. 한쪽만 비면 차이다
- comparedFields 에서도 빠지고 comparedCount 가 실제로 견준 수를 낸다
- 사람이 보는 한 줄에 「N칸 중 M칸을 견줬다. K칸은 양쪽이 비어 견줄 것이 없었다」

C 의 수정안은 comparedFields 를 개수로 바꿨는데 목록을 유지하고 개수를 따로 뒀다 —
이미 목록으로 읽는 회귀가 있고, 어느 칸을 못 봤는지는 이름이 있어야 안다.

C-B006 §4 의 대조를 돌렸다. 손으로 센 찬 칸과 도구 출력이 맞는다.

  CONCEPT   6 / 6      REFERENCE 10 / 10      QUESTION 11 / 11

묶음은 B 가 읽을 수 없어(계약 §3.2) 같은 종류의 기록으로 셌다.

python3 -m unittest discover -s scripts/tests — Ran 282 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:06:12 +09:00
DongHyeonkaandClaude Opus 5 f765a79d6f fix(studio-save): PROJECT_DECISION 의 칸을 계약에 맞추고 지울 수 있게 만든다
B-010 §4.1 에서 찾고 안 고친 것이다. 그때 안 고친 이유는 예산이 아니라 측정이었고,
이번에는 그 런에서 잰다.

- 결정문→statement · 영향→consequences(OrderedText 배열) · 판단 이유→rationale
- 근거→basis 를 뺐다. ProjectDecisionInput 에 그 칸이 없고 unevaluatedProperties:
  false 라 보내면 거절된다. 기록의 ## 근거 절은 그대로 둔다
- decisionStatus·decidedOn 을 frontmatter 에서 읽는다. enum 밖의 값은 지어내지 않고
  거절한다 — 운영에 초안을 만들어 놓고 422 를 받는 것보다 낫다

그리고 절의 모양이 둘이었다. 사실·가정은 `- ` 목록이고 DECISION 의 영향은 빈 줄로 나뉜
문단이다. 목록만 읽어서 문단으로 쓴 절이 조용히 [] 가 되고 있었다 — minItems 가 없어
그대로 저장되고 내용만 사라진다. 목록을 먼저 보고 없으면 문단으로 나누며, 글이 있는데
항목이 0 개면 거절한다. QUESTION·REFERENCE 의 이미 잰 값은 안 바뀐다(목록이라 첫 갈래에서
끝난다).

--harness-test 의 PROJECT_DECISION 거절은 우회하지 않고 값을 받게 했다. --project-id 는
사람이 Studio 목록에서 읽은 uuid 다 — 어댑터가 이름을 uuid 로 바꾸지 않는다. 없으면
여전히 거절한다. projectId 는 [string, "null"] 이고 계약이 저장 시점에는 강제하지 않으니
null 로도 만들어지고, 만들어지면 지울 수 없다. 되읽기가 그 값을 확인한다.

python3 -m unittest discover -s scripts/tests — Ran 278 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 11:03:03 +09:00
DongHyeonkaandClaude Opus 5 0d588814cc fix(studio-save): CONCEPT·REFERENCE 의 칸이 계약과 달랐다
C 가 V-009 에서 운영에 직접 걸어 찾았다. QUESTION 에서 고친 것과 같은 결함족이다.

① CONCEPT — basisVersion 이 required 인데(studio-v1.yaml:979) 어댑터가 절만 봤다.
   값은 frontmatter 에 있다. 상한 120자를 넘으면 조용히 자르지 않고 거절한다.

② REFERENCE — 셋이 한꺼번에 틀렸다(studio-v1.yaml:955-963).
   이름: appliesWhen 이 아니라 applyWhen 이다 — nextValidation 과 같은 자리다
   모양: rules 는 ReferenceRule 배열, applyWhen·exceptions·examples 는 OrderedText
         배열이다. purpose 만 문자열이다
   없는 칸: verifiedOn 이 required 인데 아예 없었다. 기록이 안 적었으면 null 로 둔다

③ 되읽기 비교가 항목 id 를 견주고 있었다. 보내는 것은 uuid5 이고 서버는 저장하면서
   uuid4 를 새로 발급한다 — 그대로 두면 모든 QUESTION·REFERENCE 저장이 「차이 있음」이
   된다. 칸 전체를 빼지 않고 id 만 뺐다. text·title·body·order 는 계속 대조하므로
   항목이 빠지거나 순서가 바뀌는 것은 여전히 걸린다. 무엇을 왜 안 보는지는
   itemIdsNotCompared 에 값으로 적는다.

python3 -m unittest discover -s scripts/tests — Ran 273 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:35:19 +09:00
DongHyeonkaandClaude Opus 5 14137382fe fix(tests): 새 시험의 픽스처를 TechLog 로 옮겨 PIPELINE CONTRACT 를 되돌린다
첫 제출에서 verify-pipeline 의 FORBIDDEN_LITERAL 이 새 시험을 걸었다. 픽스처가
옛 저장소 이름과 같은 이름의 docs 프로젝트를 가리키고 있었다.

글자를 쪼개 피하지 않았다. verify-pipeline.py:172 자신이 쓰는 수법이지만 그건 검사기가
물으려던 것에 답하는 게 아니라 글자만 피하는 것이다. 재는 것이 「비교를 돌렸나」이지 그
기록의 내용이 아니므로 프로젝트를 TechLog 로 옮겼다.

양성 대조를 함께 넣었다 — available: true 만 재면 픽스처가 조용히 같아졌을 때 시험이
초록인 채로 아무것도 안 재게 된다. warnings 가 비지 않았는지와 그중에 「유보 감소」가
있는지를 잰다.

검사기가 「옛 저장소 이름에 의존한다」와 「그 이름의 docs 프로젝트를 가리킨다」를 못 가르는
것은 고치지 않고 보고서 §7.6 에 발견으로 적었다. 검사기 수정은 C 몫이다.

  PIPELINE CONTRACT: PASS
  python3 -m unittest discover -s scripts/tests — Ran 266 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:25:03 +09:00
DongHyeonkaandClaude Opus 5 0f753c3be5 fix: TechLog 리비전 셋을 매니페스트에 맞추고, 「경고 0건」을 두 가지로 가른다
부채 ④. TechLog 의 tech-log-design-package 가 ca1bbfe·1aae8dc·ffa088b 를 적고 있었는데
매니페스트는 그 뒤 다시 만들어져 세 파일 모두를 tech-log-frontend @ 0d4d1e5 로 적는다.
옛 셋은 tech-log-frontend 에 없다 — 옛 매니페스트가 안 남아 그 사이 계약 내용이 바뀌었는지는
대조할 수 없고, 그 사실을 verified 에 적었다. 기록의 「검증 환경」이 적은 리비전은 그때 잰
조건이라 고치지 않는다. 파일 sha256 셋을 함께 적어 리비전 문자열이 아니라 내용에 못박는다.

  check_evidence --repo TechLog     exit 0 「문제 없음」 (3 → 0)
  review-package.py TechLog         exit 0 · 관문 10 전부 exit 0

R14. 이 묶음의 막는 경고는 전부 편집 전후 보존 비교에서 나오고(review-package.py 의
_warn 자리 둘이 모두 if preservation.get("available") 안이다) 그 비교는 --before 를 줘야
돈다. 빼면 검토 관문이 아무 줄도 안 남기고 통과한다.

- --before 를 필수로 만들지 않는다. 새로 쓴 기록엔 윤문 전 사본이 없어 첫 기록이 막힌다.
  없다는 것을 적고 출력한다
- preservation.reason 으로 두 원인을 가른다 — NO_BEFORE · CHECKER_UNREADABLE
- 묶음이 자기 입력을 적는다. 줬으면 before·beforeSha256, 안 줬으면 명시적 null.
  빠진 칸과 null 은 다르다
- 종료 코드는 양쪽 다 0 이다. 새 관문이 아니라 읽는 계약이라 갈리는 것은 문구다.
  _review_gate 의 stderr 와 계획 한 줄 요약 둘 다 가른다
- B-B004c-plans/verdict.json 을 지웠다. 옛 묶음 53ce6a5b… 에 묶여 있는데 지금 계획은
  0beb5420… 이라 읽히지 않는다 — 검토를 받은 것처럼 보이는 파일이 남는다

python3 -m unittest discover -s scripts/tests — Ran 266 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:18:45 +09:00
DongHyeonkaandClaude Opus 5 d2855d1e6c fix(studio-save): 정상 입력이 저장까지 못 가던 이유 셋을 고친다
C 가 운영 Studio 에 직접 걸어 찾았다. 셋 다 서버가 아니라 이 어댑터가 만든 요청의 문제다.

① 422 IDEMPOTENT_REQUEST_MISMATCH 가 어디에도 안 적혀 있었다. 코드만 보면 본문이
   틀렸다고 읽는다 — 서버 문구도 무슨 일이 있었는지만 말한다. SERVER_ERRORS 표를
   만들어 계획의 expect 에 실었다. 표에 없는 코드는 Refused 다.
   키 설계는 안 건드렸다. 본문 해시를 넣으면 「재시도가 문서를 둘 만들지 않는다」가
   깨진다 — idempotency_key 주석에 이 설계가 막는 것을 적어 두었다.

② --harness-test 가 제목만 바꾸고 slug 는 원본 그대로였다. slug 에 유일성 제약이
   있어 409 DB_UNIQUE_VIOLATION 이다. 시험 초안은 반드시 원본에서 나오므로 이 충돌은
   필연이다. 접두사는 두 번 붙지 않고, 길이 상한을 넘기면 자르지 않고 거절한다.

③ QUESTION 의 칸이 문자열이 아니었다. facts·assumptions·unknowns·constraints 는
   OrderedText 배열, options 는 QuestionOption 배열, 마지막 칸 이름은 nextValidation
   이다(studio-v1.yaml:1013-1040). id 는 uuid5 로 만든다 — 난수면 같은 기록을 다시
   계획할 때 본문이 달라져 저장 멱등 키가 흔들린다.

곁다리로 relations 가 [] 이지 None 이 아닌 이유를 주석에 남기고, 정리 단계의 값을
계약이 적은 400 이 아니라 C 가 관측한 422 로 고쳤다. 404 는 지워졌다는 뜻이 아니다 —
종류를 어긋나게 보내면 404 뒤 GET 이 200 이다.

python3 -m unittest discover -s scripts/tests — Ran 257 · OK (skipped=13)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 09:07:38 +09:00
DongHyeonkaandClaude Opus 5 05e96b5add fix(cabt): 기록이 인용한 것을 SSOT 가 안 담고 있었다
check_evidence --repo 이 clean-architecture-backend-template 에서 141건을 세고 있었다.
124건 전부를 고정 리비전 21234e38 에 대조했더니 날조는 0 이었다 — 인용은 맞고 없던
쪽이 SSOT 였다. 그래서 SSOT 를 보강한다.

넣는 것은 기록이 옮겨 적은 문장이 아니라 저장소 원문이다. 기록을 복사해 넣으면
검사기는 초록이 되지만 옮겨 적기가 어긋나도 더는 못 잡는다. 원문을 넣으면 어긋난
기록은 계속 걸린다.

- 코드 124건 → 리프 절 일곱 곳에 원문 30조각(300줄). 자리는 기록의 source 앵커와
  소스 파일의 모듈을 교차시켜 정했고, 둘이 갈린 다섯은 모듈을 따르고 그 사실을 적었다
- 식별자 13건 → spring.factories · MethodSecurityConfig 원문과 줄여 적힌 경로의 전체 경로
- source 앵커 4건 → 계약이 이미 적고 있던 SSOT 앵커를 기록 frontmatter 에 맞췄다.
  맨 앵커를 한 줄씩 앞에 둔다 — `_record_sources` 가 `- <공백 없는 한 덩어리>` 만 잇달아 읽는다
- 인용 4건 → concept-signed-cursor-structure 의 `{ ... }` 생략을 저장소 원문 형태로 고쳤다.
  생략한 자리는 `…` 로 남기고 무엇을 줄였는지 본문에 적는다

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 01:10:51 +09:00
DongHyeonkaandClaude Opus 5 96d89ec6da fix(studio-save): 정리 단계가 본문 없이 나가고 종류를 안 봤다
DELETE 가 본문을 요구한다. requestBody: required: true 이고 컨트롤러가 @RequestBody
ExpectedVersionRequest 를 받는다. 본문 없이 보내면 400 이고 초안이 남는다. expectedVersion
은 verify 가 읽은 값이어야 한다 — 만든 뒤 한 번 더 저장하므로 만들 때 version 이 아니다.

경로가 종류를 안 보고 무조건 cases 로 나갔다. 서버는 종류를 조회 조건에 넣고 그 이유를
코드에 적어 두었다 — 그렇게 하지 않으면 Case 경로로 Reference 를 지울 수 있게 되기
때문이다. 경로가 종류와 어긋나면 DOCUMENT_NOT_FOUND 가 나고 초안이 남는다.

시험 초안을 CASE 로 쓴 판단은 맞았지만 코드가 그 판단에 기대고 있었다. 종류별 경로를
표로 만들고, Decision 처럼 프로젝트 id 가 필요한 것과 모르는 종류는 거절한다 — 조용히
받으면 못 지우는 초안이 운영에 남는다.

CSRF 헤더 이름과 404 의 뜻도 계획에 적었다. X-XSRF-TOKEN 이면 403 이다.

이 고침은 계약에 맞춘 것이지 걸어 본 것이 아니다. 계획대로 보내 본 사람은 아직 없다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-11 00:36:29 +09:00
DongHyeonkaandClaude Opus 5 e1404f8a3d chore(skill): publishing-tech-log-to-studio 를 1.0.1 로 올린다
산문의 사실 하나가 틀려서 고쳤다. 내용이 바뀌면 버전이 따라 움직여야 통과 판정을 그
버전에 묶을 수 있다 — 그러라고 metadata.version 을 만들었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:19:13 +09:00
DongHyeonkaandClaude Opus 5 aae43f4f99 fix: 우연히 지켜지던 멱등성을 회귀로 못박고, 틀린 문장의 출처를 고친다
저장 멱등 키가 필드 순서에 안 흔들리는 것은 sort_keys=True 덕이고, 그것은 JSON 을
안정적으로 만들려고 넣은 것이지 멱등성을 노린 것이 아니다. 다시 직렬화한 재시도가 새
저장으로 잡히지 않게 지키고 있었는데 그 성질을 명시한 시험이 없었다. sort_keys 를 빼도
아무 시험이 안 깨졌다. 의도하지 않은 성질에 기대는 코드는 회귀로 못박지 않으면 다음
사람이 지운다.

대조군도 함께 넣었다. 순서에 안 흔들린다고 내용에도 안 흔들리면 그건 다른 결함이다.

그리고 「Decision 은 계약에 삭제 경로가 없다」의 출처가 스킬이었다. 내가 코드 주석과
보고서로 옮긴 문장이 거기서 왔다. 다섯 종류 전부의 삭제 경로를 세어 표로 바꾸고 확인한
리비전(tech-log-backend @ a000f87)을 함께 적었다 — 서버를 서술할 때 리비전이 없으면
서버가 바뀌어도 아무도 모른다.

지워지지 않는 것은 따로 있다. 게시한 적이 있으면 DOCUMENT_PUBLISHED, 관계로 참조되면
DOCUMENT_IN_USE 다. 그것이 원래 말하려던 것이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:19:01 +09:00
DongHyeonkaandClaude Opus 5 4e9226a57d fix(studio-save): 확인 없이 옮긴 문장을 고치고, 안 닿는 가지에 유효 범위를 적는다
「Decision 은 계약에 삭제 경로가 없다」고 적었는데 틀렸다. ManagementDocumentController
:123 에 DELETE /v1/studio/projects/{id}/decisions/{decisionId} 가 있다. 스킬의 문장을
확인 없이 옮겼다. 시험 초안을 CASE 로 고른 이유는 삭제 경로 때문이 아니라 그 기록이 이
배치가 만든 것이라 남의 것이 아니어서다.

멱등 키 길이 검사는 지금 정책에서 안 닿는다. 키가 studio-{op}-{32자}(+{32자})라 길이가
사실상 고정이고 저장 키가 77자다. 죽은 코드가 아니라 키 정책이 바뀌면 살아나는 방어선이라
지우지 않고, 언제 살아나는지를 주석에 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:16:55 +09:00
DongHyeonkaandClaude Opus 5 c66eee6cb2 feat(studio-save): 시험 초안 계획에 두 번째 저장과 정리 단계를 넣는다
만들기만 하면 이 런에서 expectedVersion·VERSION_CONFLICT 경로가 한 번도 안 돈다. 새로
만든 뒤 한 번 더 저장해 낙관적 락을 태운다. expectedVersion 은 만들기 응답이 준 version
이고, 모른 채 보내면 서버가 0 으로 채워 늘 충돌한다.

만든 시험 초안을 그 자리에서 지운다. DELETE /api/v1/studio/cases/{id} 가 실재하는 것을
소스와 openapi 에서 확인했고 게시 가드에 안 걸리는 것도 확인했다. 시험 초안의 종류를
CASE 로 고른 이유가 그것이다 — Decision 은 계약에 삭제 경로가 없다.

정리 단계는 --harness-test 일 때만 낸다. 진짜 기록에는 삭제 요청을 만들지 않는다.

삭제가 409 를 낼 수 있다 — 공개된 기록이거나 참조하는 곳이 있는 경우다. 둘 다 사람이
화면에서 처리하고, 계획에 그렇게 적었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:15:07 +09:00
DongHyeonkaandClaude Opus 5 f16785b93e feat(studio-save): 게시된 기록을 코드가 저장 대상에서 뺀다
사용자가 studio:publish 권한 분리를 유예하고 write 권한을 가진 실계정으로 저장하기로
했다. 설계의 명시적 예외이고, 설계가 프롬프트 통제를 인정하지 않는 이유가 여기 그대로
적용된다 — 「이미 게시된 게시물은 건드리지 않는다」를 문장이 아니라 코드로 만든다.

한 번이라도 게시한 문서는 게시를 취소해도 삭제가 409 로 거절된다. public 이 찼거나
status 가 게시 중이면 저장 대상에서 뺀다. 저장소에서 두 표시가 같은 17건을 가리키지만
하나만 차 있어도 게시로 본다 — 한쪽이 뒤늦게 채워지는 경우를 놓치지 않는다.

frontmatter 는 저장소가 아는 것이지 서버가 아는 것이 아니다. 그래서 계획의 첫 단계가
서버에 게시 상태를 묻고, PUBLISHED 면 저장 단계로 넘어가지 않는다. 새 문서를 만드는
계획에는 그 단계가 없다 — 아직 없는 문서에는 물어볼 게시 상태가 없다.

시험 초안에 [HARNESS-TEST] 접두사를 붙일 수 있게 했다. 나중에 사람이 눈으로 가린다.

무인 저장은 그대로 꺼져 있다. CR-001 이 유예됐다는 것은 무인 저장을 켠다는 뜻이 아니다 —
사람이 보는 앞에서 저장하는 것과 사람 없이 저장하는 것은 다른 이야기다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 19:11:25 +09:00
DongHyeonkaandClaude Opus 5 87d70e7c80 feat(check-preservation): 편집이 핵심 칸에 측정 주장을 새로 더했는지 본다
check-core-support 는 근거 목록이 비었는데 측정을 주장하는 것을 본다. 근거가 차 있는
기록에 그 근거가 지지하지 않는 결론을 더하는 편집은 그 규칙 밖이다 — 출처가 있다는 것과
그 출처가 그 주장을 지지한다는 것은 다르다.

「측정 주장이 있으면 경고」로 가지 않았다. 그건 저장소의 정상 기록 다수에 걸리고, 모든
기록에 걸리는 경고는 어느 기록에 대해서도 아무 말을 하지 않는다. 편집이 핵심 칸에 측정
주장을 새로 더했는지만 본다 — 적용 범위 검출과 같은 모양이고 더해진 것을 본다.

판정은 check-core-support 의 목록을 그대로 불러 쓴다. 두 곳에 두면 갈린다.

판정하지 않는다. 근거가 이 주장을 지지하는지는 근거를 읽어야 알고, 그것은 검토의 몫이다.

채택 편집 100쌍에 한 번도 안 걸린다. 그 쌍들은 문제·결론 칸이 없는 조각이라 대상이
아니고, 회귀에 그것도 넣었다.

곁들여 check-core-support 에 --file 을 더했다. 다른 검사기들이 이미 받은 것이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 17:40:02 +09:00
DongHyeonkaandClaude Opus 5 ad055fb3b9 feat(scripts): 핵심이 측정을 주장하는데 근거 목록이 비었는지 본다
후보를 둘 버렸다. 처음에 「핵심의 수치가 근거에 없으면」으로 만들었는데 사례에 아라비아
숫자가 하나도 없다 — 「응답시간의 p99 가 절반이 됐다」. 275건에 돌려 오탐 0 · 검출 0 이었다.

두 번째는 비교 낱말만 봤다. 275건에서 둘이 걸렸고 둘 다 오탐이었다 — 「계약의 절반은
라우트 레지스트리에서 유도한다」·「가드는 절반만 존재합니다」. 한국어에서 「절반」은
측정이 아닌 쓰임이 흔하다.

그래서 성능을 재는 명사와 비교하는 말이 함께 나올 때만 측정 주장으로 본다. 그 주장을
하면서 evidence 가 비어 있으면 핵심에 근거가 없는 모양이다. 근거를 대면 통과한다 —
막는 것은 주장이 아니라 근거 없는 주장이다.

경고를 만드는 것으로 끝내지 않고 review-package 의 관문에 넣었다. 관문이 exit≠0 이면
studio-save 의 approved() 가 그 묶음을 거절한다. 만드는 것과 막는 것은 둘 다 있어야
한 쌍이다.

저장소 실제 기록 275건에 하나도 안 걸린다. 문제·결론에 수치를 적는 건 정상 기록이 늘
하는 일이라 여기가 과잉 차단이 가장 나기 쉬운 자리다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 17:20:47 +09:00
DongHyeonkaandClaude Opus 5 e3230ce5ed feat(scripts): 경고를 읽는 장치가 아니라 만드는 장치를 더한다
남은 셋은 묶음 경고가 0건이라 게이트를 아무리 조여도 지나간다. 뿌리가 하나다 — 지금
검사기는 사라진 것과 새로 생긴 보호 구간을 보고, 범위가 넓어진 것과 정본을 안 거친 것을
보는 자리가 없었다.

그림이 정본을 거쳤는지 본다. SVG 는 바뀌었는데 spec.json 은 그대로면 그 그림은 정본에서
나온 것이 아니고, 화살표 뒤집기가 그 모양이다. spec 의 간선과 SVG 의 경로를 직접 견주려면
렌더러가 id 를 어떻게 붙이는지 알아야 하는데, 정본을 거쳤는지만 보면 몰라도 된다.
작업 트리와 이력 두 자리를 본다. 정본이 없는 그림은 볼 것이 아니라 세기만 한다.

적용 범위를 넓히는 말이 새로 들어왔는지 본다. 로컬에서 확인했다에 운영 환경에서도를
더하기만 하면 유보도 보호 구간도 안 바뀐다 — 지운 것이 없기 때문이다. 유보 감소의
반대편이고, 판정하지 않고 경고로 올린다.

경고 생산자를 더하는 것이 과잉 차단이 생기는 자리라 후보마다 채택 편집 100쌍을 돌렸다.
error 도 경고도 안 늘었다. 정상 편집에 경고가 붙으면 그것도 사실상 차단이다.

목록에 운영·항상 같은 흔한 말이 들어가서, 원래 있던 말을 두고 주변만 고쳐도 걸리는지
보는 대조군을 회귀에 넣었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 16:33:47 +09:00
DongHyeonkaandClaude Opus 5 16cbe141a0 fix: 고친 자리에 같은 버그를 다시 넣었고, 게이트가 관측 창을 덮었다
한글 수사의 뒤 경계가 세는 말 다음의 조사를 낱말의 일부로 봤다. 「다섯 개다」·「다섯 건을」·
「여섯 장이」가 전부 안 걸린다. 아라비아 숫자 쪽에서 같은 이유로 「500행」·「5개다」를
놓쳤던 것을 고쳤는데, 그 고침을 한글로 옮기면서 다시 넣었다. 회귀가 초록이었던 건 시험
문구가 전부 조사 없이 끝나서다.

뒤 경계를 풀었더니 채택된 편집 둘이 새로 막혔다. 「여덟 자리 → 여덟 곳」이다. 숫자가 안
바뀌었고 세는 말이 바뀌었다 — spatial-metaphor 를 고치는 정상 편집이다. 그래서 잡는 것을
수사로 좁히고 세는 말은 문맥으로만 본다. 대안도 긴 것부터로 정렬했다.

대조군을 주변이 바뀌는 쌍으로 다시 짰다. 같은 문자열은 어떤 검사기든 조용해서 대조가 되지
않는다. 그 대조군이 없었으면 위 오탐을 못 봤다.

그리고 저장 게이트가 그림 붙은 기록을 전부 막고 있었다. 저장소의 그림에 종류 표시가 하나도
없어서 그림이 붙으면 무조건 경고가 하나 붙는다. 게이트가 틀린 게 아니라 그 경고가 어느
기록에 대한 정보도 아니다 — 모든 기록에 걸리는 경고는 어느 기록에 대해서도 아무 말을 하지
않는다. 저장소 전체의 미비는 세고 보고하되 저장을 막지 않는다. 조용히 빼지도 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 16:15:04 +09:00
DongHyeonkaandClaude Opus 5 20c8535169 feat(studio-save): 검토로 보낸다는 것은 저장이 막힌다는 뜻이어야 한다
시도 1 이 「8건이 PASS 로 나가지 않는다」를 세웠는데 코드 3건에 대해서만 성립했다. 경고는
실렸는데 그것을 읽고 막는 코드가 없었다. 검토로 라우팅한다는 것이 실제로는 경고를 붙여서
통과시키는 것이었다. 막지 않으면 라우팅이 아니라 주석이다.

경고가 있으면 항목마다 판정을 받고 전부 PASS 일 때만 저장이 나간다. 판정 파일이 없으면
거절한다 — 검토를 안 받은 것과 검토가 통과시킨 것은 같은 결과일 수 없다. 판정이 안 붙은
경고가 있어도 거절한다 — 빠뜨린 것과 통과시킨 것은 다르다.

FAIL 과 UNKNOWN 은 둘 다 막되 문구가 갈린다. 근거가 모자라 판정을 못 한 것과 근거를 읽고
틀렸다고 본 것은 다음에 할 일이 다르다.

판정이 어느 경고에 붙은 것인지 정하려고 경고에 키를 붙였고, 그 키를 읽는 코드를 같은
변경에 넣었다. 내용이 바뀌면 키도 바뀌어 옛 판정이 다른 경고에 붙지 않는다.

경고가 없으면 판정 파일 없이 그대로 나간다. 정상은 이 게이트에 안 걸린다 — 채택 편집
100쌍에서 91쌍이 판정 없이 나가고 9쌍이 판정을 받아야 한다. 판정을 받아야 한다는 것은
막힌다는 뜻이 아니다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 15:53:26 +09:00
DongHyeonkaandClaude Opus 5 845ee89054 feat(scripts): 놓친 8건을 코드로 막거나 검토로 보낸다
먼저 쟀다. 경고를 일괄 차단으로 올리면 채택된 편집 9건이 막힌다 — V-004 에서 고친 바로
그 9건이다. 그래서 경고는 검토로 보내고 차단은 검토가 판정할 때 일어나게 뒀다.

코드로 막은 것 셋. code[] 리비전 — check_evidence 는 리비전이 있는지만 보고 인용한 코드가
그 리비전에서 왔는지는 안 본다. 이 배치에서 실제로 났고 사람이 손으로 잡았다. 한글 수사와
그 경계 — 「다섯 개 → 여섯 개」는 결정적이다.

앵커 검사기의 오탐을 0 으로 만드는 데 시간의 절반이 갔다. 처음 판이 저장소 전체에서
401건을 냈고 전부 오탐이었다. code[] 는 한 모양이 아니다 — 심볼, 축약 경로, 줄 범위,
호스트 절대 경로, 설정 키가 섞여 있다. 축약 경로를 「없다」로 세면 있는 코드를 없다고 하는
것이고 그게 채택된 편집 아홉 건을 막았던 실패와 같은 모양이다. 판정할 수 있는 것만
판정하고 못 보는 것은 세어서 낸다.

검토로 보낸 것 다섯. D2 는 아무 계수도 안 움직이던 자리였다 — 수치도 인용도 없이 산문만
더하면 보호 구간 비교에 잡힐 것이 없다. 1인칭 표지가 늘어난 것만 보고 그 문장을 짚어 준다.
판정이 아니라 라우팅이다.

만들다 버그를 찾았다. 경고가 인용하는 문장이 파일 첫 문단에서 한 글자씩 깎이고 있었다.
rfind 가 -1 을 낼 때 +2 를 해서 1 이 됐다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 15:47:54 +09:00
DongHyeonkaandClaude Opus 5 230ad874ca fix(tests): 원장 회귀가 프로젝트 이름을 적지 않게 한다
verify-pipeline.py 의 FORBIDDEN_LITERAL 가드가 scripts/ 안에서 저장소
체크아웃 이름을 금지한다. B-005 의 회귀가 그 이름으로 실제 원장을 찾다가
PIPELINE CONTRACT 를 FAIL 로 만들었다. glob 으로 아무 원장이나 고르게
바꿨고 시험의 뜻은 그대로다 — 더한 칸이 있어도 검사기가 읽는가.

병합에서 드러난 계약 충돌이라 통합 브랜치에서 조정한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GksxvQvM6A85xy8viYtWk6
2026-09-10 15:39:14 +09:00
DongHyeonka fdb38e9012 Merge branch 'harness/B-implementation' into harness/A-integration 2026-09-10 15:37:56 +09:00
DongHyeonkaandClaude Opus 5 96d7fbc57a fix(run-ledger): 파일은 안 깨지는데 두 세션의 기록이 섞였다
이어받은 단계에 앞 세션이 관문을 써도 그대로 들어갔다. 원자성 문제가 아니다 — 파일은
온전한 채로 두 세션의 기록이 섞인다. 그리고 관문마다 누가 적었는지가 없어서 나중에
원장을 읽어도 가릴 수 없었다.

begin 이 세대를 올리고 주인을 적는다. 낮은 세대나 다른 주인의 쓰기는 거절한다.
bin/task.py 의 attempt 와 같은 자리다.

gate 와 end 도 --session 을 받아 적는다. 관문마다 session·generation·at 이 남고
end 는 finishedBy 를 남긴다.

주인이 없는 단계는 그대로 받는다. 세션을 안 쓰는 단일 세션 사용이 깨지지 않는다.
회귀에 그 대조를 넣었다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 13:33:10 +09:00
DongHyeonkaandClaude Opus 5 8ca822dc4d feat(scripts): 런 원장을 원자적으로 쓰고 끊긴 자리에서 잇는다
원장을 손으로 써 왔다. 그래서 셋이 없었다. 쓰는 도중에 끊기면 반쪽 파일이 남고, 단계마다
시작·끝이 없어 「돌다 말았다」와 「아직 안 시작했다」가 PENDING 하나로 같아 보이고, 런 도중에
끼어든 일이 어디에도 안 남는다.

bin/task.py 가 이 계약의 초안이다 — flock 을 잡고 임시 파일에 완성한 뒤 os.replace 로 바꾸고
startedAt 부터 지금까지를 누적에 더한다. 그 규약을 runs/ 쪽으로 옮겼다. 새로 만든 것이 아니다.

끊긴 단계를 새 세션이 다시 열면 그 구간을 닫고 잇되, 누적에만 더하지 않고 interruptions 에
따로 적는다. 닫은 구간에는 세션이 죽어 있던 시간이 섞인다 — 「이 단계가 오래 걸렸다」와
「중간에 끊겼다」는 다른 말이다.

라이더에 자리를 만들었다. 이 배치에서 검증 한 작업의 누적 24분 가운데 17분 50초가 라이더였는데
상태 파일에 그 구분이 없었다. 단계가 아니라서 어느 칸에도 안 남던 것이다.

상태 전이를 거절한다 — 이미 있는 런을 덮어쓰기, 단계 겹쳐 열기, 건너뛸 수 없는 단계 건너뛰기,
사유 없는 SKIPPED, 영수증 없는 DONE, 종료 코드 없는 관문.

더한 칸이 있어도 기존 검사기가 그대로 읽는다. 통과한 원장에 새 칸을 더해도 통과하는 것을
확인했다. 손으로 쓴 원장도 계속 유효하고 이 도구는 선택적으로 부른다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 13:23:04 +09:00
DongHyeonka f216fce5e6 Merge branch 'harness/C-verification' into harness/A-integration 2026-09-10 11:37:21 +09:00
DongHyeonkaandClaude Opus 5 14afe94d76 검사: 관문이 어느 대상에 돌았는지 보고, 필수 내용 검사를 전체 훑기에 넣는다
R5 — 원장은 관문의 명령과 종료 코드를 적는데 **그 명령이 어느 대상에 돌았는지는 아무도
안 봤다.** 다른 프로젝트에 돌려 받은 exit 0 을 적어도 통과로 셌다. `_wrong_target()` 이
`docs/<이름>/` 경로와 명령 인자의 프로젝트 이름 둘 다 본다.

  정상 원장 넷            새 error 0 (B 의 document-haness 원장 PASS · warn 3)
  프로젝트 인자를 바꾼 원장  FAIL 「관문이 다른 대상에 돌았다」
  기록 경로를 바꾼 원장     FAIL

**유효 범위를 docstring 에 적었다** — 「다른 프로젝트」만 본다. 같은 프로젝트의 다른 기록에
돌린 관문은 안 잡는다. 기록 단위까지 보려면 관문마다 대상 단위를 계약이 먼저 정해야 한다.

R12 — `check-required-content.py` 를 `OUTPUT_CHECKS` 에 더한다. B-003 이 병합돼 이제 된다.
세 상태 표를 지키는 것을 확인했다 — `ca-tmpl` exit 2 · `keycloak-session-store` exit 0 ·
없는 프로젝트 exit 2.

`OUTPUT CHECKS` 의 error 가 5 → 7 이 된다. **부채가 늘어난 것이 아니라 세는 자리가 늘었다** —
`ca-tmpl` 에 계약이 없다는 한 사실이 `check_evidence` · `check-required-content` ·
`TECH LOG TREES` 세 곳에서 셈된다. 같은 사실을 세 번 세지 않는다.

회귀 `scripts/tests/test_run_target.py` 2건 (123 → 125). **「정상 원장은 그대로 통과한다」
대조를 함께 넣는다** — 무조건 거절로 성공률을 올리는 것이 R-003 이 든 실패다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
2026-09-10 11:36:45 +09:00
DongHyeonkaandClaude Opus 5 a5215eda31 feat(studio-save): 그림 쪽에도 다리를 놓는다
문장 쪽에는 놨는데 그림 쪽에는 안 놨다. 화살표를 뒤집어도 관문이 전부 0 이고 경고도 비어
자동 통과했다.

설계가 「SVG 의 스크립트·외부 리소스는 저장 단계의 허용 정책으로 제한한다」고 적었는데 그
정책이 코드로 어디에도 없었다. 화살표 방향은 사람이 봐야 하지만 스크립트가 들어 있는지는
문자열로 판정된다 — 그림 쪽에서 기계가 할 수 있는 유일한 일이라 error 로 막는다.
script·foreignObject·이벤트 처리기·바깥 href·바깥 리소스·@import 여섯이다.
#fragment 와 data: 는 바깥으로 안 나가므로 막지 않는다.

그림이 검토 뒤에 바뀌면 경고로 올린다. 무엇이 바뀌었는지는 기계가 못 말하지만 바뀌었으니
보라는 말할 수 있다. 이것이 들어가면서 읽기 계약이 그림에도 걸린다.

실제 흐름과 개념 설명을 data-figure-kind 로 가른다. 표시가 없으면 error 가 아니라
warning 이다 — 지금 저장소의 그림 272장에 이 표시가 하나도 없고, 없다고 전부 막으면
정상을 막는 쪽으로 넘어간다. 설명용이라 밝힌 그림도 막지 않는다.

검사기를 조일 때마다 정상이 통과하는 대조를 같이 넣는다. 이번 대조군은 평범한 그림,
설명용이라 밝힌 그림, 내부·data 참조, 그리고 이 저장소의 그림 272장 전부다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:32:51 +09:00
DongHyeonkaandClaude Opus 5 f1637ee532 fix(check-preservation): 절을 통째로 지우면 아무것도 안 움직이던 자리
「확인하지 못한 것」 절을 통째로 지운 편집이 경고를 비운 채 지나갔다. 배관 문제가 아니라
실을 것이 없었다 — 지운 절에 보호 구간이 없었고 그 절의 문장이 유보 목록에 없었다.
그래서 「경고가 비면 자동 통과」라는 읽기 계약으로도 그대로 통과한다.

유보 목록이 추정·가능성 쪽에만 몰려 있었다. 한계를 밝히는 말은 대개 「안 했다」 모양인데
그쪽이 얇았다. 두 갈래로 나누고 뒤쪽을 채웠다.

그리고 `##` 절이 통째로 사라지면 그 자체를 경고에 올린다. 무엇이 사라졌는지는 절 제목으로
충분하다.

error 를 늘리지 않았다. 판정하는 것이 아니라 보이게 하는 것이다. 절을 덜어 낸 것인지
한계를 지운 것인지는 근거를 읽어야 안다.

채택된 편집 100쌍을 다시 돌려 막은 쌍이 1 그대로인 것을 확인했다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:27:49 +09:00
DongHyeonkaandClaude Opus 5 4fae5b5398 fix(check-preservation): 삭제를 코드가 판정하지 않는다. 날조는 판정한다
사람이 채택한 편집 100쌍 중 9건을 막고 있었다. 막은 것이 전부 자료가 뒷받침하지 않는
덧붙인 이득과 되풀이를 지운 편집이고, 지운 문장 안에 숫자나 인용부호가 있었다는 이유로
막혔다. 반대로 「확인하지 못한 것」 절을 통째로 지운 편집은 통과했다.

부수 문장 삭제와 조건 삭제는 같은 연산이다. 지운 문장에 숫자가 있었는지로는 안 갈린다.
그래서 사라진 것은 warning 으로 내리고 새로 생긴 것만 error 로 둔다. 없던 수치·인용·코드를
더한 것은 날조이고 그것은 코드가 판정할 수 있다.

경계도 고쳤다. `(?![\w.-])` 의 `\w` 가 한글도 낱말 문자로 세어 `500행`·`5개다` 처럼 조사나
명사가 붙으면 보호가 통째로 풀렸다. 한국어에서 숫자는 거의 항상 뭔가가 바로 붙으므로
보호가 가장 필요한 자리에서 가장 안 걸렸다.

경고마다 그 값이 있던 문장을 함께 낸다. 기제와 수치까지만 있으면 검토자가 다시 찾아야 한다.

review-package 의 reviewerNotes 맨 위에 읽는 계약을 넣었다 — gates 가 전부 0 이어도 그것만
으로 통과가 아니고, warnings 가 비어 있어야 자동 통과다.

막은 쌍 9 → 1. 남은 하나는 편집이 인라인 코드를 새로 넣은 쌍이라 규칙이 제대로 도는 것이다.
0 으로 만들려면 그 규칙을 풀어야 하고 그러면 지어낸 식별자가 함께 통과한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:19:11 +09:00
DongHyeonkaandClaude Opus 5 32a369f335 feat(scripts): Studio 저장 어댑터 — 게시를 코드가 막고 무인 저장은 꺼 둔다
서버 계약은 tech-log-backend @ a000f87 의 openapi 와 컨트롤러에서 읽었다.
Idempotency-Key 는 필수이고 200자 이하다. expectedVersion 은 스키마가 minimum 1 로
못 박으므로 빈 값은 「검사 안 함」이 아니라 「반드시 충돌」이다.

멱등 키를 기록 경로에서 유도한다. 프런트는 호출마다 새 uuid 를 만들어서 재시도가 문서를
둘 만든다. 만들기는 경로만으로 키를 만들어 몇 번을 다시 시도해도 문서가 하나이고,
저장은 보낼 내용을 키에 넣는다 — 경로만 쓰면 두 번째 저장이 첫 결과로 조용히 재생된다.

게시 요청을 만드는 코드가 없고, 계획이 게시 경로를 가리키면 거절한다. 지금까지 게시를
막고 있던 것은 스킬 문서의 문장 하나였다.

무인 저장은 꺼져 있다. 서버 권한이 studio:read·studio:write 둘뿐이라 저장 계정이 게시도
할 수 있다. 켜는 것은 studio:publish 가 갈라진 뒤의 판단이지 이 파일의 기본값이 아니다.

저장 뒤 되읽어 견줄 때 문자열 하나로 판정하지 않는다. 정규화가 지운 차이를 통과로 세면
비교 틀이 결함을 만들어 내거나 지운다. 정규화 뒤에만 같아지는 것은 whitespaceOnly 로,
되읽은 값이 보낸 값의 앞부분인 것은 truncated 로 따로 낸다. 못 보는 칸은 사유와 함께
적는다 — 조용히 건너뛰면 「전부 같다」가 「본 것만 같다」를 가린다.

회귀의 첫 줄은 대조군이다. 손대지 않은 쌍이 「같다」로 나오지 않으면 나머지 「잡았다」가
전부 틀 탓이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:13:47 +09:00
DongHyeonka 5472751894 Merge branch 'harness/C-verification' into harness/A-integration 2026-09-10 11:07:35 +09:00
DongHyeonka 03c21c119f Merge branch 'harness/B-implementation' into harness/A-integration 2026-09-10 11:07:35 +09:00
DongHyeonkaandClaude Opus 5 bed1fd933f docs(CLAUDE): 검사기가 「볼 것이 없어서 통과」를 「문제 없음」으로 쓰지 않게 한다
겹침 검사기의 적용 범위를 적는다 — 판정에 쓰는 것은 <rect> 뿐이라
techviz 가 만든 SVG 에서만 유효하다. 손댄 SVG 는 배경 사각형이 안
따라 바뀌어 겹침이 있어도 없다고 답한다.

그리고 관문의 종료 코드를 셋으로 가른다. 「대상이 성립하지 않는다」를
exit 2 로 두어 오타 한 번에 관문이 조용히 무효가 되는 것을 막는다.

__pycache__ 는 .gitignore 에 이미 있는데 이 .pyc 하나만 추적되고 있어
두 세션의 작업 트리를 계속 더럽혔다. 추적에서 뺀다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GksxvQvM6A85xy8viYtWk6
2026-09-10 11:07:35 +09:00
DongHyeonkaandClaude Opus 5 e00c1a2b76 검사: 볼 것이 없어서 통과한 것을 「문제 없음」이라고 쓰지 않는다
R4 · R13 · R6 · R3. 네 가지가 같은 자리를 본다 — 검사기가 대상을 못 찾았을 때 무엇을
내는가.

세 상태를 가른다 (CLAUDE.md 「검사」 절의 표).

  봤고 괜찮다              exit 0   문제 없음 · error 0
  대상이 성립하지 않는다   exit 2   대상이 성립하지 않는다 — <이유>
  볼 것이 아직 없다        exit 0   기록 0건 — 아직 쓴 기록이 없다

R4 — 없는 프로젝트를 주면 여섯 중 넷이 초록을 냈다. `verify-tech-log-tree.py` 는 그것을
「프로젝트 1 · error 0 · PASS」로 셌다 — 오타 한 번이면 검사를 다 돈 것처럼 보인다.
판정은 `techlog.check_targets()` 하나로 모은다. 여섯 곳에 같은 규칙을 따로 쓰면 다음에
하나만 어긋난다. `check_evidence.mjs` 는 이미 exit 2 라 문구만 맞춘다.

R13 — 프로젝트 이름 쪽만 고치면 `--file` 로 오타를 내는 순간 다시 조용히 0건이 된다.
`techlog.check_files()` 로 같은 자리에 둔다. `preview-figure.py` 는 인자가 아예 없을 때
exit 1 을 냈는데 그것도 「대상이 성립하지 않는다」다.

R6 — 두 자리를 함께 고쳐야 했다.
  (a) `verify_projects()` 가 `docs/*/tech-log-studio` 만 훑어 계약 없는 프로젝트가
      목록에서 사라졌다. 기준을 `final/document.md` 로 바꾼다 — SSOT 가 있으면 대상이다.
  (b) `verify-tech-log-tree.py:194` 가 「분해 계약 없음」을 warn 으로 냈다. CLAUDE.md 는
      「계약 미채택도 error 다 — 경고로 두면 옛 스키마로 남아 있는 한 검사를 피한다」고
      적어 두었는데, 경고로 두었더니 실제로 그렇게 됐다.

R3 — `verify-pipeline.py` 가 `check-figure-text.py` 와 `check_evidence.mjs --repo` 를
프로젝트마다 돌린다(`OUTPUT CHECKS`). 게시 전에 돌리라고 적어 둔 검사인데 전체 훑기가
부르지 않아 결함이 있는 채로 PASS 로 보고됐다. `check-required-content.py` 자리는 주석으로
남겨 둔다 — 그 파일이 들어온 뒤에 더한다.

회귀 `scripts/tests/test_no_target.py` 7건 (92 → 99). 「실재하는 경로는 통과한다」 대조를
함께 넣는다 — 무조건 거절로 성공률을 올리는 것이 R-003 이 든 실패다. 판정을 일부러
되돌려 FAILED 가 나는 것을 확인한 뒤 복구했다.

알려진 부채는 그대로 둔다. `verify-pipeline.py` 는 exit 1 이다 — 미준수 런 1건과
`OUTPUT CHECKS` 5건, 그리고 `ca-tmpl` 의 계약 없음이 이제 `TECH LOG TREES` 에서도 보인다.
같은 사실이고 두 번 세지 않는다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
2026-09-10 11:07:24 +09:00
DongHyeonkaandClaude Opus 5 c2742a66cc pipeline: 원장 검사기를 문서 계약에 맞추고 터미널 증거 회귀를 잇는다
R2 — `stage-contracts.md` 「관문 요약」이 정본인데 `STAGES` 가 넷을 빠뜨리고 있었다.
S4 `preview-figure.py` · S5 `check_evidence.mjs` · S6 `check_body.mjs`·`check_evidence.mjs` ·
S7 `build-tech-log-tree.py` 를 더한다.

그리고 `style_profile.mjs` 는 관문이 아니라 측정이다 — 문서 계약이 「error 0」을 붙인 것은
`check_prose` 뿐이다(`stage-contracts.md:178`·`:252`). 그런데 원장 검사기가 exit≠0 을 전부
error 로 세어서, 정직하게 exit 1 로 적은 원장은 무조건 실패하고 0 으로 고쳐 적으면 그건
지어낸 것이 된다. `MEASUREMENT_GATES` 로 갈라 **돌았다는 것만 요구하고 종료 코드 0 은
요구하지 않는다.** 대신 `exit` 칸이 없으면 error 다 — 안 돌리고 넘어가는 것을 막는다.
곁증명(`_side_proof`)에도 같은 규칙을 넣는다.

대조군 셋으로 확인했다.
  preview-figure 를 뺀 원장          → FAIL 「관문이 빠졌다」
  style_profile 을 exit 1 로 적은 원장 → PASS
  style_profile 의 exit 칸을 지운 원장 → FAIL 「측정 관문의 종료 코드가 없다」

이 변경으로 `runs/virtualization/2026-09-08-1958/run.json` 이 빨개진다. S6 에서
`check_evidence.mjs` 를 실제로 안 돌린 원장이라 맞는 결과다. 낮춰서 초록으로 만들지 않는다.

R10 — `scripts/terminal-evidence/tests/` 가 문서에 적힌
`unittest discover -s scripts/tests` 범위 밖이고, 직접 `discover` 를 걸면 `render_terminal`
import 경로가 `sys.path` 에 없어 깨진다. 회귀가 초록인데 아무도 안 부르는 상태였다.
`scripts/tests/test_terminal_evidence.py` 가 `load_tests` 로 경로를 얹고 끌어온다 (86 → 92).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Wp9jNbePAmWc5jQwCYhK9v
2026-09-10 11:06:59 +09:00
DongHyeonkaandClaude Opus 5 230e1b20cb docs(document-haness): 관문의 종료 코드 — CASE 한 편을 근거까지 잇고 런 원장을 남긴다
이 저장소 자신의 검사 층을 대상으로 삼았다. 파이프 뒤의 $? 를 읽고 검사기 열넷이
전부 --help 를 받는다고 적었다가, 파이프를 걷고 다시 재니 둘이 exit 1 이었던 일.
관찰·원인·조치가 한 사건으로 닫히고 조치가 capture-evidence.py 다.

증거 여덟 건은 전부 그 도구가 수집했다. proves/doesNotProve 로 「이 출력이 뒷받침하는
것」과 「뒷받침하지 못하는 것」을 증거 쪽에 적어 두었다 — 본문이 그 경계를 넘었는지
대조할 것이 생긴다.

함께 올린 Question 은 「검사할 것이 없을 때 관문은 무엇을 내야 하는가」다. 현상은
재현했고 조치가 없어 Case 로 올리지 않았다. 조치 없이 쓰면 관찰만 있고 결과가 없는
글이 된다.

런 원장은 단계마다 서브에이전트를 하나씩 띄운 기록이다. 넷이 실제로 무언가를 잡았다 —
S3 이 「argparse 를 쓰지 않는 셋」이 증거 원문과 어긋나는 것을(넷이다), S1 이 그
뿌리를 SSOT·앵커·증거 메타에서, S5 가 「증거 여섯 개」가 frontmatter 의 다섯과
어긋나는 것을, S6 이 계약의 「한 번밖에 안 써서」가 메타 여덟 건과 어긋나는 것을.

셋 다 한 세션이 일곱 단계를 겸했으면 안 나왔다. 내가 쓴 글을 내가 다시 읽는 것이기
때문이다.

style_profile 은 exit 1 로 그대로 적었다. 관문이 아니라 측정이고, 돌리지 않은 값을
0 으로 적는 것이 이 원장이 막으려는 바로 그것이다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:06:50 +09:00
DongHyeonkaandClaude Opus 5 cf3996711f feat(scripts): 형식으로 판정 가능한 것만 코드로 막고 나머지는 검토로 넘긴다
형식 관문 여덟이 확신 승격·수치 조작·화살표 뒤집기·필수 칸 삭제를 하나도 못 막는
것이 재현됐다. 그 가운데 결정적으로 판정 가능한 것을 코드로 옮긴다.

check-required-content.py — 종류가 요구하는 칸이 없거나 비었는지 본다.
audit-records.py 는 평문 칸 안에 마크업이 있는지만 보고 칸이 있는지는 안 센다.
고정 목차·자료 개수·답은 강제하지 않는다. 답이 없는 QUESTION 은 정상이고
「다음 검증」이 빈 것만 결함이다.
대상이 성립하지 않으면(프로젝트 없음·계약 없음) exit 2 로 막고, 계약은 있고 기록이
0건이면 통과시키되 초록으로 두지 않는다. 「봤고 괜찮다」와 「볼 것이 없어서 통과」는
다르다.

check-preservation.py — 윤문 전후를 견준다. 지금 관문 가운데 편집 전후를 보는 것이
하나도 없어 수치를 바꾸거나 유보를 지운 편집이 그대로 통과했다.
사라진 것과 새로 생긴 것을 따로 센다. 새로 생긴 수치는 지어낸 값일 수 있다.
유보 표현이 줄면 내되 옳은지는 판정하지 않는다. 늘어난 것은 세지 않는다.

review-package.py — 아무것도 판정하지 않는다. 판정할 사람이 받을 것을 모은다.
해시·검사기 버전·여기서 실제로 돌린 관문·주장 후보·판정 기준. 종료 코드로 안 걸리는
것은 warnings 로 따로 올린다 — 확신 승격이 딱 그 모양이라 안 실으면 아무도 못 본다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:06:32 +09:00
DongHyeonkaandClaude Opus 5 7fc7c69157 fix(terminal-evidence): 마스킹이 자격증명을 덮으면서 명령을 고치고 있었다
두 가지가 겹쳐 있었다.

줄 맨 앞 앵커 때문에 `curl -H "Authorization: Bearer ..."` 처럼 명령 인자 안에 든
자격증명을 놓쳤다. 터미널 증거에서 Bearer 가 가장 흔히 나오는 자리가 그 명령줄이다.

그리고 키워드 패턴의 값이 `[^\s,;]+` 라 공백까지 먹어 닫는 따옴표를 넘어갔다.
`curl -H "X-Api-Key: TESTONLY-x" https://...` 가
`curl -H "X-Api-Key: [REDACTED] https://...` 가 된다. 다중 -H 에서는 다음 인자의
경계까지 무너진다. 증거에 실린 명령이 실제로 돌린 명령과 달라진다.

값의 끝을 따옴표 앞에서 막되, 감싼 따옴표는 되돌려 놓는다. 문자 집합만 좁히면
`TOKEN="eyJ..."` 가 여는 따옴표에서 막혀 아예 안 가려진다.

함께 메운 것: Authorization/Proxy-Authorization 의 Basic, `curl -u`/`--user`
(사용자 이름은 남긴다 — 어느 계정으로 붙었는지가 증거의 일부다), 그리고 JWT.

회귀는 「가려졌는가」만 묻지 않는다. 원문과 따옴표 수가 같은지 함께 본다.
앞선 회귀가 그것을 안 물어서 이 결함을 통과시켰다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:06:32 +09:00
DongHyeonkaandClaude Opus 5 edd45dfec6 feat(scripts): 종료 코드를 손으로 적을 수 없게 만들고 스킬에 버전을 붙인다
capture-evidence.py 는 명령을 subprocess 로 직접 돌리고 그 프로세스의 반환값을
그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을 경로가 없다.
raw 원문과 실행 메타가 같은 이름으로 함께 떨어져 「raw 는 있는데 meta 가 없다」가
구조적으로 안 생긴다.

skill-versions.py 는 스킬 9개의 metadata.version 과 검사기 17개의 내용 해시를
한 장으로 낸다. 통과 판정을 검증기 버전에 묶으려면 묶을 값이 있어야 한다.
버전 칸이 없던 스킬 여덟에 1.0.0 을 붙였다. 산문은 한 줄도 안 바꿨다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
2026-09-10 11:06:07 +09:00
DongHyeonka 43e1aadef0 feat: 가상화 문서들 추가 2026-09-10 08:54:05 +09:00
DongHyeonkaandClaude Opus 5 e9f6a93327 docs(TechLog): 도메인 규칙을 기록에 엮는다
§1.4 로 세운 도메인·비즈니스 규칙을 그것이 실제로 설명하는 기록에 넣었다.

  프로젝트가 문서 게시 파이프라인을 안 타는 이유 → 화면 다섯이 비어 있던 Case
  홈 focus 설정이 FK 없이 사는 설계 → 「열린 질문이 없습니다」 Case
  결정이 자기 화면을 안 갖는 이유 → 목록이 문서 전체를 실어야 했던 Case
  게시가 단계마다 다른 코드로 거절하는 설계 → 화면이 추측 셋을 출력한 Case (반대 사례)
  종류마다 애그리거트와 테이블이 다르다 → 매퍼가 종류를 판정해야 하는 Case
  축을 지우면 연결만 끊고 주제를 지우면 거절하는 이유 → 축 Concept
  개념이 문서 테이블에 얹힌다 → 열세 곳 Case
  화면 상태와 도메인 상태가 원래 갈려 있었다 → 이름을 두 번 바꾼 Case

Case 본문 중앙값 675 → 1,342 자. 검사 넷 전부 통과한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:37:13 +09:00
DongHyeonkaandClaude Opus 5 4769e52e48 docs(TechLog): SSOT 에 도메인과 비즈니스 규칙을 넣는다
이 문서는 결함 카탈로그로만 있었고 이 시스템이 무엇을 하는지가 없었다. 두 저장소의
도메인·유스케이스에서 확인해 §1.4 를 세웠다.

  종류 다섯이 각자 자기 애그리거트와 테이블을 갖는다 (ADR-003) — 개념은 문서 테이블에
  얹히고 concept_detail 에 기준 버전만 따로 둔다. §3 의 열세 건과 §14 의 외래키 없는
  축 표가 여기서 나온다
  화면 상태와 도메인 상태가 다르다 — 질문의 OPEN·INVESTIGATING·PAUSED 가 화면에서 하나로
  접히고 결정의 ACCEPTED 가 ADOPTED 로 보인다
  NextAction 판정 순서 — 서버가 조회 시점에 계산하고 저장하지 않는다. 프론트가 여러
  endpoint 를 조합해 workflow 를 재추론하지 않게 하려는 것이다
  검증·미리보기가 버려지지 않는 산출물인 이유와 그것이 유효한 조건 셋
  게시가 단계마다 다른 코드로 거절하는 이유 — 고칠 것과 다시 검증할 것이 다르다
  저장할 때와 공개할 때의 요구가 다르다 · 문서가 아닌 것은 다른 경로로 공개된다 ·
  주제는 참조가 있으면 안 지우고 축은 연결만 끊는다 · 없는 것을 가리키는 설정을 막는다

SSOT 62,643 → 71,870 자. 인용은 전부 저장소의 javadoc 과 코드에서 옮겼다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:35:11 +09:00
DongHyeonkaandClaude Opus 5 fd221353a3 docs(TechLog): 얇은 Case 열 편과 Question 셋을 저장소 실물로 채운다
SSOT 를 저장소에서 확인해 더 보강하고 그것으로 다시 썼다.

  §7.2   참조 검사 SQL 을 문자열로 조립하는 실제 코드 — 컴파일러가 표 이름도
         컬럼 이름도 보지 않는다는 것이 그 모양에서 드러난다
  §11.2  section-heading-rank 가 미디어 쿼리 값을 먼저 걷어내는 이유(테스트 주석)
  §16.1  질문 삭제는 참조가 둘뿐이라 같은 문제가 덜하다는 대조

Case 열 편과 Question 셋을 다시 썼다. Question 은 사실·가정·미지수·제약을 갈라
채우고 선택지마다 무엇을 감수하는지 적었다 — 오류 코드를 나누면 계약과 반입한 두
저장소가 함께 움직인다는 것처럼.

SSOT 62,643 → 68,319 자. 검사 넷 전부 통과한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:18:37 +09:00
DongHyeonkaandClaude Opus 5 6917ce2420 docs(TechLog): 남은 주제를 다시 쓰고 SSOT 를 저장소 실물로 더 보강한다
주제 11~13 을 다시 쓰고, Case 가 얇은 것들을 저장소에서 실물을 확인해 채웠다.

  §13.4  ManagementClientSafeMessages — 삭제 관련 코드 여섯의 고정 문구와
         원문 메시지를 내보내지 않는 이유(javadoc)
  §16.1  다섯 참조가 전부 DOCUMENT_IN_USE 하나로 나가고, SSOT 가 인용한 영어 문장은
         DeleteDocumentDraftUseCase 안에 남는 진단 메시지라 밖으로 나가지 않는다
  §13.6  romanizeSyllable 실물과 음운 변동을 뺀 이유, 문서 slug 와 같은 정규식을 쓰는 이유
  §15.4  check:types 가 도는 tsconfig 여섯 — app·node·test·recipes·web-worker·service-worker

SSOT 62,643 → 67,526 자. 인용한 코드는 전부 저장소에서 찾아 대조했다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:12:37 +09:00
DongHyeonkaandClaude Opus 5 b1653dbba8 docs(TechLog): 주제 7~10 을 다시 쓴다
주소가 게시 시점에 굳어 저장되는 구조, 축 링크를 두 번 옮긴 순서, 한글 slug 가
간헐적으로 보인 두 가지 어긋남을 표로 갈랐다. 화면이 실패를 없음으로 그릴 때 작성
도구에서 왜 더 오래 숨는지, Promise.all 이 거절과 던짐에서 다른 경로를 타는 이유를
채웠다. CSS module 이 왜 전역 규칙에 닿지 않는지, 403 과 404 가 원인을 어떻게
좁혔는지도 적었다.

link-audit.py 를 감사 Case 의 evidence 로 걸어 배정한 증거 하나를 메웠다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:06:20 +09:00
DongHyeonkaandClaude Opus 5 193da20d09 docs(TechLog): Reference 15편의 규칙 표기를 게시된 기록에 맞추고 주제 6 을 다시 쓴다
게시된 Reference 15편이 전부 규칙을 `### N. 제목` 으로 쓰고 적용 조건·예외·예시를 항목으로
쓴다. 내 15편은 규칙을 `**굵게**` 로, 나머지 셋을 문단으로 쓰고 있었다 — Studio 의
rules[]·applyWhen[]·exceptions[]·examples[] 는 배열이라 문단으로 두면 항목이 하나로 접힌다.

  규칙 68개를 `### N. 제목` 으로 바꿨다 (편당 3~7개, 게시된 것은 4~10개)
  적용 조건·예외·예시를 항목으로 갈랐다. 한 항목뿐이던 아홉 편은 조건을 나눠 적었다

주제 6 은 본문을 다시 썼다 — location = 이 정확히 일치하는 경로만 잡아 27개가 얼어붙은
구조, 여덟 곳이 우는 시점을 셋으로 가른 표, digest 를 다시 계산할 때 옛 값을 먼저
재현하는 이유.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 19:01:44 +09:00
DongHyeonkaandClaude Opus 5 53537e37e8 docs(TechLog): 주제 4·5 를 스킬대로 다시 쓴다
what-the-compiler-lets-through  중앙값 2,096 → 2,590 자
  seams-no-test-crosses                    → 3,091 자

bivariance 가 왜 느슨한 판정을 받는지, never 캐스트가 왜 아무것도 요구하지 않는지처럼
「이름을 댔으면 왜 있는지도 댄다」를 채웠다. 네 가지가 각각 무엇을 통과시키고 어디서
드러났는지를 표로 갈랐다. 검사 넷 중 무엇이 SQL 을 실제로 돌리는지도 표로 세웠다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:57:59 +09:00
DongHyeonkaandClaude Opus 5 a8ce0dda07 docs(TechLog): 주제 셋을 스킬대로 다시 쓰고 SSOT 를 저장소 실물로 고친다
건너뛴 참조 다섯을 읽고 나서 다시 썼다 — from-ssot-to-records.md 의 「그림과 증거는
배정 대상이다」, code-tables-diagrams.md 의 표·코드 규칙, explaining.md 의 「이름을
댔으면 왜 있는지도 댄다」.

**SSOT 를 먼저 고쳤다.** §3.3 의 코드블록이 저장소와 달랐다 — PATH_PREFIX_KINDS 는
Record 표가 아니라 튜플 배열이고, 진짜 경로 표는 EXPLORE_KIND_PATHS 다. 저장소에서
확인해 실물로 바꾸고, javadoc 이 적어 둔 이유를 함께 옮겼다. §16.7 에 BRANCH_FIELDS 와
pathOf 실물을, §4.3 에 ContractRouteCoverageTest 의 javadoc 과 면제 상수 둘을 더했다.
62,643 → 65,737 자.

**계약에 ssot-assets·ssot-evidence 를 배정했다.** 그 절차를 건너뛰어서 SSOT 가 이미
가진 그림과 측정이 글감에 배정되지 않은 채였다. TechLog 12 글감, keycloak-session-store
는 그림 21장·증거 19건을 배정하고 붙일 글감이 없는 그림 4장은 이유를 계약에 적었다.
배정하자 검사기가 「배정한 증거를 기록이 쓰지 않는다」 4건을 드러냈다.

**주제 셋을 다시 썼다.**
  hand-listed-kinds            중앙값 1,925 → 3,760 자
  declared-but-not-implemented          → 2,608 자
  values-lost-between-boundaries        → 2,602 자

게시된 기록은 keycloak 4,546 · n+1liner 3,190 이다. 표와 코드를 SSOT 에서 옮기고,
Reference 에 담을 수 없던 표(§5.5 의 여덟 자리)를 짝이 되는 Case 로 내렸다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 18:54:45 +09:00
DongHyeonkaandClaude Opus 5 6feee5ba57 docs(TechLog): 자료에 남아 있던 사람의 흔적을 제자리에 놓는다
writing-as-the-person-who-did-it 을 서브에이전트 셋으로 나눠 56편에 적용했다.
56편 중 20편만 고쳤다 — 나머지 36편은 SSOT 를 절 단위로 대조했을 때 옮길 흔적이
이미 옮겨져 있었거나 없었다. 없는 목소리를 채우지 않는다.

옮긴 것은 전부 SSOT 의 어느 절에서 왔는지 댈 수 있다.

  §3.4  「눈으로 찾을 일이 아니었다」— 세 계약을 파싱해 뽑은 이유
  §4.3  구현하지 않기로 한 것과 빠뜨린 것은 다르다
  §8.5  표의 마지막 줄을 더할 때 이 목록을 또 빠뜨렸다
  §9.4  「왜 주제 링크가 탐색으로 가지?」— 우회를 남겨 두면 계속 나온 질문
  §11.3 「세 버튼」을 실제 이름으로 되돌리고 두 언어가 섞인 것을 그 자리에
  §12.2 막지 않은 대신 메모리에 남긴 것
  §13.1 여섯 벌 인용이 어느 커밋이 짚은 말인지
  §13.3 고쳐 쓴 첫 안이 거절당한 것과 사용자가 고른 말 두 쌍
  §14.3 「문서가 그대로 나온다」는 구조 차이가 아니라 내용 양의 차이라는 정정
  §16.1 삭제가 막힌 실제 기록 이름과 그것을 막은 프로젝트 링크
  §16.9 주제 논지와 축 결론이 AI 가 써서 DB 에 직접 넣은 미검토 초안이라는 것
  §17.5 바운딩 박스로 잘못 지목한 대상이 「판단 기준」이었다는 것

검증 환경의 커밋 해시 하나가 틀려 있었다(ca1cfa2 → ca1fc92). SSOT §13.1 과
부록 A 가 적은 값이고 저장소에 그 커밋이 있다.

검사 넷 전부 통과한다 — check_prose 56편 error 0 · check_body PASS ·
check_evidence --repo 문제 없음 · verify-tech-log-tree 프로젝트 5 error 0 warn 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:31:48 +09:00
DongHyeonkaandClaude Opus 5 0650d91def docs(TechLog): 설명 뒤에 붙은 평가·예고·되풀이를 걷어낸다
rewriting-technical-prose-naturally 를 서브에이전트 셋으로 나눠 56편에 적용했다.
ai-tells.md 의 첫 절대로 다른 표현으로 바꾸는 대신 문장을 통째로 지웠다.

  설명한 것의 중요성을 다시 평가하는 꼬리   19
  이미 설명한 것을 추상어로 되풀이           19
  독자에게 읽는 법을 지시하거나 오해를 가정   9
  자료가 뒷받침하지 않는 덧붙인 이득          4

문서군 전체의 문형 편중도 풀었다 — 함께 27→7(한 묶음), 그대로 22→12(두 묶음),
하게 된다 1→0. 한 편에서 세 번 반복되던 「같은 병이 ~에서도 났다」와 두 기록에
같은 문장으로 있던 세 쌍을 갈랐다.

계약 제목 「여덟 자리」가 본문의 「여덟 곳」과 어긋나 있었다. 제목이 spatial-metaphor
규칙에도 걸리므로 계약과 기록을 함께 「여덟 곳」으로 맞췄다.

검사 넷 전부 통과한다 — check_prose 56편 error 0 · check_body PASS ·
check_evidence --repo 문제 없음 · verify-tech-log-tree error 0 warn 0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 16:03:16 +09:00
DongHyeonkaandClaude Opus 5 fc23660871 docs(keycloak-session-store): research the 42 concepts the lab used and write them up
Replaces the to-do list with the work itself. Eight layers, 42 concepts,
1200 lines, each with what it is, why it turns up here, how it fails, and
the command to check it.

Most values were read off the running system rather than recalled:

  conntrack ESTABLISHED timeout   86400s — why A-1's injection sat unmatched
                                  for 25 minutes was normal, not a fault
  FORWARD chain position 1        KUBE-ROUTER-FORWARD — why -I FORWARD 1
                                  counted zero packets
  cgroup version                  v2, and the numbers in systemctl status
                                  are read straight out of those files
  nginx restart policy            on-failure, 100ms, and it gives up after
                                  5 failures in 10 seconds
  Type and KillMode               five units on this host, four different
                                  combinations

Two facts could not be read locally and carry sources: Let's Encrypt
backdates notBefore by exactly one hour to tolerate client clock skew, and
Keycloak invalidates the whole SSO session on refresh token reuse. The
second one explains B-3 — the winning request's new token was not itself
rejected, the session it belonged to had just been deleted.

The systemd layer also explains the one CGroup line in systemctl status that
D-4 spent ps commands establishing: master 585 kept, worker replaced.

One item is marked as read rather than measured. Restart=on-failure comes
from the unit file; nginx has not been killed to watch it come back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:32:54 +09:00
DongHyeonkaandClaude Opus 5 f6c825e858 docs(TechLog): 글감 56개를 기록으로 쓴다
주제 13개 · Case 28 · Concept 5 · Reference 15 · Question 4 · Decision 4.
계약의 노드마다 종류가 요구하는 칸을 채우고, 본문이 있는 두 종류에는 SSOT 가 이미
그려 둔 도식 셋(value-boundaries · decision-path-404 · topic-variant-model)을
tech-log-studio/ 로 옮겨 붙였다. 새로 그린 그림은 없다.

검사 셋 전부 통과한다.
  check_body.mjs      56 편 중 본문이 있는 33 편 PASS
  check_prose.mjs     56 편 error 0
  check_evidence.mjs  --repo 포함 문제 없음
  verify-tech-log-tree.py  프로젝트 5 · error 0 · warn 0

인용한 코드블록은 전부 SSOT 에서 찾아 대조했다. check_evidence.mjs 가 본문의 각 줄과
source 앵커와 계약 제목을 다시 확인한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:29:33 +09:00
DongHyeonkaandClaude Opus 5 6955611439 docs(keycloak-session-store): list every concept this lab used but never explained
The lab wrote 26 experiments on top of names it never defined. Comparing the
terms used against the places that actually explain them: 24 have no
explanation anywhere. conntrack appears 35 times across the experiment
documents, refresh token rotation 17, JWKS and Liquibase 11 each.

The list groups them in eight layers with, for each, where it came up,
whether it is explained centrally, only inside one experiment, or nowhere,
and what specifically needs to be learned. It is mapped onto the six topics
of the decomposition contract rather than standing beside it — two glam of
operations-that-report-success cannot be written without systemd, and
Restart=, journald and Type=forking are all in the "nowhere" column.

The bottom layer is the worst of it. systemctl has been typed eight times
as a command and never explained, and the unit file, cgroup, restart policy
and crash-loop limit under it are unwritten.

Building the list turned up a piece of evidence the lab missed. B-7 recorded
that it narrowed the 502 by splitting layers and calling traefik directly to
bypass nginx. The host nginx journal had already written the cause in a
sentence — "upstream sent too big header ... /oauth2/callback" — and none of
the 147 evidence files contain it. That is what journald being in the
"nowhere" column cost.

Also fixes ten spatial-metaphor errors the tightened check_prose now
catches, nine of which predate this section.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:17:32 +09:00
DongHyeonkaandClaude Opus 5 1f04117bbf docs(clean-architecture-backend-template): 제1부가 채택한 것만 글감으로 남기고 다시 고른다
글감 1,001개 중 제1부(§3~§11) 앵커를 하나라도 가진 것은 112개뿐이었다. 나머지 889개는
제2부 모듈 분석 65편의 절 제목에서 나온 것이고, 그것이 재판정이 필요했던 이유다.

  주제      44 → 16   (43개가 독자 질문 없이 있었다. 지금은 전부 있다)
  글감   1,001 → 123  (제1부 앵커 112 + 제1부가 채택했는데 비어 있던 자리 11)
  후보      965 → 1,088 · PENDING 905 → 0
  error   3,042 → 0

내려온 889개는 후보 대장에 KEEP_IN_SSOT 로 남는다 — 버린 것이 아니라 분석에 남기고 독립
기록으로 만들지 않기로 한 것이다. 그 글감을 받치던 기록 파일 828개는 지웠다. 계약이 정본이고,
파일이 남아 있다는 이유로 계약에서 뺀 주제가 되살아나면 안 된다. 이력에는 그대로 있다 —
git checkout a0ca2bb -- <경로>.

제1부가 채택했는데 글감이 없던 자리 열하나를 채웠다: mongo high-water mark 가 재전달 이벤트를
삼킨 P1, admin plane 이 가드만 켜고 서비스는 켜지 않은 것과 그 짝인 결정, 실패 어휘 세 층과
SQLState 매트릭스 병합 규칙, 부하 아래에서만 새는 admission 경계, 발행 증거와 완료 판정의
분리, keyset·JSONB 결정 둘.

Concept 17개에 basis-version 을 채우고, 계약 제목과 기록 제목이 갈라져 있던 23건을 기록 쪽에
맞췄다. candidateScope 에 excludedAnchorPattern 을 적어 제2부 앵커만 가진 글감이 다시 올라올
수 없게 한다.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-07 15:02:25 +09:00
DongHyeonkaandClaude Opus 5 a0ca2bb72a docs: 계약 없이 남아 있던 두 프로젝트에 주제를 갈라 계약을 세운다
keycloak-session-store 와 TechLog 는 SSOT 만 있고 분해 계약이 없었다. 두 프로젝트
모두 후보를 처분과 함께 남기고 PROMOTE 만 주제로 올린다.

  keycloak-session-store  주제 6 · 글감 33 · 후보 42 (제외 9)
  TechLog                 주제 13 · 글감 56 · 후보 91 (제외 35)

TechLog 는 저장소가 셋이라 sourceRepository 를 목록으로 적는다. 설계 패키지는 이
기계에 체크아웃이 없고 반입한 쪽의 MANIFEST.sha256 이 세 계약의 원본 리비전을
고정하므로 그것을 리비전으로 적었다. 검사기 둘이 그 모양을 읽게 고친다 —
verify-tech-log-tree.py 는 목록의 항목마다 path·revision·verified 를 보고,
check_evidence.mjs 는 체크아웃이면 git 으로, 매니페스트면 그 파일이 리비전을
적고 있는지로 대조한다.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-07 13:30:12 +09:00
3813 changed files with 3509456 additions and 110092 deletions
@@ -1,6 +1,9 @@
--- ---
name: analyzing-codebase-for-tech-log name: analyzing-codebase-for-tech-log
description: Use when a project under <분석 대상 저장소> must be deeply analyzed and documented under docs, especially when the repository is too large for one pass and analysis must proceed by bounded module or subsystem. description: Use when a project under <분석 대상 저장소> must be deeply analyzed and documented under docs, especially when the repository is too large for one pass and analysis must proceed by bounded module or subsystem.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# Analyzing Codebase For Tech Log # Analyzing Codebase For Tech Log
@@ -11,7 +14,8 @@ Produce a highly detailed, source-traceable engineering analysis. This stage dis
## Required sequence ## Required sequence
1. **First read `<분석 대상 저장소>/analysis-queue.yaml`.** Before inspecting any project contents, apply `references/queue-contract.md`: reconcile newly discovered project directories, preserve queue order, and determine the single active project. 0. **Fix the two roots before anything else.** `<분석 대상 저장소>` is the repository being analyzed — an absolute path outside this repository, given by the caller. `docs/<프로젝트>/` is inside *this* repository. Never write into the analyzed repository, and never read analysis state from it.
1. **Read `<분석 대상 저장소>/analysis-queue.yaml` if it exists.** It exists only when several repositories are queued. Apply `references/queue-contract.md`: reconcile newly discovered project directories, preserve queue order, and determine the single active project. **If there is no queue, analyze the one repository the caller named and skip to step 3.** A missing queue is not a blocker — it means nothing is queued.
2. If an `IN_PROGRESS` project exists, analyze only that project. If none exists, activate the first `PENDING` project in queue order. Never preempt an active project because a new project appeared. 2. If an `IN_PROGRESS` project exists, analyze only that project. If none exists, activate the first `PENDING` project in queue order. Never preempt an active project because a new project appeared.
3. For the selected `<분석 대상 저장소>`, check the nearest `AGENTS.md` or equivalent repository instructions. 3. For the selected `<분석 대상 저장소>`, check the nearest `AGENTS.md` or equivalent repository instructions.
4. Record Git revision and `git status` when Git is available. Never modify or reset user source as part of analysis. 4. Record Git revision and `git status` when Git is available. Never modify or reset user source as part of analysis.
@@ -2,7 +2,7 @@
## Primary evidence first ## Primary evidence first
Store command output, logs, generated query plans, benchmark results, browser observations, and other primary material under `docs/<프로젝트>/evidence/raw/` before creating presentation assets. Store command output, logs, generated query plans, benchmark results, browser observations, and other primary material under `docs/<프로젝트>/final/evidence/raw/` before creating presentation assets.
## Terminal evidence ## Terminal evidence
@@ -15,7 +15,7 @@ For a command used as evidence, retain:
- raw stdout/stderr; - raw stdout/stderr;
- source revision when relevant. - source revision when relevant.
Render terminal UI with `./tools/terminal-evidence/render_terminal.py`. Render terminal UI with `scripts/terminal-evidence/render_terminal.py`.
The visual asset is explanatory. The raw evidence is the provenance. The visual asset is explanatory. The raw evidence is the provenance.
@@ -2,7 +2,7 @@
## 분석 기준 revision ## 분석 기준 revision
- repository: `/shared/codebase/<project>` - repository: `<분석 대상 저장소의 절대 경로>`
- revision: `<git revision or non-git snapshot note>` - revision: `<git revision or non-git snapshot note>`
## Build and module map ## Build and module map
@@ -1,7 +1,7 @@
{ {
"schemaVersion": 2, "schemaVersion": 2,
"project": "<project>", "project": "<project>",
"codebasePath": "/shared/codebase/<project>", "codebasePath": "<분석 대상 저장소의 절대 경로>",
"gitRevision": null, "gitRevision": null,
"analysisStatus": "NOT_STARTED", "analysisStatus": "NOT_STARTED",
"analysisCycle": 1, "analysisCycle": 1,
@@ -1,6 +1,9 @@
--- ---
name: deriving-tech-log-root-tree name: deriving-tech-log-root-tree
description: Use when a completed or substantially completed docs project analysis must be decomposed into grounded Tech Log Topics and candidate Case, Concept, Reference, Open Question, and Decision records. description: Use when a completed or substantially completed docs project analysis must be decomposed into grounded Tech Log Topics and candidate Case, Concept, Setup, Reference, Open Question, and Decision records.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# Deriving Tech Log Root Tree # Deriving Tech Log Root Tree
@@ -74,7 +77,9 @@ They do not hold candidates, and in a folded project they are gone.
4. Pick **Decisions** from the explicit-decision section (§10). 4. Pick **Decisions** from the explicit-decision section (§10).
5. Pick **Questions** from the unresolved section (§11). 5. Pick **Questions** from the unresolved section (§11).
6. Only now add the **Concepts** those four need in order to be understood. Concept is 6. Only now add the **Concepts** those four need in order to be understood. Concept is
derived backwards from the records that require it, never by sweeping headings. derived backwards from the records that require it, never by sweeping headings. Add the
**Setups** the analysis actually supports in the same pass — the section below says which
ones those are.
7. Give every candidate a disposition — `references/candidate-disposition.md` — and set 7. Give every candidate a disposition — `references/candidate-disposition.md` — and set
`dispositionReview` to `CONFIRMED` only for the ones a person actually re-read. `dispositionReview` to `CONFIRMED` only for the ones a person actually re-read.
8. Group `PROMOTE` candidates into Topics. Write one reader question per Topic. 8. Group `PROMOTE` candidates into Topics. Write one reader question per Topic.
@@ -82,7 +87,42 @@ They do not hold candidates, and in a folded project they are gone.
with the fields its kind requires. There is no second tree to keep in step. with the fields its kind requires. There is no second tree to keep in step.
10. Run `references/decomposition-checklist.md`. 10. Run `references/decomposition-checklist.md`.
11. Record `candidateScope`, the source document hash, and the project revision. 11. Record `candidateScope`, the source document hash, and the project revision.
12. `python3 scripts/verify-tech-log-tree.py <project>` — errors must be 0. 12. `python3 scripts/build-tech-log-tree.py <project>` first, then
`python3 scripts/verify-tech-log-tree.py <project>` — errors must be 0. Build fills
`ssotSha256`; verify errors when it is absent, so verifying before building always fails.
## When a Setup belongs in the tree
Add a **Setup** where the analysis records the commands and configuration values that stand
an environment up and someone other than the author has to run them.
Its test is not the one the other five take. They ask whether a claim is worth publishing on
its own; this one asks whether a reader would type these lines. A reproduction that only
re-obtains one measurement stays inside that Case's `재현 조건` field, and a procedure nobody
but the author would run is that Case's environment section.
Setup nodes need a project. A Topic is optional, and a Setup without one reads as that
project's shared configuration. Instead of a verification date it carries `pinned-versions`
— the versions the procedure was established on.
## Three fields the verifier requires and this procedure does not otherwise name
Write them by hand. `verify-tech-log-tree.py` counts each as an error when missing.
| Field | What goes in it |
|---|---|
| `candidateScope` | the range candidates were found in — above |
| `sourceRepository` | `path` of the analyzed repository, its `revision`, and how that was established. Leave `revision` `null` rather than inventing one; when the work is split across branches, pair names and commits under `revisions` |
| `ssotSha256` | filled by `build-tech-log-tree.py`. Its job is to catch a tree whose SSOT changed after the candidates were chosen |
```json
"sourceRepository": {
"path": "/absolute/path/to/analyzed-repo",
"revision": null,
"revisions": {"AP1 develop-pattern1": "64175266"},
"verified": "how the revision was established, or why it is null"
}
```
Use `.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md` as the Use `.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md` as the
output contract. output contract.
@@ -117,12 +157,19 @@ Do not create one Topic per source file or module. A directory is not a Topic.
- **Open Question** — no answer yet, the design turns on the answer, and there is a next - **Open Question** — no answer yet, the design turns on the answer, and there is a next
verification and a closing criterion. verification and a closing criterion.
- **Decision** — the project actually chose a direction, with grounds and an accepted cost. - **Decision** — the project actually chose a direction, with grounds and an accepted cost.
- **Setup** — a procedure the reader runs to stand the environment up. Carries
`pinned-versions` instead of a verification date, and always belongs to a project.
The independence test decides all five: The independence test decides the first five:
> Delete this record and fold it into a related Case or Concept as one section. If > Delete this record and fold it into a related Case or Concept as one section. If
> understanding, decisions, and reuse are unchanged, it is not an independent record. > understanding, decisions, and reuse are unchanged, it is not an independent record.
Setup does not answer that question, because folding a procedure into a Case is exactly what
this kind exists to stop — the commands end up in a plain-text `검증 환경` field where they
cannot be copied. Ask instead who runs it. If only the author ever will, it is that Case's
environment, not a record.
Branches may be empty. Symmetry is not a quality goal. Neither is volume — a large Branches may be empty. Symmetry is not a quality goal. Neither is volume — a large
denominator justifies a long `final/document.md`, not a long tree. denominator justifies a long `final/document.md`, not a long tree.
@@ -46,6 +46,7 @@
| Reference | 다음 프로젝트에도 적용할 규칙이며 적용 조건과 예외가 있다 | Case 결론을 선언문으로 바꾼 것 | | Reference | 다음 프로젝트에도 적용할 규칙이며 적용 조건과 예외가 있다 | Case 결론을 선언문으로 바꾼 것 |
| Question | 답이 아직 없고, 답에 따라 설계가 달라지며, 다음 검증과 종료 기준이 있다 | 실행하지 않은 테스트 목록, 막연한 "다른 방법은?" | | Question | 답이 아직 없고, 답에 따라 설계가 달라지며, 다음 검증과 종료 기준이 있다 | 실행하지 않은 테스트 목록, 막연한 "다른 방법은?" |
| Decision | 대안 중 프로젝트가 실제 방향을 정했고 근거와 감수한 비용이 있다 | 기술이 존재한다는 사실, 권장사항, 아직 정하지 않은 방향 | | Decision | 대안 중 프로젝트가 실제 방향을 정했고 근거와 감수한 비용이 있다 | 기술이 존재한다는 사실, 권장사항, 아직 정하지 않은 방향 |
| Setup | 남이 자기 기계에서 실행할 명령과 구성 값이 있고 프로젝트가 정해져 있다 | 글쓴이만 다시 돌릴 재현 순서(그 Case 의 재현 조건이다), 명령 없는 구성 설명 |
## Case 를 언제 합치나 ## Case 를 언제 합치나
@@ -40,6 +40,13 @@
- [ ] The title names a mechanism, not an absence, a count, or an analysis-scope fact. - [ ] The title names a mechanism, not an absence, a count, or an analysis-scope fact.
- [ ] No analysis section number or finding grade survives in the title. - [ ] No analysis section number or finding grade survives in the title.
## Setup
- [ ] Someone other than the author has to run it; it is not one Case's reproduction steps.
- [ ] The commands and configuration values come from the analysis, not from memory.
- [ ] `pinned-versions` names the versions the procedure was established on.
- [ ] A project is set. No verification date is invented to stand in for the versions.
## Reference ## Reference
- [ ] The rule is reusable beyond the originating incident. - [ ] The rule is reusable beyond the originating incident.
@@ -102,7 +102,8 @@
"classification": "대안을 두고 프로젝트가 실제로 고른 방향이다", "classification": "대안을 두고 프로젝트가 실제로 고른 방향이다",
"relations": ["case:eager-to-one-n-plus-one"] "relations": ["case:eager-to-one-n-plus-one"]
} }
] ],
"setup": []
} }
} }
}, },
@@ -149,4 +150,5 @@
- 후보 셋 중 하나만 글감이 됐다. `MERGE_INTO``KEEP_IN_SSOT` 이 없는 분해는 선별하지 않은 분해다. - 후보 셋 중 하나만 글감이 됐다. `MERGE_INTO``KEEP_IN_SSOT` 이 없는 분해는 선별하지 않은 분해다.
- Concept 은 Case 를 먼저 고른 뒤에 그것을 읽는 데 필요해서 더했다. - Concept 은 Case 를 먼저 고른 뒤에 그것을 읽는 데 필요해서 더했다.
- Question 에 `decision-criterion` 이 있다. 무엇이 나오면 닫는지를 적지 않으면 검증을 마쳐도 열려 있다. - Question 에 `decision-criterion` 이 있다. 무엇이 나오면 닫는지를 적지 않으면 검증을 마쳐도 열려 있다.
- 섯 종류를 억지로 채우지 않아도 된다. 여기서 다섯이 다 있는 은 실제로 다섯이 있었기 때문이다. - 섯 종류를 억지로 채우지 않아도 된다. 여기서 다섯이 다 있는 까닭은 실제로 다섯이 있었기
때문이고, 여섯 번째인 `setup` 은 비어 있다 — 이 프로젝트에는 남이 따라 할 절차가 없었다.
@@ -0,0 +1,188 @@
---
name: publishing-tech-log-to-studio
description: Use when a finished Tech Log record .md must be put into Tech Log Studio through the browser with Playwright MCP — creating or opening the working copy, uploading assets, filling the per-kind fields, and saving. Save only; this skill never publishes.
metadata:
version: "1.0.1"
language: "ko-KR"
---
# Studio 반입 — 저장까지만
## 이 스킬의 경계
기록 `.md` 하나를 Studio 편집 화면에 넣고 **저장**한다. 거기서 끝난다.
**게시하지 않는다.** 게시는 공개 사이트에 올리는 일이고, 되돌리려면 `unpublish` 를 해야 하며
그 사이에 누구나 본다. 더 중요한 것은 **한 번이라도 게시한 문서는 게시를 취소해도 지워지지
않는다는 것이다** — 삭제가 409 로 거절되고 「공개된 기록은 삭제할 수 없습니다」가 뜬다. 그래서
시험 삼아 게시하지 않는다. 게시는 사람이 미리보기를 읽고 판단한다.
편집 화면 오른쪽 `aside` 에 버튼이 `저장`·`게시` 둘뿐이다. **`게시` 를 누르면 저장·검증·
미리보기·게시가 한 번에 돈다.** 이 스킬은 `저장` 만 누른다.
## 들어가기 전 조건
| 조건 | 확인 |
|---|---|
| 기록 `.md` 가 파서를 통과했다 | `studio-body.py` 로 바꾼 파일에 `check_body.mjs`, error 0 |
| 문장 검사를 지났다 | `check_prose.mjs` error 0 |
| 인용이 SSOT 에 실재한다 | `check_evidence.mjs <프로젝트> --repo` |
| 브라우저가 이미 로그인돼 있다 | 아래 「인증」 |
**검사를 안 지난 초안을 넣지 않는다.** 저장은 빈 칸도 받아 주기 때문에(Studio 는 한 번에 다
쓰지 않아도 저장되게 만들어져 있다) 넣는 것 자체는 성공한다. 그래서 파서·문장 검사를 여기서
대신 잡아 주지 않는다.
## 인증
Studio 는 조회에도 권한을 요구한다. 읽는 것이 게시 전 초안이기 때문이다.
**이 저장소에 자격증명을 두지 않는다.** 이미 로그인된 브라우저 세션을 쓴다. 편집 화면을 열었을
때 상태 `aside` 가 25초 안에 안 뜨면 **인증이 안 된 것으로 보고 멈춘다.** 로그인 화면을
자동으로 통과하려 들지 않는다 — 사용자에게 로그인해 달라고 말하고 기다린다.
```
aside[class*="studio-document-status"] ← 이것이 안 보이면 인증 실패
```
## 주소
| 무엇 | 주소 |
|---|---|
| Studio | `https://hyeonworks.com/studio` |
| 편집 화면 | `https://hyeonworks.com/studio/documents/<id>/edit` |
| 새 문서 | Studio 에서 `새 문서` → 종류 선택 → `작업본 만들기` |
기록 frontmatter 의 `id``studio:` 가 이미 차 있으면 **새로 만들지 않는다.** 그 주소로 바로
간다. 비어 있으면 새로 만들고, 받은 uuid 와 편집 주소를 기록 frontmatter 에 적는다.
## 절차
### 1. 기록을 읽고 무엇을 넣을지 정한다
`kind` 로 칸 목록이 정해진다. 어떤 `##` 제목이 Studio 의 어느 칸인지는
[references/studio-form-map.md](references/studio-form-map.md), 그 칸이 서버에서 어떤 이름과
자료형인지는 [references/studio-api.md](references/studio-api.md).
**frontmatter 는 메타데이터고, 본문의 `##` 가 Studio 의 칸이며, 제목 아래 첫 문단이 `요약`
이다.** 계약에 없는 `##` 는 화면에 자리가 없어 통째로 사라진다.
**본문은 이제 화면으로 넣지 않는다.** 편집 화면의 본문이 블록 편집기로 바뀌어 블록 하나가
`<textarea>` 하나다 — 기록 한 편이 블록 171개인 것을 실제로 봤다. 그런데 저장이 서버로 보내는
것은 여전히 `bodyMarkdown` 이라는 마크다운 문자열 하나다. 그래서 **옮기는 일은 화면이 아니라
`PUT /api/v1/studio/documents/{id}` 로 한다** — 로그인된 그 페이지 안에서 `fetch` 로 부르므로
세션과 CSRF 가 그대로 실린다. 화면은 읽고 대조하는 데 쓴다.
### 2. 그림이 있으면 Asset 을 먼저 올린다
frontmatter `assets:``file:` 이 올릴 파일이고 `key:` 는 저장소 쪽 이름이다. 올리면 서버가
`<이름>-<해시8>` 형태의 키를 준다.
**본문을 넣기 전에 올린다.** 순서를 뒤집으면 미리보기가 본문 전체를 막는다 —
`1:1 supported local evidence key not found: <asset-key>`. 다른 경로로 올렸으면 편집 화면을
한 번 새로 고쳐야 목록을 다시 받는다. 본문 문법 문제로 오해하기 쉽다.
본문은 서버가 준 키로 바꿔서 넣는다.
```bash
python3 scripts/studio-body.py <기록.md> --key <저장소 key>=<서버가 준 key> --body-only
```
저장소의 `.md` 는 마크다운 이미지로 두고 고치지 않는다. `:::evidence` 는 Studio 렌더러의
구문이라 저장소에 쓰면 편집기에서 그림이 안 보인다.
### 3. 칸을 채운다
되풀이되는 칸(`영향`·`선택지`·`사실`처럼 여러 줄인 것)은 **줄 수를 먼저 맞추고** 값을 넣는다.
줄이 모자란 채로 채우면 뒤엣것이 조용히 버려진다. 셀렉터와 배치 실행 방법은
[references/playwright-recipes.md](references/playwright-recipes.md).
**본문이 없는 세 종류(Reference·Question·Decision)의 칸은 마크다운 블록 파서를 안 거친다.**
그래도 전부 글자로 나오지는 않는다 — 렌더러(`tech-log-frontend`
`presentation/shared/public-render/prose-text.tsx`)가 **백틱 쌍은 인라인 `<code>` 로** 살리고,
빈 줄은 문단으로, 한 줄 바꿈은 `<br>` 로 남긴다. **글자 그대로 나오는 것은 별표·파이프·`#`·
코드펜스·인용 표지 `>` 다.** 백틱을 빼지 않는다 — 빼면 식별자가 민무늬로 나온다.
**환경 구성에도 되풀이 칸이 하나 있다.** `고정한 버전` 은 줄마다 `이름`·`버전` 입력 둘이고
「버전 추가」 버튼으로 늘린다. 여기도 줄 수를 먼저 맞추고 값을 넣는다.
### 4. 저장한다
API 로 넣었으면 `PUT` 이 200 을 내는 것이 저장이다. **`expectedVersion` 은 저장 직전에 `GET`
으로 읽은 `document.version` 이다** — 지어내지 않는다. 보내지 않은 칸은 비워지므로 그 종류의
칸을 전부 담는다.
화면으로 고쳤으면 버튼을 누른다.
```
aside[class*="studio-document-status"] 안의 `저장` 버튼
→ 같은 aside 의 글자가 `저장됨` 으로 바뀔 때까지 기다린다 (최대 30초)
```
`저장됨` 을 못 보면 실패다. 그 `aside` 의 글자를 그대로 읽어서 보고한다. **버튼을 다시 누르지
않는다** — 검증 오류로 막힌 것을 연타로 뚫으려다 `게시` 를 누르게 된다.
버전은 같은 `aside` 의 첫 `dd` 에 있다. 저장 전후로 읽어 두면 실제로 올라갔는지 보인다.
### 5. 미리보기로 읽는다
`즉시 미리보기` 탭은 저장한 값이 아니라 **화면에 입력한 값**을 렌더링한다. 그래서 게시 없이도
공개 화면과 같은 블록 렌더러로 본문을 볼 수 있다.
읽을 항목은 `../writing-tech-log-records/references/studio-draft-review.md` 의 목록을 쓴다.
고칠 것이 있으면 `편집` 탭으로 돌아가 고치고 다시 저장한다.
### 6. 기록에 되적는다
새로 만들었으면 frontmatter 의 `id``studio:` 를 채우고 색인을 다시 만든다.
```bash
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
```
`build-tech-log-tree.py` 는 frontmatter 의 `id` 가 있으면 `publication``게시됨` 으로
적는다. **이 칸은 「Studio 에 있다」는 뜻이지 「공개돼 있다」가 아니다.** 공개 여부는 기록의
`status``public:` 이 말한다.
## 시험용 초안을 남기지 않는다
확인하려고 만든 작업본은 지운다. **여섯 종류 전부 삭제 경로가 있다.** 앞의 다섯은
`tech-log-backend` @ `a000f87``ManagementDocumentController` 에서 셌고, 환경 구성은
`tech-log-frontend` @ `9e5642c``management-api.openapi.yaml:606-628`(`deleteSetupDraft`)
에서 읽었다. 경로 앞머리가 줄마다 다른 것은 두 문서가 각자 적는 대로 옮겼기 때문이다.
| 종류 | 경로 | 본문 |
|---|---|---|
| Case | `DELETE /api/v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
| Reference | `DELETE /api/v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
| Concept | `DELETE /api/v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
| Setup | `DELETE /api/v1/studio/setups/{id}` | `{"expectedVersion": <저장 버전>}` |
| Question | `DELETE /api/v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
| Decision | `DELETE /api/v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | `{"expectedVersion": <저장 버전>}` |
**본문 없이 부르면 204 가 아니라 422 다.** `ExpectedVersionRequest` 가 없으면
`REQUEST_VALIDATION_FAILED` / `Request body is malformed` 로 거절된다. 헤더에는
`X-CSRF-TOKEN` 이 있어야 한다. 여섯 줄 다 같고, 전에 이 표는 경로만 적고 본문을 적지 않았다.
환경 구성 줄은 2026-09-12 에 시험 작업본 하나를 만들고 이 경로로 지워 204 를 받아 확인했다
(계약 `9e5642c`).
**전에 이 자리에 「Decision 은 계약에 삭제 경로가 없다」고 적혀 있었고 그것은 틀렸다.**
확인 없이 적힌 문장이 옮겨 다녔다 — 이 배치에서 그 문장을 코드 주석과 보고서로 다시 옮긴
일이 있었다. **서버를 서술할 때는 확인한 리비전을 함께 적는다.** 리비전이 없으면 서버가
바뀌어도 아무도 모른다.
**지워지지 않는 것은 따로 있다.** 게시한 적이 있으면 `DOCUMENT_PUBLISHED` 로 409 가 나고
(「공개된 기록은 삭제할 수 없습니다」), 관계로 참조된 문서는 `DOCUMENT_IN_USE` 로 막힌다
(「이 기록을 참조하는 곳이 있어 삭제할 수 없습니다」). 참조하는 쪽의 관계를 먼저 끊는다.
## 실패했을 때
| 증상 | 원인 |
|---|---|
| `aside` 가 안 뜬다 | 인증. 로그인 화면을 자동으로 넘기지 않는다 |
| 미리보기가 본문 전체를 막는다 | Asset 을 올리기 전에 본문을 넣었다. 화면을 새로 고친다 |
| 칸을 못 찾는다 (`label` 매치 0) | 그 종류에 없는 칸이다. `studio-form-map.md` 를 다시 본다 |
| 값이 잘렸다 | 되풀이 칸의 줄 수를 안 맞추고 채웠다 |
| `저장됨` 이 안 뜬다 | 검증이 막은 것이다. `aside` 글자를 읽어 보고한다 |
@@ -0,0 +1,252 @@
# Playwright MCP 로 편집 화면을 다루는 법
여기 적은 셀렉터는 지어낸 것이 아니라 이 저장소가 실제로 돌린 것이다. 원문은
`.playwright-mcp/_p1.mjs`·`_p2.mjs`·`_p3.mjs` 에 남아 있다.
## 두 가지 방식
| 방식 | 언제 |
|---|---|
| MCP 도구를 하나씩 (`browser_navigate` · `browser_snapshot` · `browser_fill_form` · `browser_click`) | 기록 한 건. 화면을 보면서 한다 |
| `browser_run_code_unsafe` 로 한 번에 | 여러 건을 같은 모양으로 채운다 |
**처음 넣는 기록은 하나씩 한다.** 배치는 이미 한 번 성공한 모양을 반복할 때만 쓴다. 화면을
안 보고 배치를 돌리면 못 찾은 칸이 조용히 버려진다.
## 새로 만들 때 — `/studio/documents/new`
종류를 라디오로 고르고 `작업본 만들기` 를 누른다. 라디오의 이름은 화면에 보이는 그대로다.
| 계약 `kind` | 라디오 이름 | 화면이 적어 놓은 칸 |
|---|---|---|
| `CASE` | `검증 기록` | 문제 · 결론 · 환경 · 재현 · 본문 |
| `REFERENCE` | `적용 기준` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| `CONCEPT` | `동작 원리` | 기준 버전 · 본문 |
| `SETUP` | `환경 구성` | 버전 · 본문 |
| `QUESTION` | `열린 질문` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| `PROJECT_DECISION` | `설계 결정` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
**여섯 다 한글이다.** 이 표는 전에 「Concept 만 라디오 이름이 한글(`개념`)이다. 나머지 넷은
영어다」라고 적고 있었고 그것은 낡았다 — 2026-09-12 에 `/studio/documents/new` 를 열어 여섯
라디오의 이름을 그대로 읽었다. **화면 문구는 이렇게 조용히 바뀐다.** 표를 외워서 넣지 말고
스냅샷으로 읽은 이름을 쓴다.
```js
await page.goto('https://hyeonworks.com/studio/documents/new');
await page.getByRole('radio', { name: /^환경 구성/ }).check();
await page.getByRole('button', { name: '작업본 만들기' }).click();
// 주소가 /studio/documents/<uuid>/edit 로 바뀐다. 그 uuid 를 기록 frontmatter 에 적는다
```
화면에 이렇게 적혀 있다 — 「이 화면의 작업본은 현재 Studio 세션에서만 유지됩니다.」
**만들었으면 그 자리에서 칸을 채우고 저장한다.** 만들어 두고 나중에 돌아오지 않는다.
## 화면의 기준점
```js
const RAIL = 'aside[class*="studio-document-status"]';
```
`aside` 가 상태 레일이다. 여기에 버전(`dd` 첫 번째)과 저장 상태(`저장됨`)와 버튼
(`저장`·`게시`)이 있다.
**이것이 25초 안에 안 뜨면 인증이 안 된 것이다.** 로그인 화면을 자동으로 넘기려 들지 않는다.
```js
await page.goto('https://hyeonworks.com/studio/documents/' + id + '/edit');
try { await page.waitForSelector(RAIL, { timeout: 25000 }); }
catch { return { error: 'AUTH? ' + page.url() }; }
```
## 버전 읽기
```js
const version = await page.locator(RAIL + ' dd').first().innerText();
```
저장 전후로 읽는다. 안 올라갔으면 저장이 안 된 것이다.
## 칸 채우기
칸은 `label` 안의 `span` 이 이름을 들고 있다.
```js
const loc = page.locator('xpath=//label[./span[normalize-space(.)="' + label + '"]]')
.locator('textarea, input').first();
if (await loc.count() !== 1) { /* 그 종류에 없는 칸이다 — 채우지 말고 기록한다 */ }
if (await loc.inputValue() === value) { /* 이미 같다 — 건드리지 않는다 */ }
await loc.fill(value);
```
**매치가 1이 아니면 채우지 않고 못 찾았다고 적는다.** 비슷한 이름의 다른 칸에 넣는 것보다
안 넣는 쪽이 낫다.
**이미 같은 값이면 건드리지 않는다.** 안 그러면 바뀐 것이 없는데 버전만 올라간다.
## 되풀이되는 칸은 줄 수를 먼저 맞춘다
`영향`·`선택지`·`사실`처럼 여러 줄인 칸은 `fieldset``legend` 가 이름이고 줄이
`.studio-ordered-item` 이다.
```js
const fs = page.locator('xpath=//fieldset[./legend[normalize-space(.)="' + legend + '"]]').first();
let cur = await fs.locator('.studio-ordered-item').count();
while (cur > want) { // 줄이 남으면 뒤에서 지운다
await fs.locator('.studio-ordered-item').last().getByRole('button', { name: '삭제' }).click();
await page.waitForTimeout(60); cur--;
}
while (cur < want) { // 모자라면 더한다
await fs.getByRole('button', { name: legend + ' 추가' }).click();
await page.waitForTimeout(60); cur++;
}
```
**값을 넣기 전에 한다.** 줄이 모자란 채로 채우면 뒤엣것이 조용히 버려진다.
**행 클래스가 칸마다 다르다.** `관계``.studio-relation-item` 이고 나머지가
`.studio-ordered-item` 이다. 하나로 세면 관계는 늘 0으로 읽힌다.
**더한 뒤에는 카운트가 아니라 라벨 목록으로 검산한다.** 실제로 「판단 기준 추가」를 7번
눌렀는데 행이 9개 생겼다. 카운트만 믿으면 못 잡는다.
```js
// 채우기 전에 그 fieldset 안의 라벨을 전부 읽어 몇 줄인지 다시 센다
const labels = await fs.locator('label span').allInnerTexts();
```
## 칸은 `aria-label` 로 찾는다 (2026-09-17 에 다시 잼)
**위의 `label` 두 모양 이야기는 낡았다.** 지금 `label.studio-field` 가 감싸는 것은 제목과 요약
둘뿐이고, 나머지 칸은 전부 `aria-label` 을 갖는다. xpath 로 `span` 을 뒤질 일이 없다.
```js
document.querySelector('input[aria-label="제목"]')
document.querySelector('textarea[aria-label="요약"]')
document.querySelector('input[aria-label="slug"]')
document.querySelector('input[aria-label="판단 기준 1 제목"]')
document.querySelector('textarea[aria-label="적용할 때 3"]')
```
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
`+ 버전 추가` 다. 줄 수를 먼저 맞추고 값을 넣는 규칙은 그대로다.
## 본문 칸이 사라졌다 — 블록 편집기다
**`본문 Markdown` 이라는 textarea 는 더 없다.** 본문은 `.studio-block-editor` 이고 블록 하나가
`<textarea>` 하나다. 기록 한 편이 블록 171개인 것을 실제로 봤다.
```
.studio-block-editor
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
├ .studio-block-editor__blocks
│ └ .studio-authoring-block[data-kind] heading · paragraph · code · bullet · raw
└ button + 블록 추가
```
**코드 블록의 `input.studio-code-language` 에 울타리 뒤 정보 문자열 전체가 들어간다**
`bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"` 가 그 입력의 값이었다. 라벨을
넣을 별도 칸은 없다.
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 그래서 쓰인 기록을 화면으로 옮기지
않는다 — [studio-api.md](studio-api.md) 의 `PUT` 으로 `bodyMarkdown` 을 통째로 보낸다.
`.studio-markdown-source``readonly` 라 입력 자리가 아니지만, 화면이 지금 무엇을 직렬화할지를
그대로 보여 주므로 **저장 전에 눈으로 대조하기에 좋다.**
## 저장 다음은 게시가 아니라 검증이다
편집 화면의 `aside` 에는 `저장``게시` 둘뿐인데, 저장만 된 기록은 서버가 `nextAction`
**`VALIDATE`** 로 준다. 검증 화면은 `/studio/documents/{id}/validation` 이다. 이 스킬은 저장까지라
거기까지 가지 않는다 — 다만 「저장했는데 왜 게시가 안 되나」의 답이 이것이다.
## Asset 을 올릴 때
`Asset 업로드` 는 모달을 띄우고 **`filechooser` 이벤트를 내지 않는다.** 기다리면 타임아웃이다.
모달 안의 `input[type=file]` 에 직접 넣는다.
```js
await page.getByRole('button', { name: 'Asset 업로드' }).click();
await page.locator('input[type=file]').setInputFiles(svgPath);
// 대체 텍스트 칸에 기록 본문의 alt 를 그대로 넣는다
```
**올린 직후 목록이 갱신되지 않는다.** 화면의 `Asset 검색` 을 다시 눌러야 새 키가 나온다.
새로 올리면 서버가 새 키를 준다(`<이름>-<해시8>`) — 옛 키는 목록에 남는다. 다른 문서가
참조할 수 있으니 지우지 않는다.
## 값을 읽을 때는 `getByLabel` 을 쓰지 않는다
칸이 `<label><span>이름</span><textarea></label>` 모양이라 `getByLabel(...).inputValue()`
30초 타임아웃으로 실패한다. `fill` 은 되는데 읽기만 안 된다. 읽기는 `evaluate` 로 한다.
```js
const got = await page.evaluate((name) => {
const el = [...document.querySelectorAll('label')]
.find(l => l.querySelector('span')?.textContent.trim() === name);
return el?.querySelector('textarea, input')?.value ?? null;
}, '판단 기준');
```
**넣은 값을 다시 읽어 원문과 대조한다.** 넣었다는 것과 들어갔다는 것은 다르다.
## 저장
```js
await page.locator(RAIL).getByRole('button', { name: '저장' }).first().click();
await page.waitForFunction(() => {
const el = document.querySelector('aside[class*="studio-document-status"]');
return el && /저장됨/.test(el.innerText);
}, null, { timeout: 30000 });
```
**버튼을 이름으로 고른다.** `aside button` 의 첫 번째로 고르면 화면이 바뀌었을 때 `게시`
누르게 된다.
`저장됨` 을 못 보면 실패다. 다시 누르지 말고 레일의 글자를 읽어서 보고한다.
```js
const why = (await page.locator(RAIL).innerText()).replace(/\s+/g, ' ').slice(0, 140);
```
## 바뀐 것이 없으면 저장하지 않는다
```js
if (filled.length === 0 && rowsChanged.length === 0) { /* CLEAN — 저장 버튼을 누르지 않는다 */ }
```
누를 때마다 `version` 이 올라가고, 버전이 올라가면 앞서 만든 검증·미리보기 산출물이 무효가
된다(`validatedVersion == document.version` 이 깨진다).
## 미리보기
**`즉시 미리보기` 는 탭이 아니다.** 편집 화면 오른쪽에 나란히 놓인 `region` 이라
`getByRole('tab', ...)` 은 0개를 찾는다. 화면에 `[role="tab"]` 자체가 없다.
```js
await page.getByRole('region', { name: /즉시 미리보기/ }).scrollIntoViewIfNeeded();
```
저장한 값이 아니라 화면에 입력한 값을 렌더링한다. **Asset 을 방금 올렸으면 편집 화면을 한 번
새로 고친다** — 안 그러면 편집 화면이 들고 있는 옛 asset 목록에서 키를 못 찾아 본문 전체가
막힌다.
```text
초안을 미리 볼 수 없습니다
1:1 supported local evidence key not found: <asset-key>
```
## 배치로 돌릴 때 돌려받을 것
한 건마다 이만큼은 남긴다. 무엇이 채워졌고 무엇을 못 찾았는지 없이 「성공」만 돌려받으면
못 찾은 칸이 묻힌다.
```js
{ name, version, saved: 'OK v13' | 'CLEAN' | 'FAIL <레일 글자>',
filled: ['문제','결론'], skipped: 2, miss: ['적용 조건'], rows: ['영향 2→3'] }
```
## 절대로 하지 않는 것
- `게시` 버튼을 누르지 않는다
- 로그인 화면에 자격증명을 입력하지 않는다
- `저장됨` 이 안 뜬다고 연타하지 않는다
- 시험 삼아 문서를 만들지 않는다 — 한 번 게시한 문서는 취소해도 삭제가 409 로 거절된다
@@ -0,0 +1,180 @@
# Studio 가 실제로 주고받는 것
여기 적은 것은 **2026-09-17 에 로그인한 브라우저에서 직접 불러 확인한 것**이다. 문서를 보고
적은 것이 아니라 요청을 보내고 돌아온 것을 옮겼다. 확인한 판이 바뀌면 이 파일도 낡는다.
## 왜 이 파일이 필요해졌나
**편집 화면의 본문이 블록 편집기로 바뀌었다.** 전에는 본문이 `본문 Markdown` 이라는 textarea
하나였고 거기에 통째로 붙여넣으면 됐다. 지금은 블록 하나가 `<textarea>` 하나다 — 기록 한 편이
블록 171개인 것을 실제로 봤다. 화면으로 그것을 채우는 것은 사람이 한 편을 쓸 때의 방식이지
쓰인 기록을 옮기는 방식이 아니다.
그런데 **저장이 서버로 보내는 것은 블록이 아니라 마크다운 문자열 하나**다. 화면은 그 문자열
위의 편집기이고, 저장 요청은 `bodyMarkdown` 한 칸을 보낸다. 그래서 옮기는 일은 화면이 아니라
이 API 로 한다.
## 인증
**자격증명을 이 저장소에 두지 않는다.** 이미 로그인된 브라우저 세션을 쓰고, 요청은 그 페이지
안에서(`fetch`) 보낸다. 쿠키가 그대로 실린다.
CSRF 토큰은 `XSRF-TOKEN` 쿠키에 있고 `x-csrf-token` 헤더로 되돌려 준다.
```js
document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/)?.[1]
```
로그인이 안 돼 있으면 `/studio` 가 「기록을 쓰려면 로그인이 필요합니다」를 낸다.
**로그인 화면을 자동으로 통과하려 들지 않는다** — 사람에게 로그인해 달라고 말하고 멈춘다.
## 응답 껍데기
성공이든 실패든 같은 모양이다.
```json
{ "success": true, "data": { }, "error": null,
"meta": { "requestId": "…", "traceId": "…", "correlationId": "…", "page": null } }
```
실패하면 `data``null` 이고 `error``{code, category, message, retryable, details}` 가 온다.
`details.supportedMethods` 처럼 고칠 방법을 담아 주는 경우가 있다.
## 경로
| 무엇 | 요청 |
|---|---|
| 목록 | `GET /api/v1/studio/documents?limit=100` |
| 한 편 | `GET /api/v1/studio/documents/{id}` |
| 새로 만들기 | `POST /api/v1/studio/documents` → 201 |
| **저장** | `PUT /api/v1/studio/documents/{id}` → 200 |
| 지우기 | `DELETE /api/v1/studio/{종류}/{id}` → 204 |
**`limit` 은 100 이 상한이다.** 200 을 주면 422 `REQUEST_VALIDATION_FAILED` 다. 더 받으려면
`data.nextCursor``cursor` 로 넘긴다.
**`/documents/{id}``PUT``GET` 만 받는다.** 거기에 `DELETE` 를 보내면 405 이고
`details.supportedMethods``["PUT","GET"]` 을 준다. 삭제는 종류별 경로다 —
`cases` · `references` · `concepts` · `setups` · `questions`, 그리고 Decision 은
`projects/{projectId}/decisions/{decisionId}`.
## 한 편 읽기
```
GET /api/v1/studio/documents/{id}
→ data: { document, currentValidation, latestPreview, currentPublication,
dependencyRevision, nextAction }
```
`nextAction` 이 다음에 할 일을 말한다 — 저장만 된 기록은 `VALIDATE` 다. 검증 화면은
`/studio/documents/{id}/validation` 이고 편집 화면의 `aside` 에는 **`저장``게시` 둘뿐**이라
검증은 그 화면에서 따로 한다.
## 저장
```
PUT /api/v1/studio/documents/{id}
headers: content-type: application/json
accept: application/json
x-csrf-token: <XSRF-TOKEN 쿠키>
idempotency-key: <요청마다 새로>
body: { "expectedVersion": <읽어 온 document.version>, "document": { … } }
```
**`expectedVersion` 은 방금 읽은 `document.version` 이다.** 저장이 끝나면 1 올라간다. 지어내지
말고 저장 직전에 `GET` 으로 읽는다.
`document`**그 종류의 칸을 전부** 담는다. 빠뜨린 칸은 비는 것으로 저장된다 — 부분 갱신이
아니다.
**다만 `GET` 으로 받은 것을 그대로 되보내면 400 이다.** `id` · `version` · `updatedAt` 은 서버가
주기만 하고 받지는 않아서, 그 셋이 들어 있으면 `VALIDATION_FAILED`
`UnrecognizedPropertyException` 으로 거절된다(2026-09-17 에 두 번 겪었다). 서버 상태는 안
바뀌니 셋을 빼고 다시 보내면 된다. 어느 칸을 보내고 어느 칸을 빼는지는 바로 아래 표에 있다.
## 종류마다의 칸
여섯이 함께 갖는 것: `kind` · `title` · `slug` · `summary` · `relations` · `projectId` ·
`topicId` · `variantIds`. 서버가 주지만 보내지 않는 것: `id` · `version` · `updatedAt`.
| kind | 그 종류만의 칸 |
|---|---|
| `CASE` | `problem` · `conclusion` · `environment` · `reproduction` · `bodyMarkdown` · `lastVerifiedOn` |
| `CONCEPT` | `bodyMarkdown` · `basisVersion` |
| `SETUP` | `bodyMarkdown` · `pinnedVersions` |
| `REFERENCE` | `purpose` · `rules` · `applyWhen` · `exceptions` · `examples` · `verifiedOn` |
| `QUESTION` | `facts` · `assumptions` · `unknowns` · `constraints` · `options` · `nextValidation` · `resolution` · `questionStatus` |
되풀이되는 칸의 모양이다. `id` 는 서버가 붙이므로 새로 넣을 때는 빼고 `order` 만 0 부터 센다.
| 칸 | 항목 |
|---|---|
| `relations` | `{ targetId, reason, order }` |
| `facts` · `assumptions` · `unknowns` · `constraints` · `applyWhen` · `exceptions` · `examples` | `{ text, order }` |
| `options` | `{ title, description, order }` |
| `rules` | `{ title, body, order }`**제목과 본문이 따로다** |
| `pinnedVersions` | `{ name, version }``order` 가 없다 |
## `bodyMarkdown` 은 바이트 그대로 돌아온다
넣은 것과 읽어 온 것이 같은지 확인했다. **코드 울타리의 정보 문자열이 그대로 살아남는다.**
```
```bash label="[host] ① 친다"
```
넣고 다시 읽었을 때 `label="…"` 까지 한 글자도 안 바뀌었다. 그래서 `studio-body.py` 가 만든
본문을 그대로 실어 보내면 된다.
## 화면에서 블록이 어떻게 생겼나
옮기는 일은 API 로 하지만, 사람이 화면을 읽을 때와 무엇이 어긋났는지 볼 때 필요하다.
```
.studio-block-editor
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
├ .studio-block-editor__blocks
│ └ .studio-authoring-block[data-kind] 블록 하나
└ button + 블록 추가
```
`data-kind``heading` · `paragraph` · `code` · `bullet` · `raw` 다. `raw`(화면 이름
**고급 블록**)가 표처럼 블록으로 안 갈리는 것을 그대로 담는다.
**코드 블록의 `input.studio-code-language` 는 언어 이름만 담지 않는다.** 울타리 뒤의 정보
문자열 **전체**가 거기 들어간다 — `bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"`
가 그 입력의 값이었다. 라벨을 넣을 별도 칸은 없다.
**`.studio-markdown-source` 는 읽는 데만 쓴다.** `readonly` 이고 `display:none` 이라 입력
자리가 아니다. 다만 화면이 지금 무엇을 직렬화할지를 그대로 보여 주므로, 저장 전에 눈으로
대조하기에 좋다.
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 화면으로 본문을 채우려면 블록을 하나씩
만들어야 한다.
## 화면의 칸 셀렉터
칸은 이제 `aria-label` 로 찾는다. `label.studio-field` 는 제목과 요약 둘만 감싼다.
```js
document.querySelector('input[aria-label="제목"]')
document.querySelector('textarea[aria-label="요약"]')
document.querySelector('input[aria-label="slug"]')
document.querySelector('input[aria-label="판단 기준 1 제목"]')
document.querySelector('textarea[aria-label="적용할 때 3"]')
```
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
`+ 버전 추가` 다.
상태 레일은 그대로다 — `aside[class*="studio-document-status"]`(실제 class 는
`studio-document-status-bar`). 그 안에 **`저장``게시` 둘뿐**이고, 저장된 상태에서는
`저장``disabled` 이며 글자가 `저장됨` 이다.
## 하지 않는 것
- **게시하지 않는다.** 이 스킬은 저장까지다. 한 번이라도 게시한 문서는 게시를 취소해도 삭제가
409 로 거절된다
- **`expectedVersion` 을 지어내지 않는다.** 저장 직전에 읽는다
- **부분 갱신이라고 생각하지 않는다.** 보내지 않은 칸은 비워진다
- **로그인 화면을 자동으로 넘기지 않는다**
@@ -0,0 +1,137 @@
# 기록 `.md` 의 어디가 Studio 의 어느 칸인가
## 세 자리
| 기록 `.md` | Studio |
|---|---|
| frontmatter | 메타데이터. 화면 칸이 아니다 |
| 제목 바로 아래 첫 문단 | **`요약` 칸** |
| `## <이름>` | **같은 이름의 칸** |
`## 요약` 이라는 절을 만들지 않는다 — Studio 에 그런 칸이 없어 통째로 사라진다. `## 출처`
칸이 아니다. 원본 경로는 frontmatter 의 `source` 에 있다.
## 종류마다의 칸
| 종류 | 기록 `.md``##` 이름 | 본문 |
|---|---|---|
| **Case** | `관계` · `문제` · `결론` · `검증 환경` · `재현 조건` · `본문` | 있음 |
| **Concept** | `관계` · `본문` | 있음 |
| **Setup** | `관계` · `본문` — 본문 안의 `##` 는 칸이 아니다 | 있음 |
| **Reference** | `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시` | 없음 |
| **Question** | `관계` · `사실` · `가정` · `미지수` · `제약` · `선택지` · `다음 검증` | 없음 |
| **Decision** | **`근거`** · `결정문` · `판단 이유` · `영향` | 없음 |
**Decision 만 관계 절 이름이 `근거` 다.** 그리고 근거가 1개 이상 없으면 게시가 거절된다.
## 환경 구성은 `##` 를 칸으로 세지 않는다
다른 다섯은 `## <이름>` 하나가 칸 하나다. 환경 구성은 화면 칸이 `고정한 버전`
`절차 Markdown` 둘뿐이고, **`## 실행 절차` · `## 구성 값` · `## 확인 방법` 은 그 `절차 Markdown`
안의 소제목**이다. 계약이 `bodyMarkdown` 설명에 「절 이름을 강제하지 않는다 — 프로젝트마다
셋업의 모양이 다르다」고 적는다.
그래서 넣는 법이 다르다. 기록의 `## 본문` 아래 전체가 `bodyMarkdown` 한 칸으로 들어가고, 화면
칸에 따로 옮길 값은 `고정한 버전` 하나뿐이다.
| 기록 `.md` | 어디로 |
|---|---|
| frontmatter `pinnedVersions[]` | `고정한 버전` — 줄마다 `이름`·`버전` 입력 둘 |
| `## 본문``<!-- body:start -->`~`<!-- body:end -->` | `절차 Markdown` 통째로 |
| `## 관계` | `관계` |
작업본을 만들면 `절차 Markdown` 이 비어 있지 않다. Studio 가 위의 절 셋을 미리 넣어 두므로,
본문을 넣기 전에 그 내용을 지운다.
## 화면의 라벨은 기록의 절 이름과 다르다
**이것이 이 문서에서 가장 자주 틀리는 자리다.** 위 표는 기록 `.md` 가 쓰는 이름이고,
편집 화면의 라벨은 다른 말을 쓴다. 그리고 `/studio/documents/new` 의 종류 카드에 적힌 요약
(`목적 · 규칙 · 적용 조건 · 예외 · 예시`)은 **카드 문구이지 편집 화면의 라벨이 아니다.**
Reference 에서 실제로 확인한 대응이다.
| 기록의 `##` | 편집 화면의 라벨 |
|---|---|
| `목적` | **`이 기준을 쓰는 이유`** |
| `규칙` | **`판단 기준`** — 제목과 본문 두 칸이 한 줄이다 |
| `적용 조건` | **`적용할 때`** |
| `예외` | **`예외와 주의`** |
| `예시` | `예시` |
| `관계` | `관계` |
종류 이름도 화면마다 달랐다. 상태 레일이 Reference 를 **`적용 기준`** 이라고 부르는 동안
`새 문서` 화면의 라디오는 `Reference` 였다. 2026-09-12 에는 `새 문서` 쪽도 여섯 다 한글이다 —
검증 기록 · 적용 기준 · 동작 원리 · 환경 구성 · 열린 질문 · 설계 결정.
**화면을 먼저 스냅샷으로 읽고 그 라벨을 쓴다.** 이 표를 외워서 넣지 않는다 — 화면이 바뀌면
표가 먼저 낡는다.
## 기록에 없는데 화면에 있는 칸
| 칸 | 무엇 |
|---|---|
| `축` | 주제 안의 변이(SPA · Mediator · BFF · Forward-Auth). **주제를 고른 뒤에 나타난다.** 안 고르면 주제 공통 기록이 된다 |
| `마지막 검증일` | 기록의 `verifiedOn` 이다. 없으면 비워 둔다 |
**근거가 없으면 비워 둔다.** `verifiedOn` 이 없는 채로 저장하면 미리보기에 「마지막 검증」 절이
값 없이 뜬다. 그것은 날짜를 지어내는 것보다 낫다 — 게시 전에 사람이 채울지 정한다.
## frontmatter 에서 화면으로 가는 값
| frontmatter | 어디로 |
|---|---|
| `id` | 편집 주소 `/studio/documents/<id>/edit` |
| `kind` | 새 문서를 만들 때 고르는 종류 |
| `slug` · `title` | 화면 위쪽의 슬러그·제목 칸 |
| `topic` · `topicName` · `project` | 주제·프로젝트 선택 |
| `basisVersion` (Concept) | 기준 버전 칸 |
| `pinnedVersions` (Setup) | 고정한 버전 칸. `name` · `version` 이 한 줄 |
| `questionStatus` (Question) · `decisionStatus` (Decision) | 상태 선택 |
| `assets[].file` | 올릴 Asset 파일 |
| `assets[].key` | 본문 `:::evidence key` 의 저장소 쪽 이름. 올리면 서버 키로 바뀐다 |
**계약이 화면에 주는 상태와 도메인이 들고 있는 상태가 다르다.** `QuestionStatus` 는 화면에서
`OPEN`·`RESOLVED` 둘인데 도메인은 `OPEN`·`INVESTIGATING`·`PAUSED`·`RESOLVED` 넷이고,
Decision 의 도메인 `ACCEPTED` 가 화면에서는 `ADOPTED` 로 보인다. 화면에서 고른 값이 도메인
상태를 덮어쓰지는 않는다.
## 본문이 없는 세 종류
`Reference`·`Question`·`Decision` 의 칸은 **평문으로 렌더링된다.**
**「평문」이 「전부 글자 그대로」라는 뜻은 아니다.** 렌더러
(`tech-log-frontend``presentation/shared/public-render/prose-text.tsx`)가 셋을 해석한다.
| 무엇 | 평문 칸에서 |
|---|---|
| 백틱 쌍 | **인라인 `<code>` 로 산다.** 빼지 않는다 — 빼면 식별자가 민무늬로 나온다 |
| 빈 줄 | 문단이 갈린다 |
| 한 줄 바꿈 | `<br>` |
| 별표 · 파이프 · `#` · 코드펜스 · 인용 표지 `>` | **글자 그대로 나온다.** 넣지 않는다 |
**전에 이 자리에 「백틱과 파이프가 글자 그대로 보인다」고 적혀 있었고 백틱 쪽은 틀렸다.**
백틱이 글자로 나오던 것은 고쳐진 옛 버그이고 그 파일 주석에 그렇게 적혀 있다. 칸이 어떻게
보이는지는 **렌더러가 정본이다** — 이 문서가 아니다.
- 나열은 쉼표로 잇지 말고 `이름 : 값` 으로 줄을 나눈다
코드·표·그림이 필요하면 짝이 되는 Case·Concept·Setup 에 담고 `관계` 로 가리킨다.
## 본문을 넣기 전에
저장소의 `.md` 는 그림을 마크다운 이미지로 싣는다. Studio 는 `:::evidence` 를 쓴다. 바꾸는 것은
스크립트가 한다.
```bash
python3 scripts/studio-body.py <기록.md> --body-only # 저장소 key 그대로
python3 scripts/studio-body.py <기록.md> --key a=a-1a2b3c4d --body-only # 서버가 준 key 로
```
저장소 파일 자체는 고치지 않는다.
## 되돌아오는 값
저장이 끝나면 Studio 가 `id` 를 준다. 새로 만든 기록이면 frontmatter 의 `id``studio:`
채운다. 그 두 칸이 차면 색인이 `publication``게시됨` 으로 적는데, **그것은 「Studio 에
있다」는 뜻이고 공개됐다는 뜻이 아니다.** 공개 여부는 `status``public:` 이 말한다.
@@ -1,6 +1,9 @@
--- ---
name: refactoring-from-analysis name: refactoring-from-analysis
description: Use when a completed codebase analysis should be turned into one bounded, evidence-backed refactoring WorkItem and implemented in isolation. description: Use when a completed codebase analysis should be turned into one bounded, evidence-backed refactoring WorkItem and implemented in isolation.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# Refactoring From Analysis # Refactoring From Analysis
@@ -1,6 +1,9 @@
--- ---
name: rewriting-technical-prose-naturally name: rewriting-technical-prose-naturally
description: Use when an existing Korean technical document, section, or heading already contains grounded facts but sounds AI-written, slogan-like, overly polished, abstract, compressed, or mechanically contrasted and must be rewritten without changing its technical meaning. description: Use when an existing Korean technical document, section, or heading already contains grounded facts but sounds AI-written, slogan-like, overly polished, abstract, compressed, or mechanically contrasted and must be rewritten without changing its technical meaning.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# Rewriting Technical Prose Naturally # Rewriting Technical Prose Naturally
@@ -103,8 +103,18 @@ const ACRONYM_OK = new Set([
'CANCEL','GREEN','RED','TODO','NOTE','CODE','BLOCK','AND','OR','NOT','NULL','TRUE','FALSE', 'CANCEL','GREEN','RED','TODO','NOTE','CODE','BLOCK','AND','OR','NOT','NULL','TRUE','FALSE',
]); ]);
// YAML frontmatter 는 글쓴이가 고르는 문장이 아니라 **계약에서 옮겨 온 메타데이터**다.
// 여기에 문장 규칙을 걸면 고칠 수 없는 error 가 나고, 글쓴이는 통과하려고 **칸을 지운다** —
// 실제로 `topicName: 측정이 거짓말하는 자리` 가 spatial-metaphor 로 잡혀 그 주제의 기록들이
// 그 칸 없이 저장됐다. 줄 번호는 유지한 채 비운다.
function stripFrontMatter(src) {
const m = /^---\r?\n[\s\S]*?\r?\n---(\r?\n|$)/.exec(src);
if (!m) return src;
return m[0].replace(/[^\n]/g, ' ') + src.slice(m[0].length);
}
function strip(src) { function strip(src) {
return src return stripFrontMatter(src)
.replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, ' ')) .replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, ' '))
.replace(/`[^`\n]*`/g, (m) => ' '.repeat(m.length)) .replace(/`[^`\n]*`/g, (m) => ' '.repeat(m.length))
// 표와 인용은 「쓰지 않는다」 예시가 사는 자리다. 규칙을 적은 문서가 그 규칙을 어긴 것으로 // 표와 인용은 「쓰지 않는다」 예시가 사는 자리다. 규칙을 적은 문서가 그 규칙을 어긴 것으로
@@ -22,7 +22,9 @@ const BASE = {
// 산문만 남긴다. 코드블록·표·제목·목록·링크주소·인라인코드는 문장이 아니다. // 산문만 남긴다. 코드블록·표·제목·목록·링크주소·인라인코드는 문장이 아니다.
function strip(src) { function strip(src) {
let t = src; // frontmatter 는 산문이 아니다. 빼지 않으면 그 블록 하나가 길이 398자짜리
// 「문장」으로 세어져 avgLen · longRatio 를 통째로 흔든다 (실측)
let t = src.replace(/^---\n[\s\S]*?\n---\n/, '');
// 짝이 맞는 코드펜스 제거 // 짝이 맞는 코드펜스 제거
t = t.replace(/```[\s\S]*?```/g, '\n'); t = t.replace(/```[\s\S]*?```/g, '\n');
// 짝이 안 맞는 펜스(구획을 중간에서 잘랐을 때): 남은 펜스부터 끝까지 버린다 // 짝이 안 맞는 펜스(구획을 중간에서 잘랐을 때): 남은 펜스부터 끝까지 버린다
@@ -71,6 +73,7 @@ export function profile(raw) {
// 백틱 안(식별자)은 빼고, 맨몸으로 쓰인 영문만 센다 // 백틱 안(식별자)은 빼고, 맨몸으로 쓰인 영문만 센다
const bare = raw const bare = raw
.replace(/^---\n[\s\S]*?\n---\n/, ' ') // frontmatter 는 산문이 아니다
.replace(/```[\s\S]*?```/g, ' ') .replace(/```[\s\S]*?```/g, ' ')
.replace(/<!--[\s\S]*?-->/g, ' ') // HTML 주석(techviz 등)은 산문이 아니다 .replace(/<!--[\s\S]*?-->/g, ' ') // HTML 주석(techviz 등)은 산문이 아니다
.replace(/<\/?[a-zA-Z][^>]*>/g, ' ') // <details>, <summary> 같은 태그 .replace(/<\/?[a-zA-Z][^>]*>/g, ' ') // <details>, <summary> 같은 태그
@@ -78,7 +81,10 @@ export function profile(raw) {
.replace(/`[^`\n]*`/g, ' ') .replace(/`[^`\n]*`/g, ' ')
.replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1'); .replace(/!?\[([^\]]*)\]\([^)]*\)/g, '$1');
const PROPER = /^(Redis|Nginx|Hibernate|Spring|Actuator|Keycloak|PostgreSQL|Java|Gradle|Lettuce|Kubernetes|Docker|OAuth|Sentinel|Lua|SQL|API|TTL|ACL|TLS|HTTP|JSON|YAML|CI|AI|DB|ID|URL)$/i; const PROPER = /^(Redis|Nginx|Hibernate|Spring|Actuator|Keycloak|PostgreSQL|Java|Gradle|Lettuce|Kubernetes|Docker|OAuth|Sentinel|Lua|SQL|API|TTL|ACL|TLS|HTTP|JSON|YAML|CI|AI|DB|ID|URL)$/i;
const engWords = (bare.match(/[A-Za-z][A-Za-z0-9_.-]{1,}/g) || []).filter(w => !PROPER.test(w)); // 분자와 분모가 같은 글을 봐야 「문장당」이 뜻을 갖는다. 문장 수는 strip() 이 목록을
// 걷어낸 `text` 에서 세는데 영문은 `bare`(목록 포함)에서 세고 있었다 — 본문이 거의
// 목록인 Question 기록에서 이 비율이 구조적으로 부풀었다 (실측 11.94)
const engWords = (text.match(/[A-Za-z][A-Za-z0-9_.-]{1,}/g) || []).filter(w => !PROPER.test(w));
// 한글 비율은 글쓴이가 고를 수 있는 산문만 본다. 백틱 안 식별자는 보호 구간이라 제외한다. // 한글 비율은 글쓴이가 고를 수 있는 산문만 본다. 백틱 안 식별자는 보호 구간이라 제외한다.
const hangul = (bare.match(/[가-힣]/g) || []).length; const hangul = (bare.match(/[가-힣]/g) || []).length;
const letters = (bare.match(/[가-힣A-Za-z]/g) || []).length || 1; const letters = (bare.match(/[가-힣A-Za-z]/g) || []).length || 1;
@@ -0,0 +1,208 @@
---
name: running-tech-log-pipeline
description: Use when a codebase must go all the way to a saved Tech Log Studio draft — running the seven stages (SSOT, tree, record, diagram, prose, voice, Studio save) as separate subagents, one skill per stage, with a run ledger that records which skill each stage actually used and which gate it passed.
metadata:
version: "1.0.0"
language: "ko-KR"
---
# Tech Log 파이프라인 실행
## 이 스킬이 하는 일
스킬 일곱 개를 **한 줄기로 잇는다.** 각 스킬은 자기 단계만 알고 앞뒤를 모른다. 이 스킬이
단계 사이의 인계물과 순서와 관문을 정하고, 단계마다 **서브에이전트를 하나씩 띄운다.**
한 실행이 남기는 것은 산출물과 **런 원장**(`runs/<프로젝트>/<runId>/run.json`) 둘이다.
원장은 단계마다 어떤 스킬을 실제로 읽었고 어떤 관문을 어떤 종료 코드로 지났는지를 적는다.
`python3 scripts/verify-pipeline-run.py <원장>` 이 그 원장을 검사한다.
**원장이 이 스킬의 존재 이유다.** 스킬을 안 읽고 쓴 글과 읽고 쓴 글은 결과물만 봐서는
구분되지 않는다. 원장은 그것을 기계가 구분할 수 있게 만든다.
## 왜 단계마다 서브에이전트인가
한 세션이 일곱 단계를 이어서 하면 앞 단계의 문맥이 뒤 단계로 새어 든다. 그러면 세 가지가
망가진다.
- **스킬을 안 읽고도 그럴듯하게 쓴다.** 3단계에서 본문 규칙을 읽었으니 5단계에서
`rewriting-technical-prose-naturally` 를 열지 않아도 문장이 나온다. 스킬이 적용됐는지
확인할 방법이 사라진다.
- **관문이 형식이 된다.** 자기가 쓴 글을 자기가 검사하면 실패를 「이건 예외」로 넘긴다.
- **어디서 어긋났는지 못 찾는다.** 결과가 이상할 때 일곱 단계 중 어디가 원인인지 가릴 수 없다.
서브에이전트는 **자기 단계의 입력 파일과 자기 스킬만** 받는다. 다른 단계가 무엇을 했는지
모른다. 그래서 스킬을 안 읽으면 못 쓰고, 못 쓰면 원장에 남는다.
## 일곱 단계
| # | 단계 | 스킬 | 에이전트 | 산출물 |
|---|---|---|---|---|
| S1 | 코드베이스 → SSOT | `analyzing-codebase-for-tech-log` | `ssot-analyst` | `docs/<프로젝트>/final/document.md` |
| S2 | SSOT → 분해 계약 | `deriving-tech-log-root-tree` | `tree-deriver` | `docs/<프로젝트>/tech-log-studio/tech-log-tree.json` |
| S3 | 글감 → 기록 | `writing-tech-log-records` | `record-writer` | `.../<주제>/<종류>/<기록>.md` |
| S4 | 기록 → 그림 | `technical-visualizer` | `diagram-maker` | `final/assets/<이름>/` · `final/.techviz/<이름>/` |
| S5 | AI 티 제거 | `rewriting-technical-prose-naturally` | `prose-rewriter` | 같은 기록 파일 (제자리 수정) |
| S6 | 일한 사람의 목소리 | `writing-as-the-person-who-did-it` | `voice-writer` | 같은 기록 파일 (제자리 수정) |
| S7 | Studio 저장 | `publishing-tech-log-to-studio` | `studio-validator` | Studio 작업본 + `studio:` URL. **게시하지 않는다** |
단계마다의 입력·관문·원장 칸은 [references/stage-contracts.md](references/stage-contracts.md).
서브에이전트에 그대로 넣는 프롬프트는 [references/subagent-prompts.md](references/subagent-prompts.md).
## 에이전트를 그때그때 만들지 않는다
단계마다 맡을 에이전트가 `.claude/agents/` 에 있다. `Agent` 도구의 `subagent_type` 에 위 표의
이름을 준다.
```
Agent(subagent_type="record-writer", prompt=<references/subagent-prompts.md 의 S3>)
```
프롬프트만 새로 써서 일반 에이전트를 띄우면 **어떤 규칙으로 일했는지가 어디에도 안 남는다.**
`skillEcho` 는 「스킬을 열었다」를 증명하지만 **누가 열었는지는 증명하지 않는다** — 매번 새로
띄운 에이전트도 SKILL.md 를 읽고 한 줄을 옮겨 적을 수 있다. 그래서 원장의 `runBy` 가 이름을
담고, 검사기가 계약과 대조한 뒤 그 `.md` 가 실재하는지까지 본다.
**배정의 정본은 `scripts/verify-pipeline-run.py` 의 `STAGES` 다.** 틀(`templates/run.json`)에도
같은 값이 적혀 있고, 둘이 갈리면 시험이 잡는다.
에이전트 정의는 그 단계의 「하는 일 · **안 하는 일** · 관문 · 보고」를 적는다. 프롬프트가
그것을 되풀이하지 않아도 되는 것이 이 방식의 값이다 — 프롬프트는 **이번 런의 입력 경로와
글감**만 준다.
**단계가 아닌 에이전트가 넷 더 있다.** 기록 한 편을 놓고 역할을 가른 것이라 아무 단계에도
붙지 않는다. 부를지는 사람이 정하고, 원장의 단계 칸에는 안 들어간다.
| 에이전트 | 언제 | 안 하는 일 |
|---|---|---|
| `source-auditor` | S3 앞. 원본의 주장을 `관측`·`추론`·`미검증`으로 가른 표를 만든다 | 기록을 안 쓴다 |
| `fact-reviewer` | 쓴 뒤. 결과 문장을 원문과 한 글자씩 역대조한다 | 파일을 안 고친다 |
| `reader-reviewer` | 쓴 뒤. 제목·요약·목차만 보고 30초 안에 읽히는지 본다 | 본문을 안 연다 |
| `setup-runner` | Setup 을 쓴 뒤. 손으로 끝까지 칠 수 있는지 읽는다 | 실제로 치지는 않는다 |
## 순서가 고정된 곳
세 자리는 바꾸면 결과가 틀어진다.
**S3 → S4.** 그림은 기록을 쓴 다음에 만든다. 무엇을 그릴지는 기록 본문이 정하고
(`writing-tech-log-records/references/choosing-a-diagram.md` 의 세 관문), 그림의 근거는 그
기록의 `source` 앵커가 가리키는 SSOT 절이다. **기록 본문이 그림의 소재를 고르고, SSOT 절이
그림의 사실을 댄다.** 기록 `.md``techviz prepare` 에 넣지 않는다 — 기록은 SSOT 의 인용이라
줄 번호가 근거가 되지 못한다.
**S4 → S5 → S6.** 문장 손질은 본문이 다 찬 뒤에 한다. 그림을 붙이면 본문에 그림을 읽는 문단이
한둘 늘고, 그 문단도 같은 손질을 받아야 한다. S5 가 먼저다 — 번역투와 반복 문형을 걷어낸 뒤라야
S6 이 채울 자리(선택·비교·어긋남)가 보인다. 순서를 뒤집으면 S5 가 S6 이 넣은 목소리를
「과한 대구」로 다시 깎는다.
**S6 → S7.** Studio 에는 문장 손질이 끝난 것만 넣는다. 저장한 뒤에 고치면 Studio 쪽 `version`
이 올라가고 검증 산출물이 무효가 된다.
## S5·S6 뒤에는 S3 관문을 다시 돌린다
문장을 고치면 본문 문법과 인용이 함께 움직인다. `check_prose` 가 error 0 이어도 `check_body`
가 깨지거나, 고쳐 쓴 코드블록 한 줄이 SSOT 와 달라져 `check_evidence` 가 걸린다.
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
원장에서는 이것이 S5·S6 의 관문이지 S3 의 재실행이 아니다.
## 건너뛰어도 되는 단계
이미 있는 것을 다시 만들지 않는다. 건너뛴 단계도 **원장에 `SKIPPED` 와 사유를 적는다.**
적지 않고 빠뜨린 것과 판단해서 건너뛴 것을 구분해야 한다.
| 단계 | 건너뛰는 조건 |
|---|---|
| S1 | `final/document.md` 가 있고 그 안에서 이 글감의 근거를 찾을 수 있다 |
| S2 | `tech-log-tree.json` 에 이 글감이 `PROMOTE` · `CONFIRMED` 로 이미 있다 |
| S4 | 그림이 필요 없다(세 관문에 걸린다) 또는 `ssot-assets` 가 배정한 그림이 이미 있다 |
| S7 | 사용자가 Studio 반입을 요청하지 않았다 |
**S3·S5·S6 은 건너뛰지 않는다.** 기록을 쓰는 단계와 문장을 고치는 두 단계는 이 파이프라인의
산출물 자체다.
## 실행 절차
### 0. 런을 연다
```bash
python3 scripts/verify-pipeline-run.py --init runs/<프로젝트>/<runId>/run.json \
--project <프로젝트> --record <기록 경로>
```
`runId``YYYY-MM-DD-HHMM` 이다. 원장의 틀은
[templates/run.json](templates/run.json) 이고, `--init` 이 그 틀을 채워 놓는다.
`--init` 은 단계마다 `skillRevision` 도 적는다 — 그 시점 그 스킬의 커밋이다. 스킬은
나중에 고쳐지고, 그러면 이 런의 영수증(`skillEcho`)이 현재 SKILL.md 에서 사라진다.
그 커밋이 적혀 있으면 검사기가 이력을 훑지 않고 그것 하나로 대조한다. 작업 트리가 그
커밋과 다르면 `null` 이다 — 모르는 리비전을 지어내지 않는다. 칸이 없는 옛 원장은
이력 훑기로 떨어지고, 그것도 정상이다.
### 1. 단계마다 서브에이전트를 띄운다
에이전트는 위 표의 것을 쓴다 — `Agent(subagent_type="<이름>", ...)`. 새로 만들지 않는다.
프롬프트는 [references/subagent-prompts.md](references/subagent-prompts.md) 의 것을 쓴다.
프롬프트에 **반드시** 들어가야 하는 넷이 있다.
1. **스킬 이름과 「먼저 그 SKILL.md 를 끝까지 읽어라」** — 요약을 주지 않는다. 요약을 주면
에이전트가 스킬을 안 연다.
2. **자기 단계의 입력 파일 경로만.** 앞 단계가 무엇을 했는지 설명하지 않는다.
3. **관문 명령 원문과 「error 0 까지 고쳐라」.**
4. **스킬 영수증** — 그 SKILL.md 에서 자기 단계에 해당하는 규칙 한 줄을 **원문 그대로**
인용해 돌려보내게 한다. 이것이 스킬을 열었다는 기계 검증 가능한 증거다
(`verify-pipeline-run.py` 가 그 문자열이 실제 SKILL.md 안에 있는지 대조한다).
한 단계가 끝나면 그 결과를 원장에 적고 다음 단계를 띄운다. **단계를 병렬로 띄우지 않는다**
S1~S7 은 앞 단계의 산출물이 뒤 단계의 입력이다. 병렬이 되는 것은 같은 단계 안에서
**서로 다른 기록 여러 건**을 처리할 때뿐이다.
### 2. 원장을 검사한다
```bash
python3 scripts/verify-pipeline-run.py runs/<프로젝트>/<runId>/run.json
```
error 0 이어야 런이 끝난 것이다. 이 검사기가 보는 것은 결과물의 품질이 아니라 **절차의
준수**다 — 단계가 빠졌는지, 스킬 영수증이 그 스킬의 실제 문장인지, 관문이 돌았고 종료 코드가
0 이었는지, 적어 낸 산출물이 디스크에 있는지.
영수증은 셋이 아니라 **넷으로 갈린다.** 지금 SKILL.md 에 있으면 통과, 그 스킬의 과거
커밋에만 있으면 warn(그 뒤에 스킬이 고쳐졌다 — 어느 커밋에 있었는지 함께 적는다), 어느
판에도 없으면 error, 과거를 볼 수 없었으면(git 이 없다 · 이력 상한에 걸렸다) 또 다른
warn 이다. **warn 은 통과가 아니다** — 요약 줄이 「대조 못 한 영수증」을 따로 센다.
지난 런의 영수증이 warn 으로 바뀌었다고 원장을 고쳐 쓰지 않는다. 그것은 영수증이다.
### 3. 프로젝트 검사기를 돌린다
```bash
python3 scripts/verify-tech-log-tree.py <프로젝트>
python3 scripts/verify-project-layout.py <프로젝트>
python3 scripts/audit-records.py <프로젝트>
python3 scripts/check-figure-text.py <프로젝트>
```
## 이 스킬이 하지 않는 것
- **판단을 대신하지 않는다.** 어떤 후보를 `PROMOTE` 할지, 그림이 필요한지, 어떤 종류인지는
각 단계의 스킬이 정한다. 이 스킬은 그 스킬이 실제로 불렸는지만 본다.
- **관문을 완화하지 않는다.** 관문이 error 를 내면 그 단계는 끝나지 않은 것이다. 원장에
`FAILED` 로 적고 멈춘다. 다음 단계로 넘기지 않는다.
- **게시하지 않는다.** S7 은 저장까지다. 게시는 사람이 공개 화면을 보고 판단한다.
## 자주 어긋나는 자리
| 증상 | 원인 | 원장에 남는 모습 |
|---|---|---|
| 문장이 여전히 AI 같다 | S5 를 같은 세션이 겸했다 | `skillEcho` 가 비었거나 SKILL.md 에 없는 문장 |
| 그림 안에 문장이 있다 | S4 가 `check-figure-text.py` 를 안 돌렸다 | 관문 목록에 그 명령이 없다 |
| SSOT 에 없는 인용이 있다 | S3 이 앞 기록에서 코드를 옮겨 적었다 | `check_evidence` 종료 코드 ≠ 0 |
| 기록이 색인에 없다 | S2 를 건너뛰고 S3 을 했다 | S2 가 `SKIPPED` 인데 사유가 없다 |
| Studio 에서 그림이 안 보인다 | S7 이 Asset 을 올리기 전에 본문을 넣었다 | S7 관문에 미리보기 확인이 없다 |
| 스킬은 열었는데 결과가 그 역할 같지 않다 | 일반 에이전트를 띄웠다 | `runBy` 가 계약 이름이 아니다 |
@@ -0,0 +1,267 @@
# 단계 계약
단계마다 다섯을 정한다 — **입력 · 스킬 · 에이전트 · 관문 · 산출물.** 원장에 적히는 것도
이 다섯이다.
**에이전트는 그때그때 만들지 않는다.** `.claude/agents/<이름>.md` 가 그 단계의 「하는 일 ·
안 하는 일 · 관문 · 보고」를 적고 있고, `Agent` 도구의 `subagent_type` 에 그 이름을 준다.
배정의 정본은 `scripts/verify-pipeline-run.py``STAGES` 이고, 원장의 `runBy` 가 그 이름을
담는다 — 검사기가 계약과 대조한 뒤 그 `.md` 가 실재하는지까지 본다.
관문은 종료 코드가 0 이어야 지난 것이다. 0 이 아니면 그 단계는 `FAILED` 이고 다음 단계로
넘어가지 않는다.
---
## S1 — 코드베이스 → SSOT
| | |
|---|---|
| 스킬 | `analyzing-codebase-for-tech-log` |
| 에이전트 | `ssot-analyst``.claude/agents/ssot-analyst.md` |
| 입력 | 분석 대상 저장소 경로(사용자가 준다) · 그 저장소의 `AGENTS.md`(있으면) |
| 산출물 | `docs/<프로젝트>/final/document.md` |
| 관문 | `python3 scripts/verify-project-layout.py <프로젝트>` |
**분석 대상 저장소는 이 저장소 밖이다.** `docs/<프로젝트>` 는 이 저장소 기준이고, 분석 대상은
사용자가 준 절대 경로다. 두 저장소를 섞지 않는다 — 대상 저장소를 고치지 않는다.
**`analysis-queue.yaml` 은 여러 프로젝트를 줄 세울 때만 쓴다.** 사용자가 저장소 하나를
지목했으면 큐 없이 그 하나를 분석한다. 큐가 있으면 큐가 정한 활성 프로젝트를 따른다.
작업 재료(`analysis/` · `notes/` · `checkpoints/` · `state.json` · `source-index.md`)는 분석
중에만 있고, 끝나면 `final/document.md` 로 합치고 지운다.
```bash
python3 scripts/fold-analysis-into-final.py <프로젝트>
```
**S2 가 무엇을 기대하는지 알고 쓴다.** S2 는 `final/document.md` 를 절 단위로 훑어 후보를
찾는다. 절 제목이 무엇을 다루는지 말하지 않으면 후보가 안 잡힌다. 접어 넣은 문서라면
제1부(통합 분석)가 후보 범위이고 제2·3부는 근거다.
산출물이 이미 있고 이 글감의 근거가 그 안에 있으면 `SKIPPED` 로 적고 사유를 남긴다.
### 이미 접어 넣은 프로젝트를 다시 볼 때 — 대조 모드
`final/document.md` 가 이미 있는 프로젝트에 S1 을 다시 돌리는 것은 **분석이 아니라 대조**다.
스킬의 절차(작업 재료를 만들고 → 분석하고 → 접어 넣는다)를 그대로 밟으면 두 곳이 깨진다.
- `docs/<프로젝트>/``state.json`·`analysis/` 를 만들면 배치 검사기가 error 로 센다.
완료된 프로젝트의 폴더는 `final/``tech-log-studio/` 뿐이다.
- `final/document.md` 를 고치면 `ssotSha256` 이 어긋나 분해 계약이 통째로 무효가 된다.
그래서 대조 모드는 이렇게 돈다.
| | 분석 모드 | 대조 모드 |
|---|---|---|
| 작업 재료 | `docs/<프로젝트>/analysis/` | `runs/<프로젝트>/<runId>/stage/S1/` |
| SSOT | 만들거나 접어 넣는다 | **고치지 않는다.** 보강 후보만 적는다 |
| 끝 조건 | fold 하고 재료를 지운다 | 어긋난 것·빠진 것을 목록으로 남긴다 |
SSOT 가 코드와 **어긋나는** 것을 찾으면 그것은 보강 후보가 아니다. 크게 적고 사람에게
올린다 — 이미 그 SSOT 를 근거로 쓴 기록이 있기 때문이다.
---
## S2 — SSOT → 분해 계약
| | |
|---|---|
| 스킬 | `deriving-tech-log-root-tree` |
| 에이전트 | `tree-deriver``.claude/agents/tree-deriver.md` |
| 입력 | `docs/<프로젝트>/final/document.md` **하나** |
| 산출물 | `docs/<프로젝트>/tech-log-studio/tech-log-tree.json` |
| 관문 | `python3 scripts/build-tech-log-tree.py <프로젝트>``python3 scripts/verify-tech-log-tree.py <프로젝트>` (error 0) |
`analysis/**` 를 후보를 찾으려고 열지 않는다. 분석에만 있는 자료를 발견하면 `final/document.md`
를 먼저 보강한다.
**검사기나 계약이 요구하는데 스킬의 절차가 안 적는 칸이 있다.** 손으로 채운다.
| 칸 | 무엇 |
|---|---|
| `candidateScope` | 후보를 찾은 범위. 적지 않으면 모듈 분석 절 제목이 전부 글감이 된다 |
| `sourceRepository` | 분석한 저장소의 경로·리비전·그렇게 판단한 근거. 모르면 `null`, 지어내지 않는다 |
| `ssotSha256` | `build-tech-log-tree.py` 가 채운다. 그래서 build 를 먼저 돌리고 verify 를 돌린다 |
| `ssot-assets` · `ssot-evidence` | SSOT 가 이미 그린 그림과 이미 돌린 측정을 글감에 배정한다. 계약 문서에만 있고 절차에는 이 단계가 없다 |
| `candidateScope.excludedAnchorPattern` | 「범위 밖 글감」 검사가 이 칸으로 판정한다. 없으면 그 검사가 통째로 꺼진다 |
`assetLedger` 는 계약이 요구하지만 스크립트 어느 것도 읽지 않는다. 사람이 보는 칸이다.
**`source` 앵커의 형식이 어디에도 적혀 있지 않다.** keycloak 은 「h2 슬러그 + `-ap1`」 같은
합성 앵커를 쓰고, 제목 슬러그를 쓴 프로젝트도 있다. 검사기는 SSOT 경로를 포함하는지만 보고
실재하는 heading 으로 풀지 않는다. **한 프로젝트 안에서는 한 형식으로 통일한다** — S4 가
`--heading` 값을 이 앵커에서 옮기기 때문이다.
**검사기는 「후보 ↔ 글감」만 본다. 「SSOT ↔ 후보」는 안 본다.** 그래서 SSOT 에 있는 재료를
후보 대장에 올리지도 않고 지나쳐도 error 가 0 이다. 범위의 절을 끝까지 읽는 것은 사람의 일이다.
제외가 0 건인 분해는 선별하지 않은 분해다. 후보마다 처분(`PROMOTE`·`MERGE_INTO`·
`KEEP_IN_SSOT`·`NEEDS_EVIDENCE`·`NEEDS_DECISION`)을 적고, 사람이 다시 읽은 것만
`dispositionReview: CONFIRMED` 로 둔다.
---
## S3 — 글감 → 기록
| | |
|---|---|
| 스킬 | `writing-tech-log-records` |
| 에이전트 | `record-writer``.claude/agents/record-writer.md` |
| 입력 | `tech-log-tree.json` 의 노드 하나 · 그 노드의 `source` 앵커가 가리키는 SSOT 절 |
| 산출물 | `docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md` |
| 관문 | 아래 셋 |
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
**노드가 `PROMOTE` 이고 `CONFIRMED` 인지 먼저 본다.** 아니면 쓰지 않는다.
**인용한 줄은 SSOT 에서 찾아 대조한다.** 앞선 기록에서 옮겨 적은 것은 확인한 것이 아니다 —
그렇게 게시된 기록에 SSOT 와 다른 redirect URI 가 네 곳 있었다.
그림이 필요해 보이면 **`final/assets/` 에 이미 있는지부터 본다.** 계약의 `ssot-assets` 가 이
글감에 배정한 그림이 있으면 그 파일을 그대로 가리킨다. 없을 때만 S4 로 넘긴다.
---
## S4 — 기록 → 그림
| | |
|---|---|
| 스킬 | `technical-visualizer` |
| 에이전트 | `diagram-maker``.claude/agents/diagram-maker.md` |
| 입력 | S3 이 쓴 기록 `.md`(무엇을 그릴지) · 그 기록의 `source` 앵커가 가리키는 SSOT 절(그림의 사실) |
| 산출물 | `final/.techviz/<이름>/{context.json,prompt.md,spec.json}` · `final/assets/<이름>/` |
| 관문 | `techviz lint` · `check-figure-text.py` · `check-figure-overlap.py` · `preview-figure.py` 로 눈 확인 |
**입력이 둘이라는 것이 이 단계의 전부다.**
- **무엇을 그릴지는 기록 본문이 정한다.** 세 관문(자리가 Case·Concept 인가 · 표가 아닌가 ·
옆 문단이 이미 말하지 않았는가)을 지나야 그린다. 그리고 **그림이 주장하는 것을 기록 본문이
말해야 한다.** 본문이 안 적은 단계를 그림만 넣으면 설명 없는 주장이 남는다. 순서는
본문을 먼저 보강하고 그다음 그림을 붙인다.
- **그림의 사실은 SSOT 절이 댄다.** 기록은 SSOT 의 인용이라 줄 번호가 근거가 되지 못한다.
`techviz prepare` 에는 `final/document.md` 를 넣고, 절은 기록의 `source` 앵커로 지목한다.
```bash
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "<기록의 source 앵커가 가리키는 절 제목>" \
-o docs/<프로젝트>/final/.techviz/<이름>/context.json
```
`--line` 은 쓰지 않는다. context 는 관리 블록을 접은 좌표를 쓰므로 파일 줄 번호와 어긋난다.
**그림 안에는 이름만 넣는다.** 문장은 `<desc>` 와 옆 문단에 둔다. lint 는 이것을 못 잡는다 —
`check-figure-text.py` 가 잡는다.
**lint 는 좌표를 안 본다.** 관계가 이어져 있는지만 본다. 그래서 구역 둘이 겹쳐 그려지거나
라벨이 상자에 먹혀도 통과한다. `check-figure-overlap.py` 가 그것을 본다.
```bash
python3 scripts/check-figure-overlap.py <프로젝트>
python3 scripts/check-figure-overlap.py --file 그림.svg
```
**그래도 마지막에는 눈으로 본다.** 검사기가 보는 것은 배경 사각형의 좌표라, 글자가 상자
밖으로 조금 나가거나 화살표가 라벨을 지나는 것은 못 잡는다.
기록에 되적는다 — frontmatter `assets:``key``file`(=`final/assets/<이름>/<이름>.svg`)
을 적고, 본문에는 마크다운 이미지로 넣는다. `:::evidence` 는 저장소에 쓰지 않는다. 사본을
`tech-log-studio/` 쪽에 두지 않는다 — 사본에는 `.techviz/<이름>/` 이 없어 다시 못 만든다.
---
## S5 — AI 티 제거
| | |
|---|---|
| 스킬 | `rewriting-technical-prose-naturally` |
| 에이전트 | `prose-rewriter``.claude/agents/prose-rewriter.md` |
| 입력 | S3·S4 를 지난 기록 `.md` (제자리 수정) |
| 산출물 | 같은 파일 |
| 관문 | `check_prose.mjs` error 0 · `style_profile.mjs` · S3 관문 재실행 |
```bash
S=.agents/skills/rewriting-technical-prose-naturally/scripts
node $S/check_prose.mjs --warn <기록.md> # error 0 까지 고친다
node $S/style_profile.mjs <기록.md>
```
문서 전체를 다시 쓴 것이 아니면 `--doc` 을 빼고 부른다.
**`density.mjs` 는 본문이 있는 종류에만 건다.** 그 기준은 글 한 편(낱말 1277+ · 코드블록 1+ ·
수치 13+)을 잰 값이고, Reference·Question·Decision 은 칸이 평문이라 코드블록을 넣는 것 자체가
규칙 위반이다. 본문 없는 종류에 걸면 구조적으로 통과할 수 없는 관문이 되고, 통과시키려면
없는 측정값을 지어내야 한다.
**문체 수치를 맞추려고 문장을 넣지 않는다.** 검사기는 표면 패턴만 보고 뜻은 못 본다.
**보호 구간을 건드리지 않는다** — 수치·날짜·버전·단위·코드·명령어·URL·직접 인용·공식 명칭은
원문과 한 글자도 달라지면 안 된다. 그래서 문장을 고친 뒤 `check_evidence.mjs` 를 다시 돌린다.
---
## S6 — 일한 사람의 목소리
| | |
|---|---|
| 스킬 | `writing-as-the-person-who-did-it` |
| 에이전트 | `voice-writer``.claude/agents/voice-writer.md` |
| 입력 | S5 를 지난 기록 `.md` · **그리고 그 기록의 상류 자료** (SSOT · 커밋 메시지 · 주석 · `확인하지 못한 것` 칸) |
| 산출물 | 같은 파일 |
| 관문 | `check_voice.mjs` · `check_prose.mjs` 재실행 · S3 관문 재실행 |
```bash
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <기록.md>
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
```
**S5 다음이다.** 번역투와 반복 문형을 걷어낸 뒤라야 채울 자리가 보인다. 그리고 이 스킬이
넣은 문장을 `check_prose` 가 다시 본다 — 그래서 재실행이 관문이다.
**자료에 흔적이 없으면 이 단계는 여기서 끝난다.** 없는 사람을 만들지 않는다. 「처음에는」·
「고민 끝에」·「놀랍게도」를 자료 없이 쓰면 지어낸 것이다. 원장에는 `DONE` 에 「흔적 없음」을
적는다 — `SKIPPED` 가 아니다. 찾아봤다는 것이 이 단계의 일이다.
**`check_voice.mjs` 는 목소리가 모자란지 재지 않는다.** 지어낸 목소리를 잡는다. 조용하다고
목소리가 생긴 것은 아니다.
---
## S7 — Studio 저장
| | |
|---|---|
| 스킬 | `publishing-tech-log-to-studio` |
| 에이전트 | `studio-validator``.claude/agents/studio-validator.md` |
| 입력 | S6 을 지난 기록 `.md` · frontmatter `assets:` 가 가리키는 SVG |
| 산출물 | Studio 작업본. 기록 frontmatter 의 `id`·`studio:` |
| 관문 | 상태 레일이 `저장됨` · `python3 scripts/build-tech-log-tree.py``verify-tech-log-tree.py` |
**저장까지다. 게시하지 않는다.**
Asset 을 본문보다 먼저 올린다. 순서를 뒤집으면 미리보기가 본문 전체를 막는다.
바뀐 것이 없으면 저장 버튼을 누르지 않는다 — 누를 때마다 `version` 이 올라가고 앞서 만든
검증·미리보기 산출물이 무효가 된다.
---
## 관문 요약
| 단계 | 에이전트 | 명령 |
|---|---|---|
| S1 | `ssot-analyst` | `verify-project-layout.py <프로젝트>` |
| S2 | `tree-deriver` | `build-tech-log-tree.py``verify-tech-log-tree.py` (error 0) |
| S3 | `record-writer` | `studio-body.py``check_body.mjs` · `check_prose.mjs` · `check_evidence.mjs --repo` |
| S4 | `diagram-maker` | `techviz lint` · `check-figure-text.py` · `check-figure-overlap.py` · `preview-figure.py` (눈 확인) |
| S5 | `prose-rewriter` | `check_prose.mjs` (error 0) · `style_profile.mjs` · S3 관문 |
| S6 | `voice-writer` | `check_voice.mjs` · `check_prose.mjs` · S3 관문 |
| S7 | `studio-validator` | `저장됨` 확인 · `build-tech-log-tree.py``verify-tech-log-tree.py` |
@@ -0,0 +1,305 @@
# 서브에이전트 프롬프트
단계마다 에이전트를 하나 띄운다. 아래를 그대로 쓰고 `<...>` 만 바꾼다.
**에이전트를 새로 만들지 않는다.** 단계마다 맡을 에이전트가 `.claude/agents/` 에 있고,
`Agent` 도구의 `subagent_type` 에 그 이름을 준다. 절마다 첫 줄에 적혀 있다.
```
Agent(subagent_type="record-writer", prompt=<아래 S3 프롬프트>)
```
에이전트 정의가 그 단계의 「하는 일 · 안 하는 일 · 관문 · 보고」를 이미 적고 있다. 그래서
프롬프트는 **이번 런의 입력 경로와 글감**을 준다 — 아래 것을 그대로 쓰되, 정의와 어긋나는
지시를 프롬프트로 덮어쓰지 않는다.
## 모든 프롬프트에 들어가는 넷
1. **스킬 이름과 「SKILL.md 를 끝까지 먼저 읽어라」.** 요약을 주지 않는다. 요약을 주면
스킬을 안 연다.
2. **자기 단계의 입력 경로만.** 앞 단계가 무엇을 했는지 설명하지 않는다.
3. **관문 명령 원문.** 「검사해라」가 아니라 붙여 넣을 수 있는 명령을 준다.
4. **스킬 영수증** — SKILL.md 에서 한 줄을 **원문 그대로** 인용해 돌려보내게 한다.
`verify-pipeline-run.py` 가 그 문자열이 실제 파일 안에 있는지 대조한다.
## 돌려받는 형식 (모든 단계 공통)
```json
{
"stage": "S3",
"skill": "writing-tech-log-records",
"skillEcho": "<SKILL.md 에서 그대로 옮긴 한 줄>",
"status": "DONE",
"outputs": ["docs/keycloak/tech-log-studio/.../case-x.md"],
"gates": [{"cmd": "node ... check_body.mjs /tmp/studio-body.md", "exit": 0}],
"notes": "<판단한 것과 못 한 것>"
}
```
`skillEcho` 를 지어내지 말라고 프롬프트에 적는다. 파일에 없는 문장이면 검사기가 잡는다.
## 파일을 쓰라고 할 때는 방법을 함께 준다
서브에이전트의 `Write` 는 「보고는 파일이 아니라 글로 돌려라」는 기본 정책에 막힐 수 있다.
S1 처럼 산출물이 `.md` 인 단계는 그래서 한 줄을 더한다.
```
파일을 쓸 때 Write 툴이 막히면 Bash heredoc (`cat > 경로 <<'EOF'`) 을 써라.
```
이것을 안 적으면 에이전트가 산출물을 만들지 못하고 본문에 통째로 붙여 돌려준다.
---
## S1 — 코드베이스 → SSOT
**에이전트:** `ssot-analyst``subagent_type="ssot-analyst"`
```
너는 Tech Log 파이프라인의 1단계를 맡는다.
먼저 .agents/skills/analyzing-codebase-for-tech-log/SKILL.md 를 끝까지 읽어라.
references/ 아래 문서도 그 스킬이 읽으라는 것을 읽어라. 요약본은 주지 않는다.
대상 저장소: <절대 경로>
쓸 곳: docs/<프로젝트>/
analysis-queue.yaml 이 없으면 이 저장소 하나만 분석한다. 큐가 없다는 이유로 멈추지 마라.
대상 저장소를 고치지 마라. 읽기만 한다.
끝나면 분석 재료를 SSOT 로 합쳐라:
python3 scripts/fold-analysis-into-final.py <프로젝트>
합친 뒤 analysis/ · notes/ · checkpoints/ · state.json · source-index.md 를 지운다.
관문:
python3 scripts/verify-project-layout.py <프로젝트>
error 0 이 될 때까지 고쳐라.
돌려줄 것 (JSON):
stage, skill, skillEcho, status, outputs, gates, notes
skillEcho 는 방금 읽은 SKILL.md 에서 네 작업에 해당하는 규칙 한 줄을 원문 그대로 옮긴 것이다.
지어내지 마라 — 파일에 그 문자열이 있는지 기계가 대조한다.
```
---
## S2 — SSOT → 분해 계약
**에이전트:** `tree-deriver``subagent_type="tree-deriver"`
```
너는 Tech Log 파이프라인의 2단계를 맡는다.
먼저 .agents/skills/deriving-tech-log-root-tree/SKILL.md 를 끝까지 읽어라.
references/candidate-disposition.md 와 references/decomposition-checklist.md 도 읽어라.
출력 계약은 .agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md 다.
입력: docs/<프로젝트>/final/document.md — 이것 하나다.
analysis/** 를 후보를 찾으려고 열지 마라.
출력: docs/<프로젝트>/tech-log-studio/tech-log-tree.json
검사기가 error 로 요구하는데 스킬 본문이 안 적는 칸 셋을 손으로 채워라:
candidateScope — 후보를 찾은 범위
sourceRepository — 분석한 저장소의 경로·리비전·판단 근거. 모르면 null 로 두고 지어내지 마라
ssotSha256 — build 가 채운다. 그래서 build 를 먼저 돌린다
제외가 0 건인 분해는 선별하지 않은 분해다. 후보마다 처분을 적고, 다시 읽은 것만
dispositionReview: CONFIRMED 로 둬라.
관문:
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
error 0 까지 고쳐라.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S3 — 글감 → 기록
**에이전트:** `record-writer``subagent_type="record-writer"`
```
너는 Tech Log 파이프라인의 3단계를 맡는다.
먼저 .agents/skills/writing-tech-log-records/SKILL.md 를 끝까지 읽어라.
그 스킬이 가리키는 references/ 중 네 종류에 해당하는 것을 읽어라 —
record-kinds.md · writing-each-kind.md · body-syntax.md · code-tables-diagrams.md ·
explaining.md · ai-tells.md · choosing-a-diagram.md.
**종류가 Setup 이면 본문을 쓰기 전에 `Skill` 도구로 `writing-practitioner-guides` 를 연다.**
명령을 어떤 형태로 쓸지는 그 스킬이 정한다 — 사람이 직접 치는 실습 가이드이지 에이전트가
실행하기 편한 명령이 아니다. **그 스킬을 못 열면 Setup 을 쓰지 말고 그 사실을 돌려줘라.**
안 열고 쓴 Setup 은 검사기를 다 지나면서도 실행이 중간에 끊긴다 — 실제로 그렇게 나갔다.
글감: docs/<프로젝트>/tech-log-studio/tech-log-tree.json 의 <주제> / <종류> / "<제목>"
그 노드가 PROMOTE 이고 dispositionReview 가 CONFIRMED 인지 먼저 확인해라. 아니면 쓰지 마라.
근거: 그 노드의 source 앵커가 가리키는 docs/<프로젝트>/final/document.md 의 절.
인용하는 줄은 SSOT 에서 찾아 대조해라. 기억이나 다른 기록에서 옮겨 적지 마라.
쓸 곳: docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<파일>.md
그림이 필요해 보이면 docs/<프로젝트>/final/assets/ 에 이미 있는지부터 봐라.
없으면 이 단계에서 만들지 말고 notes 에 "그림 필요: <무엇을>" 이라고 적어라. 4단계가 만든다.
관문:
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
error 0 까지 고쳐라.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S4 — 기록 → 그림
**에이전트:** `diagram-maker``subagent_type="diagram-maker"`
```
너는 Tech Log 파이프라인의 4단계를 맡는다.
먼저 두 개를 읽어라:
.agents/skills/technical-visualizer/SKILL.md — 끝까지
.agents/skills/writing-tech-log-records/references/choosing-a-diagram.md
기록: <기록.md>
이 기록을 읽고 무엇을 그릴지 정해라. 세 관문을 지나야 그린다 —
자리가 Case·Concept 인가 / 표로 될 것이 아닌가 / 옆 문단이 이미 말하지 않았는가.
그리고 그림이 주장하는 것을 이 기록 본문이 말하고 있어야 한다. 본문이 안 적은 단계를
그림만으로 넣지 마라.
그림의 근거는 기록이 아니라 기록의 source 앵커가 가리키는 SSOT 절이다:
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "<그 절 제목>" \
-o docs/<프로젝트>/final/.techviz/<이름>/context.json
--line 은 쓰지 마라.
그림 안의 <text> 는 전부 이름이어야 한다. 문장은 <desc> 와 옆 문단에 둬라.
관문:
./scripts/techviz lint docs/<프로젝트>/final/.techviz/<이름>/spec.json \
--context docs/<프로젝트>/final/.techviz/<이름>/context.json
python3 scripts/check-figure-text.py <프로젝트>
python3 scripts/check-figure-overlap.py <프로젝트>
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs
마지막 것은 PNG 로 떠서 라벨이 상자를 덮지 않는지 눈으로 봐라. lint 는 그것을 못 잡는다.
기록에 되적어라 — frontmatter assets: 에 key 와 file, 본문에는 마크다운 이미지.
:::evidence 를 저장소 .md 에 쓰지 마라.
돌려줄 것: 위 JSON 형식. skillEcho 포함. 그리지 않기로 했으면 status 를 SKIPPED 로 하고
어느 관문에 걸렸는지 적어라.
```
---
## S5 — AI 티 제거
**에이전트:** `prose-rewriter``subagent_type="prose-rewriter"`
```
너는 Tech Log 파이프라인의 5단계를 맡는다.
먼저 .agents/skills/rewriting-technical-prose-naturally/SKILL.md 를 끝까지 읽어라.
그 스킬이 "첫 rewrite 전에 읽으라"고 지정한 references 세 개도 읽어라 —
document-skeleton.md · article-shape.md · korean-tech-blog-register.md.
고칠 파일: <기록.md> (제자리에서 고친다)
너의 일은 문체다. 사실을 만들지 마라. 분류가 틀렸거나 근거가 모자란 것은 네 일이 아니다 —
발견하면 고치지 말고 notes 에 적어라.
보호 구간을 건드리지 마라: 수치 · 날짜 · 버전 · 단위 · 코드 · 명령어 · URL · 직접 인용 ·
공식 명칭. 한 글자도 달라지면 안 된다.
관문:
S=.agents/skills/rewriting-technical-prose-naturally/scripts
node $S/check_prose.mjs --warn <기록.md> # error 0 까지
node $S/style_profile.mjs <기록.md>
그리고 문장을 고쳤으니 본문 문법과 인용을 다시 본다:
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
수치를 맞추려고 문장을 넣지 마라. 검사기는 표면 패턴만 본다.
돌려줄 것: 위 JSON 형식. skillEcho 포함.
```
---
## S6 — 일한 사람의 목소리
**에이전트:** `voice-writer``subagent_type="voice-writer"`
```
너는 Tech Log 파이프라인의 6단계를 맡는다.
먼저 .agents/skills/writing-as-the-person-who-did-it/SKILL.md 를 끝까지 읽어라.
references/voice-moves.md 도 읽어라. 그 스킬이 "이것보다 먼저 본다"고 한
../rewriting-technical-prose-naturally/references/article-shape.md 를 먼저 읽어라.
고칠 파일: <기록.md> (제자리에서 고친다)
상류 자료: docs/<프로젝트>/final/document.md · <대상 저장소의 커밋 메시지·주석·README>
찾을 것은 자료에 남아 있는 사람의 흔적이다 — 무엇을 골랐고 무엇과 견주었나,
확인하지 못한 것이 무엇이고 그것이 어느 주장에 걸리나, 처음 생각과 어긋난 자리가 있나.
없는 사람을 만들지 마라. 「처음에는」·「고민 끝에」·「놀랍게도」를 자료 없이 쓰면 지어낸 것이다.
넣은 문장마다 그것이 어느 파일 어느 줄에서 왔는지 댈 수 있어야 한다.
자료에 흔적이 없으면 아무것도 넣지 말고 그렇게 보고해라. 그것도 이 단계를 한 것이다.
관문:
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <기록.md>
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
check_voice.mjs 는 목소리가 모자란지 재지 않는다. 지어낸 목소리를 잡는다.
돌려줄 것: 위 JSON 형식. skillEcho 포함. 흔적이 없었으면 status DONE 에 notes 로 적어라.
```
---
## S7 — Studio 저장
**에이전트:** `studio-validator``subagent_type="studio-validator"`
```
너는 Tech Log 파이프라인의 7단계를 맡는다.
먼저 .agents/skills/publishing-tech-log-to-studio/SKILL.md 를 끝까지 읽어라.
references/studio-form-map.md 와 references/playwright-recipes.md 도 읽어라.
넣을 기록: <기록.md>
도구: Playwright MCP (mcp__playwright__browser_*)
저장까지만 한다. 게시 버튼을 누르지 마라. 한 번 게시한 문서는 취소해도 삭제가 409 로 거절된다.
상태 레일 aside[class*="studio-document-status"] 가 25초 안에 안 뜨면 인증이 안 된 것이다.
로그인 화면에 자격증명을 입력하지 말고 거기서 멈추고 사용자에게 알려라.
frontmatter 의 id 와 studio: 가 이미 있으면 그 주소로 가라. 새로 만들지 마라.
그림이 있으면 Asset 을 본문보다 먼저 올려라. 순서를 뒤집으면 미리보기가 본문을 막는다.
관문:
상태 레일의 글자가 저장됨 으로 바뀌는 것을 확인 (최대 30초)
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
저장됨 이 안 뜨면 버튼을 다시 누르지 말고 레일의 글자를 그대로 보고해라.
돌려줄 것: 위 JSON 형식. skillEcho 포함. gates 에 저장 전후 version 을 적어라.
```
@@ -0,0 +1,108 @@
{
"schemaVersion": 2,
"runId": "<YYYY-MM-DD-HHMM>",
"project": "<프로젝트>",
"record": "<docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md — 이 런이 만드는 기록>",
"startedAt": "<ISO-8601>",
"finishedAt": null,
"stages": [
{
"id": "S1",
"name": "코드베이스 → SSOT",
"skill": "analyzing-codebase-for-tech-log",
"runBy": "ssot-analyst",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S2",
"name": "SSOT → 분해 계약",
"skill": "deriving-tech-log-root-tree",
"runBy": "tree-deriver",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S3",
"name": "글감 → 기록",
"skill": "writing-tech-log-records",
"runBy": "record-writer",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S4",
"name": "기록 → 그림",
"skill": "technical-visualizer",
"runBy": "diagram-maker",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S5",
"name": "AI 티 제거",
"skill": "rewriting-technical-prose-naturally",
"runBy": "prose-rewriter",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S6",
"name": "일한 사람의 목소리",
"skill": "writing-as-the-person-who-did-it",
"runBy": "voice-writer",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
},
{
"id": "S7",
"name": "Studio 저장",
"skill": "publishing-tech-log-to-studio",
"runBy": "studio-validator",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
"notes": ""
}
]
}
+49 -1
View File
@@ -1,6 +1,9 @@
--- ---
name: technical-visualizer name: technical-visualizer
description: Create source-grounded, diagram-only technical visuals from nearby documentation context. Select a logical composition grammar, compile VizSpec 1.1 into SVG and editable formats, and reject disconnected-card output. description: Create source-grounded, diagram-only technical visuals from nearby documentation context. Select a logical composition grammar, compile VizSpec 1.1 into SVG and editable formats, and reject disconnected-card output.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# Technical Visualizer # Technical Visualizer
@@ -192,6 +195,16 @@ Load supporting guidance only as needed:
`techviz build ... --document docs/<프로젝트>/final/document.md` 가 그 블록을 갱신한다. `techviz build ... --document docs/<프로젝트>/final/document.md` 가 그 블록을 갱신한다.
### Tech Log 파이프라인에서 불릴 때
`running-tech-log-pipeline` 의 4단계가 이 스킬이다. 입력이 둘이라는 것만 다르다.
- **무엇을 그릴지는 방금 쓴 기록 본문이 정한다.** 세 관문은
`../writing-tech-log-records/references/choosing-a-diagram.md` 에 있고, 그림이 주장하는
것을 기록 본문이 말하고 있어야 한다.
- **그림의 사실은 SSOT 절이 댄다.** 기록은 SSOT 의 인용이라 줄 번호가 근거가 되지 못한다.
`prepare` 에는 `final/document.md` 를 넣고 절은 기록의 `source` 앵커로 지목한다.
### Tech Log 기록으로 옮길 때 ### Tech Log 기록으로 옮길 때
런의 `document.md` 는 마크다운 이미지로 그림을 싣지만, Studio 기록은 다르다. SVG 를 Studio 에 Asset 으로 런의 `document.md` 는 마크다운 이미지로 그림을 싣지만, Studio 기록은 다르다. SVG 를 Studio 에 Asset 으로
@@ -203,10 +216,45 @@ Load supporting guidance only as needed:
``` ```
`references/code-tables-diagrams.md`(writing-tech-log-records)의 규칙이 함께 적용된다. 특히 `references/code-tables-diagrams.md`(writing-tech-log-records)의 규칙이 함께 적용된다. 특히
**`<text>` 는 이름만 담고 문장은 `<desc>` 와 옆 문단에 둔다.** 이 저장소의 기존 손그림 SVG 는 이 규칙을 **`<text>` 는 이름만 담고 문장은 `<desc>` 와 옆 문단에 둔다.** `render` 뒤에 반드시 돌린다 —
`spec.json``label`·`details`·edge `label` 이 그대로 `<text>` 가 되므로 스펙을 쓸 때부터 이름으로 쓴다.
```bash
python3 scripts/check-figure-text.py <프로젝트> # <text> 가 전부 이름인가
python3 scripts/check-figure-overlap.py <프로젝트> # 상자와 라벨이 서로를 덮지 않는가
```
### 컴파일한 뒤 반드시 눈으로 본다
**lint 는 라벨이 상자를 덮는 것을 못 잡는다.** 엣지가 노드를 지나가는 것(`edge-through-node`)은
보지만 라벨은 앵커 점만 보고 폭을 재지 않는다. 그래서 `PASS` 인 그림에도 라벨이 상자에 먹히거나
경계선 위에 얹히는 일이 생긴다. SVG 를 PNG 로 떠서 본다.
```bash
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs
python3 scripts/preview-figure.py --file 그림.svg
```
지금까지 확인한 것:
| 증상 | 원인 | 대응 |
|---|---|---|
| 엣지 라벨이 옆 상자에 먹힌다 | 라벨이 길다 | 라벨을 짧은 이름으로. 자세한 것은 노드 `details` 로 |
| 라벨이 group 점선 위에 얹힌다 | `two-zone-pipeline` 은 지역 **안쪽** 엣지 라벨을 캔버스 top 에 고정한다 | 지역 안 엣지를 없애거나 `component-flow` + `groups` 로 바꾼다 |
| 원기둥이 제목·항목을 덮는다 | `shape: cylinder``details` 가 많다 | `details` 를 줄이거나 `shape: box` | 이 저장소의 기존 손그림 SVG 는 이 규칙을
어기고 문단과 각주 번호·커밋 해시를 캔버스 안에 넣어 두었다. 다시 만들 때 그 문장들은 본문으로 내린다. 어기고 문단과 각주 번호·커밋 해시를 캔버스 안에 넣어 두었다. 다시 만들 때 그 문장들은 본문으로 내린다.
### 그림을 만들기 전에 ### 그림을 만들기 전에
`rewriting-technical-prose-naturally``## Figures` 를 먼저 읽는다. 옆 문단이 이미 말한 것을 상자와 `rewriting-technical-prose-naturally``## Figures` 를 먼저 읽는다. 옆 문단이 이미 말한 것을 상자와
화살표로 다시 그린 그림은 만들지 않는다. 그림이 담아야 할 것은 순서·구조·측정값·실제 산출물이다. 화살표로 다시 그린 그림은 만들지 않는다. 그림이 담아야 할 것은 순서·구조·측정값·실제 산출물이다.
**표로 되는 것을 그림으로 그리지 않는다.** `comparison` 프로필은 연결성 검사에서 빠지기 때문에
항목을 나란히 늘어놓기만 해도 lint 를 통과한다. 그것이 표다 — 표는 값을 비교하고 그림은
포함·순서·경계처럼 자리로만 보이는 것을 맡는다. 스펙을 쓰기 전에 묻는다.
> **관계선을 다 지워도 뜻이 남는가.** 남으면 표다. 마크다운 표로 쓴다.
`verify-project-layout.py` 의 「표로 되는 그림」이 관계선 없이 항목마다 같은 수의 `details`
늘어놓은 spec 을 센다. `comparison` 이 맞는 자리는 비교 자체가 자리로 드러나는 때다 — 겹치는
범위, 갈라지는 경계처럼.
@@ -1,6 +1,9 @@
--- ---
name: writing-as-the-person-who-did-it name: writing-as-the-person-who-did-it
description: Use when a Korean technical document is accurate, well-ordered and well-evidenced but reads like a report produced by nobody — no one chose anything, nothing surprised anyone, and the limits are an inventory instead of an admission. description: Use when a Korean technical document is accurate, well-ordered and well-evidenced but reads like a report produced by nobody — no one chose anything, nothing surprised anyone, and the limits are an inventory instead of an admission.
metadata:
version: "1.0.0"
language: "ko-KR"
--- ---
# 일한 사람이 쓴 글로 만들기 # 일한 사람이 쓴 글로 만들기
@@ -0,0 +1,580 @@
---
name: writing-practitioner-guides
description: Use when writing hands-on guides, runbooks, troubleshooting docs, lab walkthroughs, or validation procedures that a human will execute themselves in a terminal over SSH. Symptoms that this applies - the reader is expected to type the commands, the doc teaches how to read a system rather than reporting a result, or a draft contains python -c, embedded JSON parsing, one-liners nobody types by hand, or a config file or YAML authored with printf/echo >>/heredoc instead of an editor.
---
# Writing Practitioner Guides
## Overview
**Optimize for human operation, not command compactness.**
A guide is not a script. The reader types each command, reads its raw output,
and decides what to do next. Commands that are efficient for an agent to run
once are often useless for a human learning to read a system.
The failure this prevents: an agent writes `curl … | python3 -c 'import json…'`
because it produces a clean answer in one call. The reader gets the answer and
learns nothing about the tool they will need at 3am.
## The boundary
Three tiers. Pick the lowest one that fits.
```
interactive command → short pipeline → saved script file
```
| Tier | When | Form |
|---|---|---|
| **interactive** | reading state, one question | `kubectl get pods`, `ss -lntp`, `journalctl -u nginx -n 50` |
| **short pipeline** | filtering that a person would actually type | `ps aux \| grep java`, `… \| jq .status`, `for p in a b; do … done` |
| **saved script** | it has become a program | write the file with an editor, then run it |
**Move to a saved script when any of these is true:**
- multiple branches (`if`) or nested loops
- combining several requests and computing across them
- non-trivial JSON transformation
- you cannot tell what it does by reading it once
- it will be run again later
For a saved script, the guide says to open an editor, shows the code as a
**separate file listing** (not a terminal command), then shows the run command.
```bash
vim scripts/check_sessions.py
```
```python
# file: scripts/check_sessions.py
...
```
```bash
python3 scripts/check_sessions.py
```
Terminal command ≠ program source. Never blur them with a heredoc.
## Files the reader has to understand before changing them
The tiers above stop at the saved script. The same split reaches further: **any
file whose content the reader must read and understand to change it is written
in an editor, not assembled by a shell one-liner.** Config files and YAML are
that kind of file.
> **조회·진단·실행은 CLI를 적극 사용하고, 사람이 내용을 이해하면서 작성해야 하는 설정 파일은
> 에디터를 사용한다.**
| Reading or acting on the system → CLI | Authoring a file a human must understand → editor |
|---|---|
| CPU flags → `grep /proc/cpuinfo`, `lscpu` | cloud-init YAML → `nano kc-lab-1.yaml` |
| service state → `systemctl status` | systemd unit → `sudo nano /etc/systemd/system/x.service` |
| VM state → `virsh list --all` | `nginx.conf``sudo nano /etc/nginx/nginx.conf` |
| network → `ip addr`, `virsh net-list --all` | `~/.bashrc``nano ~/.bashrc` |
| logs → `journalctl -u x` | Kubernetes manifest → `nano deploy.yaml` |
| fetch / copy → `curl`, `cp`, `scp` | a script → `nano x.sh``chmod +x x.sh``./x.sh` |
| create a VM → `virt-install` | |
### This rule is not "replace sed with nano"
> 단순히 **「`sed`를 `nano`로 바꿔라」**라고 하면 안 됩니다. 그러면 모든 shell 명령을 기계적으로
> 에디터 작업으로 바꿀 가능성이 큽니다.
The trigger is the *file-authoring step*, not the appearance of a shell tool.
| Kind | Examples | Verdict |
|---|---|---|
| what an operator types by hand | `virsh`, `systemctl`, `ssh`, `curl`, `virt-install` | keep |
| reading / diagnosing | `grep`, `lsmod`, `cat`, `stat`, `groups` | keep |
| shell tricks that author a file | `printf >`, `echo >>`, `cat <<EOF`, `python3 -c`, `ssh '… cat > …'` | rewrite as an editor step |
A pipe is not the problem. A 12-line `virt-install` is not the problem —
creating the VM *is* that command's purpose, so the CLI is the right way to show
it. `sed` is fine for a query or a throwaway substitution; it is wrong as the
default interface for editing config, because what the reader will actually do
during an incident is open the file and read what is in it.
### Rewrite: `~/.bashrc`
```bash
# before
echo 'export LIBVIRT_DEFAULT_URI=qemu:///system' >> ~/.bashrc
virsh uri
```
```bash
# after
nano ~/.bashrc
```
```text
export LIBVIRT_DEFAULT_URI=qemu:///system
```
```bash
source ~/.bashrc
virsh uri
```
Two reasons, and the second is the one that gets forgotten:
1. The reader sees the chain — file → variable → reload this shell → `virsh`
now resolves that URI. `echo >>` produces only the end state.
2. **`echo >>` is not idempotent.** Someone who walks the guide a second time
appends the same line again. Opening the file shows what is already there.
### Rewrite: cloud-init meta-data
```bash
# before
printf 'instance-id: kc-lab-1-%s\nlocal-hostname: kc-lab-1\n' "$(date +%s)" > meta-kc-lab-1
```
```bash
# after
nano meta-kc-lab-1
```
```yaml
instance-id: kc-lab-1-20260912
local-hostname: kc-lab-1
```
> `instance-id`는 이전 cloud-init 실행과 다른 인스턴스로 인식시키기 위해 이전 값과 겹치지 않게
> 지정한다.
The `printf` form makes the reader decode `%s`, `\n`, `$(...)`, `date +%s` and
`>` before reaching the two keys the page is about. A document teaching
cloud-init should not be teaching shell `printf`. The editor form also gives the
one line about `instance-id` a place to sit, right where it is typed.
### Rewrite: a file on a remote host
```bash
# before
ssh donghyeon@192.168.122.11 'umask 077; cat > ~/kc-lab-2.yaml' < kc-lab-2.yaml
ssh donghyeon@192.168.122.11 'cloud-init schema -c ~/kc-lab-2.yaml; rm -f ~/kc-lab-2.yaml'
```
```bash
# after
scp kc-lab-2.yaml donghyeon@192.168.122.11:~/
ssh donghyeon@192.168.122.11
```
then, on the guest:
```bash
chmod 600 ~/kc-lab-2.yaml
cloud-init schema -c ~/kc-lab-2.yaml
rm ~/kc-lab-2.yaml
```
The first line asked the reader to hold SSH, redirection, `umask`, file creation
and local stdin at once. The rewrite uses more commands and puts fewer things in
each: **one action, one command.** In a guide someone is learning from, that
trade is the right way round.
One more rewrite belongs to this rule — replacing a `python3 -c` YAML check with
the format's own validator. It sits in **Command priority** below, where the
ranking it changes lives.
## Command priority
| Rank | Reach for | Examples |
|---|---|---|
| 1 | the system's own CLI | `kubectl`, `systemctl`, `psql`, `redis-cli`, `docker compose` |
| 2 | standard OS tools | `ps`, `ss`, `lsof`, `free`, `top`, `dmesg` |
| 3 | network / protocol tools | `curl`, `dig`, `openssl`, `nc`, `tcpdump` |
| 4 | short Unix combinators | `grep`, `jq`, `awk`, `head`, `tail`, `less`, `watch` |
| — | **avoid** | python/node heredocs, `python -c` that *processes data*, giant awk programs, pipelines built to produce one tidy answer |
**The line is doing-the-work vs checking-a-fact, not the language.**
```bash
# not fine — this is data processing the native tool should do
curl -s "$URL" | python3 -c '
import json,sys
for r in json.load(sys.stdin)["data"]["result"]:
print(r["metric"]["pod"], r["value"][1])'
```
That one iterates, reshapes, and formats. `jq`, or the tool's own
output flag, does that — and when neither is installed, say so and show the
raw output instead of writing a parser.
`grep`, `jq`, `awk`, and a one-line `for` are what practitioners type. Do not
ban them. Ban the ones written for the *agent's* convenience.
### Validate with the format's own checker first
Checking a fact is allowed — but a hand-rolled syntax check is the *last*
resort, not the default. If the domain ships a command that validates this file,
that command is the step. Drop to a one-line syntax check only when nothing
validates the format.
```bash
# before — checks that it parses as YAML, and nothing else
python3 -c 'import yaml,sys; yaml.safe_load(open("kc-lab-1.yaml")); print("YAML OK")'
# after — the domain's own validator: schema, keys, and deprecations too
cloud-init schema -c ~/kc-lab-2.yaml
```
Same rank elsewhere: `nginx -t`, `sshd -t`, `systemd-analyze verify x.service`,
`kubectl apply --dry-run=server -f deploy.yaml`, `terraform validate`,
`docker compose config`. Each one is also the command the reader will reach for
when the service refuses to start, which a `python3 -c` line never becomes.
### Two shapes of the same tool
Most tools have a *reading* form and a *value-extracting* form. Pick by what
the reader does next with the output.
| The reader will… | Form | Example |
|---|---|---|
| **look at the response** and judge | reading form | `curl -I <url>` · `curl -v <url>` |
| **compare or count the value** across runs or hosts | extracting form | `curl -s -o /dev/null -w '%{http_code}\n' <url>` |
The extracting form hides everything except the field you chose. Use it only
when that field *is* the answer — a status code you will compare before and
after an injection, or a number you will repeat 900 times. When the reader is
still figuring out what is wrong, they need the headers and the TLS handshake,
not `200`.
Same split elsewhere: `kubectl get` (read) vs `-o jsonpath=` (extract),
`systemctl status` (read) vs `systemctl show -p X --value` (extract),
`psql` interactive (read) vs `psql -tAc` (extract).
**Show the reading form first at least once per tool.** A reader who has only
ever seen `-w '%{http_code}'` cannot debug a TLS error.
## Progressive narrowing
Never jump to the precise command. Show the widening-to-narrowing path the
reader will actually walk.
```
list / status → detail / describe → logs → targeted inspection
```
```bash
kubectl get pods # what is there
kubectl get pods -o wide # where, and on which node
kubectl describe pod api-7c874-8m2js # why is this one unhappy
kubectl logs api-7c874-8m2js # what did it say
kubectl logs api-7c874-8m2js --previous # what did it say before it died
```
**Observe before filtering.** Show the raw output at least once before piping
it. A reader who has never seen `ss -lntp` output needs to see it whole.
## Mutation ordering
```
observe → diagnose → reproduce → mutate
```
`rm`, `kill`, `delete`, `UPDATE`, restart, config change do not appear in the
diagnosis phase. If a step changes state, say what it changes and how to undo it.
## Shape of one step
Four elements, in this order. A command with no interpretation is not a step.
```
무엇을 확인하는가 one line — the question this answers
$ command the command, short
어디를 봐야 하는가 which field/line in the output matters
이 결과가 의미하는 것 what it tells you, and what to do next
```
Show the real output. If it was measured, quote it verbatim; if it is
illustrative, say so.
### When the step changes state
The four elements above are the shape of a step that **reads**. A step that
**changes state** needs a different one — otherwise the why, the failure
symptoms and the special cases all pile into the same paragraph:
> 정보 밀도는 높은데 **처음 따라 하는 사람의 시선 이동이 어렵습니다.**
```
목적 → 행동(번호 매긴 명령) → 예상 결과 → 왜 필요한가 → 문제가 생기면
```
```text
### 4. libvirt 기본 연결을 system으로 설정한다
목적
virsh가 사용자 세션이 아니라 시스템 libvirt에 연결되도록 한다.
1. 설정 파일을 연다.
$ nano ~/.bashrc
2. 다음 줄을 추가한다.
export LIBVIRT_DEFAULT_URI=qemu:///system
3. 저장한 설정을 현재 셸에 반영한다.
$ source ~/.bashrc
4. 확인한다.
$ virsh uri
예상 결과
qemu:///system
왜 필요한가
qemu:///session과 qemu:///system은 서로 다른 libvirt 연결이다.
VM을 system 쪽에 만들고 virsh가 session 쪽을 보고 있으면
VM을 만들었는데도 목록에서 찾지 못할 수 있다.
문제가 생기면
$ virsh uri
부터 확인한다.
```
**A step that only reads takes the first shape; a step that changes state takes
this one.** Same headings every time, so the reader's eye lands in the same
place on step 11 as on step 1.
## Verify before publishing
Every command in a guide must have been run, or be marked as unverified.
**Check that the tools you reach for are actually installed on the machine
the reader will be on** — `jq` and `yamllint` are absent more often than you
expect, and a guide that assumes them sends the reader to install things
mid-diagnosis.
Two failures this catches, both real:
- `kubectl get endpoints` — deprecated since v1.33, prints a warning
- `kubectl exec keycloak-0 -- curl …` — the image has no curl, exit 127
A guide that teaches a stale or failing command makes the reader doubt their
own environment.
## No placeholders
`<token>` puts the value outside the document. Give the command that produces
it. For secrets, confirm existence or length — never print the value.
```bash
TOKEN=$(ssh node1 'sudo cat /var/lib/rancher/k3s/server/node-token')
echo "${#TOKEN} chars"
```
One exception: a value an earlier step already printed on screen, which the
reader carries into a later step in a different shell — a session id, a realm
UUID. The producing command is already in the document, so the value is not
outside it. Write that slot as `{{NAME}}` (uppercase, digits, underscore), and
say in the prose above the block which step printed it.
Not `${NAME}` — guides use `$TOKEN`, `$SID` and friends as real shell
variables, and a reader who reads a placeholder as one will paste it unchanged.
`{{ }}` is not shell syntax, so pasting it fails where the reader can see it.
## The reader cannot see what is not on screen
Everything below came out of reading 35 finished guides in one pass. Each rule
names the accident that produced it. None of them is a style preference — in
every case the guide passed every other rule in this file and still handed the
reader a wrong answer that looked right.
### A value copied into a container is a snapshot, not a reference
`kubectl run --env="K0=$K0"`, `docker run -e`, cloud-init user-data: each bakes
the string as it was at that moment. From then on two spellings mean different
things, and they differ by two characters:
```bash
exec pod -- sh -c '...$K0...' # the value baked into the pod
exec pod -- sh -c '...'"$K0"'...' # the value in the shell typing this
```
**They agree until something restarts.** In one guide a `rollout restart` changed
the pod IP, and a later step still read the baked copy; the request went to an
address that no longer existed, and the failure it produced was a `500` — which
was also that step's expected result. The screen looked correct.
So: when a step replaces the thing a baked value points at — a pod IP, a node
address, a lease — say in that step which spelling the next commands use.
Prefer recreating the container. If state inside it forbids that, show the
live-read form and add one line saying why it differs from the block above.
**Distance is not the test.** A variable defined 700 lines up can be safe, and
one defined nine lines up can be stale. Ask what happened in between.
### A step that only works inside a window says when, not just what
The four-part shape above — what / command / where to look / what it means —
has no *when*. Several steps read correctly only during a restart, only while a
rule is installed, only within sixty seconds. Read late, they do not error;
they print a different, plausible number.
Name the command that opens the window and the one that closes it, and say
**what the reader sees if they type it after it closed** — that is what most
readers will actually get.
### A check must be able to fail
For every line that says "이렇게 나오면 통과", state what would print the same
thing while the condition is false. Three real cases:
- `virsh list --all` used to prove a group membership took effect — it connects
to the per-user URI, which works with no group at all
- a `postmaster.pid` path checked on a fixed node, when the volume may be on the
other one — "no such file" passes either way
- `source ~/.bashrc` on a host whose login shell is zsh — true in that shell,
gone at next login, and the symptom surfaces one guide later
If the check passes when the step was skipped — because it reads a different
scope, host or shell than the step wrote to — it is not a check. Put the scope
in the command, or write one line naming the failure this check cannot see.
### Never ship a command the guide knows is wrong
Three guides printed a command and told the reader, in prose underneath, to
edit it before typing. Prose is not a guard. The worst of them was a **valid**
assignment:
```bash
OLDID=980ee9b7-... # ← copy from the output above
```
Paste it and `OLDID` holds the literal string, and the `delete` two lines later
runs. Every other hardcoded value in those guides failed loudly — `(0 rows)`,
`(nil)`. This one succeeded at the wrong thing.
If prose under a block says "replace X first", the block already says it — or
says `{{X}}`, which is not shell syntax and fails where the reader can see it.
### One block, one machine
A code block is the reader's copy unit. When the machine changes, the block
ends, even when the commands form one logical step. Splitting the explanation
in prose does not help: they copy the block.
And a machine label tells the reader *where* a block runs — it does not put
them there. When consecutive blocks carry different labels, the transition is
its own step with its own command (`ssh host`, `exit`, "open a second
terminal"). Count them per guide: entries and exits must balance. A
verification section that runs somewhere else needs both.
### A shell function defined mid-guide is worse than all three tiers
It leaves no file, so the reader cannot re-read it. It dies with the shell, so
a new window silently breaks every later step. `unset` does not reach it.
One guide defined `R()` on line 188 and used it through line 617 — a one-letter
name for `kubectl -n … exec deploy/redis -- redis-cli`. At the point of use
the reader cannot see what command they are running.
If something is repeated often enough to want a name, make it a script file
with that name. If it must be a function, define it in the same block as its
first use, name it for what it does (`redis`, not `R`), and restate the
definition at the top of any later section more than a screen away.
### Names the reader must already have
A host alias, a directory, a volume, a file that no earlier step created is not
a placeholder — it is an assumption, and the placeholder rule above does not
catch it. Four guides in one set ran entirely on `ssh kc-lab-1` with no stanza
anywhere that creates it; the second guide hits it on its first command.
Show the command that creates it, or name the guide that owns it, in the prose
above the first block that uses it.
### Walk the guide twice
`echo >>` is one case of a larger rule. Anything created under a fixed name — a
pod, a volume, a DHCP reservation, a namespace — says what the second run
prints and how to clear it first. `--rm` only removes on clean exit; Ctrl+C
leaves the pod and the next run dies on `AlreadyExists`.
And anything that destroys and recreates a host says what identity changes with
it — SSH host keys, MAC-bound leases, certificates — and what that breaks the
next time the reader connects.
### The output block belongs to the command above it
If it came from a wider `grep`, an extra `uniq -c`, or a `sed` that rewrote
names, show that command too or say so on the line before. Readers compare
their screen to yours character by character; a silent edit makes them hunt for
a fault that is not there.
### Context selectors are read as a set
`-n`, `--context`, `-h`, `-U`. One guide's `kubectl exec` was missing
`-n keycloak-lab` while the eight commands around it had it. On its own the
line looks fine; next to its neighbours it is obviously running somewhere else.
Read them as a column, not line by line.
### When the same action has several forms, one of them is the step
One guide gave three ways to kill a backend and marked none of them. Show the
step; put the rest under a heading that says they are alternatives. Leaving the
reader to choose asks them to weigh a trade-off the guide has not explained yet.
### A fixed output path makes a block single-use
"Change the variable and run it again" is incomplete when the block writes to a
fixed file: the second run overwrites the first run's baseline, silently, and
the comparison two sections later has nothing to compare against. Name the
output path among the things to change.
### Run the remedy you prescribe
A guide diagnosed lexical sorting and prescribed `sort -g`. Every line in that
file began with the same `200`, so `sort -g` compared equal and fell through to
byte order — its output was identical to plain `sort`. The fix the guide
offered did not fix anything, and the sentence "miss this and you misread the
maximum" stayed true after following it.
## Rationalization table
| Excuse | Reality |
|---|---|
| "The pipeline gives a clean answer" | The reader needs to read the raw output, not your summary of it |
| "python -c is shorter than explaining" | It is shorter for you. The reader learns nothing and cannot adapt it |
| "jq/awk are also programming" | They are what practitioners type. The line is *program vs command*, not *language* |
| "I'll show the efficient way" | Efficient for one run. This doc is for someone learning to read the system |
| "The reader can copy-paste it" | Copy-paste is not the goal. Knowing where to look is |
| "I verified the logic mentally" | Run it. Two commands in a recent guide were wrong and both looked right |
| "It's obvious what this output means" | Then write the one line. If it is obvious it costs nothing |
| "`-w '%{http_code}'` is precise" | Precise about one field. The reader debugging TLS needs `-v`, not `200` |
| "It's fewer commands" | Fewer for you to type. The reader cannot tell which action they are in the middle of |
| "The file ends up the same either way" | Only on the first run. `echo >>` appends again every time someone repeats the guide |
| "So I should use nano for everything" | No. The trigger is authoring a file the reader must understand. `grep`, `virsh`, a long `virt-install` stay as they are |
| "`python3 -c` only checks syntax" | Then it misses the schema. If the format has a validator — `cloud-init schema`, `nginx -t`, `--dry-run` — that is the step |
| "The prose right under it says to change that value" | The reader copies the block. Prose is not a guard — put `{{NAME}}` in the block |
| "The variable is defined earlier in the same shell" | Ask what restarted in between. A pod IP baked at line 877 was stale by line 764 |
| "The label says which machine it runs on" | A label says where, not how to get there. The transition is its own step |
| "It's the same command, just shorter" | A one-letter function hides the command at the exact moment the reader needs to read it |
| "The check passed" | Ask what it would print if the step had been skipped. Three checks in one set passed either way |
## Red flags — stop and rewrite
- `python3 -c` or a `<<'PY'` heredoc inside a guide
- JSON parsed with a language runtime instead of `jq` or the tool's own `-o`
- a pipeline whose purpose you cannot state in one clause
- a command with no "what to look at" line under it
- `delete`/`kill`/`restart` before any observation step
- `<placeholder>` with no command that produces it
- output shown that you never actually ran
- only the extracting form of a tool appears, never the reading form
- a config file or YAML built with `printf >`, `echo >>`, or `cat <<EOF`
- one line stacking connect + redirect + file creation (`ssh host 'cat > f' < f`)
- `python3 -c` checking syntax when the format has its own validator
- a step that appends, so walking the guide twice appends the line twice
- prose under a block telling the reader to edit the command before typing it
- a block whose machine label differs from the one above, with no command between
- a one- or two-letter shell function, or any function defined far from its use
- a host alias, directory or volume that no step in any guide creates
- `--rm` with no line saying what a Ctrl+C leaves behind
- a "통과" line that would print the same thing if the step had been skipped
- a step that reads correctly only during a window, with no word about the window
- a remedy you have not run against the data that made you prescribe it
## References
Per-technology command vocabulary — what practitioners reach for first, not
an encyclopedia. Load only the one you need.
- [kubernetes.md](references/kubernetes.md)
- [linux-systemd.md](references/linux-systemd.md)
- [networking-tls.md](references/networking-tls.md)
- [datastores.md](references/datastores.md)
@@ -0,0 +1,52 @@
# 데이터 저장소 — what to reach for first
## PostgreSQL
```bash
psql -U <user> -d <db>
```
```
\conninfo 지금 어디에 붙어 있나
\dt 테이블 목록
\d <table> 구조 · 인덱스 · 제약
\du 롤
\l 데이터베이스 목록
\x 세로 출력 토글 (넓은 행을 볼 때)
```
```sql
SHOW <setting>; -- 전역값
SELECT * FROM pg_stat_activity; -- 지금 도는 쿼리
EXPLAIN <query>; -- 계획만
EXPLAIN (ANALYZE, BUFFERS) <query>; -- 실제 실행. 변경 쿼리에 쓰면 실제로 바뀐다
```
한 줄로 값만 뽑을 때.
```bash
kubectl exec deploy/postgres -- psql -U <user> -d <db> -tAc 'select count(*) from <t>'
```
문장 로깅 — 애플리케이션을 고치지 않고 「무엇이 DB 를 어떻게 쓰는지」 본다.
```sql
ALTER SYSTEM SET log_statement = 'all';
SELECT pg_reload_conf();
-- 끝나면 되돌린다
ALTER SYSTEM RESET log_statement;
```
## Redis
```bash
redis-cli ping
redis-cli info server | head
redis-cli dbsize
redis-cli --scan --pattern '<prefix>*' # KEYS 대신. 블로킹하지 않는다
redis-cli type <key>
redis-cli ttl <key>
redis-cli --no-raw get <key> # 바이너리를 이스케이프해 보여 준다
redis-cli config get appendonly
```
`\xac\xed` 로 시작하면 Java 네이티브 직렬화라 사람이 읽을 수 없다.
`/data` 가 볼륨이 아니면 영속화 설정은 컨테이너와 함께 사라진다.
```bash
kubectl get pod -l app=redis -o jsonpath='{.items[0].spec.volumes}'
```
@@ -0,0 +1,72 @@
# Kubernetes — what to reach for first
Order matters. Widen first, then narrow.
## 무엇이 있나
```bash
kubectl get pods # 이름 · 상태 · 재시작 횟수
kubectl get pods -o wide # + 노드 · 파드 IP
kubectl get all # 워크로드 계열만. Secret·PVC·Ingress 는 안 나온다
kubectl get secret,configmap,pvc,ingress
```
## 왜 이 파드가 이런가
```bash
kubectl describe pod <pod> # 이벤트가 여기 붙는다 — 로그보다 먼저 본다
kubectl logs <pod>
kubectl logs <pod> --previous # CrashLoop 이면 죽은 이유는 여기 있다
kubectl logs <pod> -c <container> # 컨테이너가 여럿일 때
kubectl events --for pod/<pod>
kubectl get events --sort-by=.lastTimestamp | tail -20
```
## 사슬을 따라간다
```bash
kubectl get deploy,rs,pod -l app=<label>
```
Deployment 는 파드를 직접 만들지 않는다. ReplicaSet 을 만들고 그것이 파드를
만든다. RS 가 여러 개 남아 있는 것은 정상이며(배포 이력) 활성인 것만 0 이 아니다.
StatefulSet 은 RS 를 쓰지 않고 파드를 직접 만든다 — 이름이 고정이라
`Terminating` 이 안 풀리면 대체 파드가 생기지 않는다.
## Service 가 파드를 잡고 있나
```bash
kubectl describe svc <svc> | grep -i endpoints
kubectl get endpointslice -l kubernetes.io/service-name=<svc>
```
`kubectl get endpoints` 는 v1.33+ 에서 deprecated 다.
비어 있으면 셀렉터와 라벨이 안 맞거나 readiness 미통과다.
```bash
kubectl get svc <svc> -o jsonpath='{.spec.selector}'
kubectl get pods --show-labels
```
## Secret 이 실제로 들어갔나 — 값은 찍지 않는다
```bash
kubectl get secret <s> -o jsonpath='{.data}' | tr ',' '\n' | grep -o '"[A-Z_]*"' # 키 이름만
kubectl get secret <s> -o jsonpath='{.data.<KEY>}' | base64 -d | wc -c # 길이만
kubectl exec <pod> -- sh -c 'echo ${#MY_ENV}' # 파드 안 주입 확인
```
## 적용과 대기
```bash
kubectl apply -f <file>
kubectl rollout status deploy/<name> --timeout=180s # 끝날 때까지 블록한다
kubectl rollout undo deploy/<name>
```
## 안에서 볼 때
```bash
kubectl exec -it <pod> -- sh
kubectl port-forward svc/<svc> 8080:80
kubectl debug -it <pod> --image=busybox --target=<container> # 최소 이미지에 도구가 없을 때
```
**최소 이미지에는 `curl` 도 `wget` 도 없다.** Keycloak 공식 이미지가 그렇다
(`exit 127`). 밖에서 물어보거나 임시 파드를 띄운다.
```bash
kubectl run tmp --rm -it --restart=Never --image=curlimages/curl:8.11.1 -- \
curl -s http://<podIP>:9000/metrics
```
@@ -0,0 +1,51 @@
# Linux · systemd — what to reach for first
## 서비스 상태
```bash
systemctl status <unit> # 상태 · Main PID · CGroup · 최근 로그
systemctl is-active <unit> # 한 단어. 스크립트용
systemctl cat <unit> # 유닛 파일에 적힌 것
systemctl show <unit> # 기본값까지 합쳐 실제 적용되는 것
```
`cat``show` 는 다르다. `Restart=on-failure` 만 적혀 있어도 `show`
`RestartUSec`·`StartLimitBurst` 같은 기본값을 함께 보여 준다.
## 로그
```bash
journalctl -u <unit> -n 50 # 최근 50줄
journalctl -u <unit> -e # 끝으로 (페이저)
journalctl -u <unit> -f # 실시간
journalctl -u <unit> -p err # 에러만
journalctl -u <unit> --since '1 hour ago'
journalctl -u <unit> -o json # 메타데이터까지
```
긴 줄이 접히면 `less -S` 로 좌우 스크롤한다.
**nginx 에러 로그는 2048바이트에서 잘린다**(`NGX_MAX_ERROR_STR`). 저널
포맷을 바꿔도 안 늘어난다 — 기록 자체가 잘렸기 때문이다. access 로그에는
제한이 없으므로 그쪽을 본다.
## 프로세스 · 포트 · 자원
```bash
ps aux | grep <name>
ps -eo pid,ppid,etimes,lstart,args | grep <name> # 얼마나 오래 떠 있나
ss -lntp # 듣고 있는 TCP 포트 + 프로세스
lsof -i :8080
free -m
top / htop
```
## cgroup
```bash
systemd-cgls /system.slice/<unit>.service
cat /sys/fs/cgroup/system.slice/<unit>.service/memory.current
cat /sys/fs/cgroup/system.slice/<unit>.service/pids.current
```
`systemctl status``Memory:` `Tasks:` `CPU:` 가 여기서 읽은 값이다.
## nginx
```bash
nginx -t && systemctl reload nginx # -t 를 통과할 때만 reload
ps -eo pid,lstart,args | grep 'nginx: worker' # reload 판정은 워커 PID 로
```
reload 하면 마스터는 유지되고 워커만 새로 뜬다. 로그 문구가 아니라 이걸 본다.
@@ -0,0 +1,61 @@
# 네트워크 · TLS — what to reach for first
## HTTP
```bash
curl -I <url> # 한 번 볼 때
curl -v <url> # 헤더 · TLS 협상까지
curl -s -o /dev/null -w '%{http_code}\n' <url> # 여러 번 재서 비교할 때만
```
`-w` 형태는 측정용이다. 눈으로 한 번 볼 때는 `-I``-v` 로 충분하다.
## 이름 해석
```bash
dig +short <name>
getent hosts <name> # /etc/hosts 와 NSS 순서까지 반영된 결과
```
## TLS
```bash
echo | openssl s_client -connect <host>:443 -servername <host> 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates -ext subjectAltName
echo | openssl s_client -connect <host>:443 -servername <host> 2>/dev/null \
| grep -E '^ *[0-9]+ s:|^ *i:|Verify return code'
```
체인 단계가 1개면 `cert.pem` 을 쓴 것이다. `fullchain.pem` 이어야 한다.
발급 시각의 외부 기준이 필요하면 SCT 를 본다.
```bash
| openssl x509 -noout -ext ct_precert_scts
```
## 연결 추적
```bash
sudo conntrack -L | grep <port>
sudo conntrack -C
cat /proc/sys/net/netfilter/nf_conntrack_tcp_timeout_established # 보통 86400
```
`ESTABLISHED` 는 대부분의 방화벽 규칙 평가를 건너뛴다. 규칙을 넣었는데
아무 일도 없으면 여기를 먼저 본다.
## 방화벽 규칙
```bash
sudo iptables -S FORWARD # 순서가 중요하다 — 내 규칙이 몇 번째인가
sudo iptables -L FORWARD -v -n # 카운터가 0 이면 도달하지 않았다
sudo iptables -t raw -S PREROUTING # conntrack 보다 먼저 잡는 자리
```
## 패킷
```bash
sudo tcpdump -i <iface> -n port <p> -c 20
```
오버레이 네트워크(flannel VXLAN 등)에서는 물리 인터페이스에 안쪽 IP 가 안
보인다. `flannel.1` 같은 터널 인터페이스에서 잡는다.
## 시계
```bash
timedatectl show -p NTP -p NTPSynchronized
A=$(date -u +%s.%N); B=$(ssh <host> 'date -u +%s.%N'); C=$(date -u +%s.%N)
curl -sI https://www.google.com | grep -i '^date:' # 어느 쪽이 맞는지 외부 기준
```
두 기계의 로그를 나란히 놓기 전에 확인한다.
@@ -1,14 +1,14 @@
# writing-tech-log-records # writing-tech-log-records
Tech Log Studio에 올릴 기록을 쓰는 스킬. Case · Concept · Reference · Question · Decision 다섯 종류의 Tech Log Studio에 올릴 기록을 쓰는 스킬. Case · Concept · Setup · Reference · Question · Decision
종류 선택, 칸 채우기, Case 본문 작성, 게시 전 대조를 다룬다. 여섯 종류의 종류 선택, 칸 채우기, 본문 작성, 게시 전 대조를 다룬다.
## 파일 ## 파일
| 파일 | 무엇 | | 파일 | 무엇 |
|---|---| |---|---|
| `SKILL.md` | 진입점. 종류 선택과 절차 | | `SKILL.md` | 진입점. 종류 선택과 절차 |
| `references/record-kinds.md` | 섯 종류의 칸·상한·게시 조건 | | `references/record-kinds.md` | 섯 종류의 칸·상한·게시 조건 |
| `references/from-ssot-to-records.md` | 긴 글에서 글감을 뽑는 기준과 `tech-log-tree.json` | | `references/from-ssot-to-records.md` | 긴 글에서 글감을 뽑는 기준과 `tech-log-tree.json` |
| `references/writing-each-kind.md` | 종류마다 무엇을 어떤 순서로 쓰나 | | `references/writing-each-kind.md` | 종류마다 무엇을 어떤 순서로 쓰나 |
| `templates/*.md` | 종류별 빈 틀. 복사해서 채운다 | | `templates/*.md` | 종류별 빈 틀. 복사해서 채운다 |
@@ -56,5 +56,5 @@ FAIL 초안.md:3:1 unsupported block syntax: html
## 알아둘 제약 ## 알아둘 제약
코드·표·다이어그램·이미지는 **본문에만** 들어간다. 본문이 있는 종류는 CaseConcept 이다. Reference·Question·Decision의 모든 코드·표·다이어그램·이미지는 **본문에만** 들어간다. 본문이 있는 종류는 Case·Concept·Setup 셋이고,
칸은 평문으로 렌더링된다. 설계상 그렇다 — 본문을 가진 종류는 Case뿐이다. Reference·Question·Decision의 모든 칸은 평문으로 렌더링된다.
@@ -1,6 +1,6 @@
--- ---
name: writing-tech-log-records name: writing-tech-log-records
description: Use when writing or revising a Tech Log Studio record — Case, Concept, Reference, Question, or Decision — including choosing the right kind, filling each kind's fields, authoring body Markdown with code blocks, tables, callouts, diagrams and evidence images, and linking records so a published document renders correctly on the public site. description: Use when writing or revising a Tech Log Studio record — Case, Concept, Setup, Reference, Question, or Decision — including choosing the right kind, filling each kind's fields, authoring body Markdown with code blocks, tables, callouts, diagrams and evidence images, and linking records so a published document renders correctly on the public site.
metadata: metadata:
version: "1.1.0" version: "1.1.0"
language: "ko-KR" language: "ko-KR"
@@ -12,21 +12,26 @@ metadata:
## 개요 ## 개요
Studio는 섯 종류를 구분한다. 종류를 잘못 고르면 채울 칸이 맞지 않는다. Studio는 섯 종류를 구분한다. 종류를 잘못 고르면 채울 칸이 맞지 않는다.
| 종류 | 쓰는 때 | 본문 | | 종류 | 쓰는 때 | 본문 |
|---|---|---| |---|---|---|
| **Case** | 내가 재현하고 검증해 결론을 냈다 | 있음 | | **Case** | 내가 재현하고 검증해 결론을 냈다 | 있음 |
| **Concept** | 남의 것이 어떻게 동작하는지 읽고 정리했다 | 있음 | | **Concept** | 남의 것이 어떻게 동작하는지 읽고 정리했다 | 있음 |
| **Setup** | 남이 자기 손으로 따라 할 절차를 남긴다 (`SETUP`, 화면 이름 「환경 구성」) | 있음 |
| **Reference** | 반복 적용할 기준을 굳혔다 | 없음 | | **Reference** | 반복 적용할 기준을 굳혔다 | 없음 |
| **Question** | 아직 판단이 안 끝났다 | 없음 | | **Question** | 아직 판단이 안 끝났다 | 없음 |
| **Decision** | 프로젝트가 방향을 정했다 (`PROJECT_DECISION`) | 없음 | | **Decision** | 프로젝트가 방향을 정했다 (`PROJECT_DECISION`) | 없음 |
**Setup 만 끝난 일을 적지 않는다.** 나머지 다섯은 이미 일어난 일을 적고, Setup 은 읽는 사람이
자기 기계에서 실행할 순서를 적는다. 그래서 본문에 명령이 들어가고, 버전만 본문 밖의
`pinnedVersions` 에 남는다. 프로젝트가 필수이고 주제는 비워도 된다.
## 절대 규칙 ## 절대 규칙
**코드·표·다이어그램·이미지는 본문에만 들어간다. 본문이 있는 종류는 CaseConcept 둘뿐이다.** **코드·표·다이어그램·이미지는 본문에만 들어간다. 본문이 있는 종류는 Case·Concept·Setup 셋이다.**
본문이 없는 세 종류의 칸은 평문으로 렌더링돼 백틱과 파이프가 글자 그대로 보인다. 그런 자료는 Case 나 본문이 없는 세 종류(Reference·Question·Decision)의 칸은 평문으로 렌더링돼 백틱과 파이프가 글자
Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md` 그대로 보인다. 그런 자료는 본문이 있는 종류에 담고 `관계`로 가리킨다. `references/record-kinds.md`
## 필수 절차 ## 필수 절차
@@ -35,21 +40,37 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
**`tech-log-tree.json` 에 노드가 없는 글은 쓰지 않는다** — 트리에 먼저 올리고, 그 노드의 **`tech-log-tree.json` 에 노드가 없는 글은 쓰지 않는다** — 트리에 먼저 올리고, 그 노드의
후보가 `PROMOTE` 이면서 `dispositionReview: CONFIRMED` 인지 확인한 뒤에 쓴다. `PENDING` 후보가 `PROMOTE` 이면서 `dispositionReview: CONFIRMED` 인지 확인한 뒤에 쓴다. `PENDING`
사람이 다시 읽지 않았다는 뜻이라, 그 위에 쓴 글은 과분류를 그대로 물려받는다. 계약 밖에서 쓴 사람이 다시 읽지 않았다는 뜻이라, 그 위에 쓴 글은 과분류를 그대로 물려받는다. 계약 밖에서 쓴
기록은 색인에 `unlisted` 로 남는다. 기록은 색인에 `unlisted` 로 남는다. 그 노드의 `ssot-assets`·`ssot-evidence` 도 함께 본다 —
1. **종류 선택** — 위 표. 애매하면 "재현했나"를 묻는다. SSOT 가 이미 그린 그림과 이미 돌린 측정 가운데 이 글감에 배정된 것이 거기 적혀 있다.
1. **종류 선택** — 위 표. 애매하면 「끝난 일을 적나, 남이 따라 할 절차를 적나」를 먼저 묻고,
끝난 일이면 "재현했나"를 묻는다.
2. **칸 채우기** — 칸과 게시 조건은 `references/record-kinds.md`. 2. **칸 채우기** — 칸과 게시 조건은 `references/record-kinds.md`.
3. **본문 작성**(Case·Concept) — **종류마다 무엇을 어떤 순서로 쓰는지는 3. **본문 작성**(Case·Concept·Setup) — **종류마다 무엇을 어떤 순서로 쓰는지는
`references/writing-each-kind.md`.** 문법은 `references/body-syntax.md`, 표·코드·그림은 `references/writing-each-kind.md`.**
**Setup 은 본문을 쓰기 전에 `Skill` 도구로 `writing-practitioner-guides` 를 연다.** 명령을
어떤 형태로 쓸지는 그 스킬이 정한다. 그다음이 `references/writing-each-kind.md` 의 Setup 절이다 —
단계 하나의 모양과 이 저장소에서만 걸리는 셋이 거기 있다.
문법은 `references/body-syntax.md`, 표·코드·그림은
`references/code-tables-diagrams.md`. **문장은 `references/explaining.md`.** `references/code-tables-diagrams.md`. **문장은 `references/explaining.md`.**
**문서군 전체의 리듬은 `references/ai-tells.md`.** 이 둘은 첫 초안부터 적용한다 — AI 티를 **문서군 전체의 리듬은 `references/ai-tells.md`.** 이 둘은 첫 초안부터 적용한다 — AI 티를
남겨 두고 나중에 걷어내는 순서가 아니다. **문체를 손보기 전에 문장을 고른다** — 설명이 끝난 남겨 두고 나중에 걷어내는 순서가 아니다. **문체를 손보기 전에 문장을 고른다** — 설명이 끝난
뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 먼저 뺀다(`ai-tells.md` 첫 절). 문체 규칙의 뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 먼저 뺀다(`ai-tells.md` 첫 절). 문체 규칙의
정본은 `ai-tells.md` 다. 정본은 `ai-tells.md` 다.
순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 그때 **그림이 필요한지, 필요하면 무엇을 그릴지는 `references/choosing-a-diagram.md`.** 담을 곳
`technical-visualizer` 로 그림을 만든다. 손으로 SVG 를 그리지 않고, 모든 글에 그림을 만들지도 (Case·Concept·Setup 만) · 표가 아닌지 · 옆 문단이 이미 말하지 않았는지 세 관문을 지나야 그린다.
않는다. 순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 **`final/assets/` 에 그
그림이 이미 있는지부터 본다.** SSOT 를 만들 때 그려 둔 것이 있고 계약의 `ssot-assets` 가 이
글감에 배정해 두었으면 기록의 `assets`**그 파일을 그대로** 가리킨다. 사본을 따로 만들지
않는다 — 사본에는 `.techviz/<이름>/` 이 없어 다시 만들 수 없다. **없을 때만**
`technical-visualizer` 로 새로 만든다. 손으로
SVG 를 그리지 않고, 모든 글에 그림을 만들지도 않는다.
인용할 측정도 같다. `final/evidence/` 에 있는 원문을 가리키고 같은 것을 다시 돌리지 않는다.
**그림과 측정을 그 자리에서 만들어 내기 전에 SSOT 가 이미 가진 것을 먼저 찾는다** — keycloak
에서는 그러지 않아 정본까지 갖춘 그림 13 장이 남고 이름이 다른 그림 5 장이 새로 만들어졌다.
4. **검사** — 셋 다 돌린다. 파서와 문장과 증빙은 각각 다른 것을 본다. 4. **검사** — 셋 다 돌린다. 파서와 문장과 증빙은 각각 다른 것을 본다.
- `scripts/check_body.mjs` Case 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다. - `scripts/check_body.mjs` — 본문이 Studio 파서를 통과하는지. 같은 파서를 그대로 부른다.
저장소의 그림은 마크다운 이미지이므로 `python3 scripts/studio-body.py <기록> -o /tmp/x.md`
로 바꾼 파일에 돌린다. 저장소 파일에 그대로 돌리면 `unsafe image URL` 로 실패한다.
- `rewriting-technical-prose-naturally/scripts/check_prose.mjs` — 문장 규범. **error 0 이 될 때까지 고친다.** - `rewriting-technical-prose-naturally/scripts/check_prose.mjs` — 문장 규범. **error 0 이 될 때까지 고친다.**
칸 하나나 한 절만 고쳤으면 `--doc` 없이 부른다. 이어서 `style_profile.mjs` 로 문체 수치를 본다. 칸 하나나 한 절만 고쳤으면 `--doc` 없이 부른다. 이어서 `style_profile.mjs` 로 문체 수치를 본다.
- `scripts/check_evidence.mjs <프로젝트> --repo`**인용한 것이 실재하는지.** 본문 코드블록의 - `scripts/check_evidence.mjs <프로젝트> --repo`**인용한 것이 실재하는지.** 본문 코드블록의
@@ -65,18 +86,20 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
## 어느 스킬이 무엇을 하나 ## 어느 스킬이 무엇을 하나
이 스킬이 첫 초안을 만든다. 나머지는 초안이 나온 뒤에 각각 다른 것을 고친다. 이 스킬이 첫 초안을 만든다. `writing-practitioner-guides` 는 Setup 본문을 쓰는 동안 함께 열고,
나머지는 초안이 나온 뒤에 각각 다른 것을 고친다.
| 스킬 | 하는 일 | 하지 않는 일 | | 스킬 | 하는 일 | 하지 않는 일 |
|---|---|---| |---|---|---|
| `deriving-tech-log-root-tree` | 후보에 처분을 매기고 `PROMOTE` 를 글감으로 올린다 | 글을 쓰지 않는다 | | `deriving-tech-log-root-tree` | 후보에 처분을 매기고 `PROMOTE` 를 글감으로 올린다 | 글을 쓰지 않는다 |
| `writing-tech-log-records` | 종류를 고르고 칸과 본문을 쓴다. `explaining.md`·`ai-tells.md` 를 처음부터 적용한다 | — | | `writing-tech-log-records` | 종류를 고르고 칸과 본문을 쓴다. `explaining.md`·`ai-tells.md` 를 처음부터 적용한다 | — |
| `technical-visualizer` | 문장으로 따라가기 어려운 관계를 그림으로 만든다 | 모든 글에 그림을 붙이지 않는다 | | `writing-practitioner-guides` | 사람이 직접 치는 명령의 형태를 정한다 — 한 줄에 담는 계층, 도구 우선순위, 출력을 읽는 형태와 값 하나만 뽑는 형태, 넓은 명령에서 좁은 명령으로 | Tech Log 의 종류와 칸을 모른다. SSOT 대조도 색인 갱신도 하지 않는다 |
| `technical-visualizer` | 문장으로 따라가기 어려운 관계를 그림으로 만든다 | 무엇을 그릴지 정하지 않는다 — `choosing-a-diagram.md` 가 정한다 |
| `rewriting-technical-prose-naturally` | 사실과 구조가 이미 맞는 초안의 번역투·반복 문형·과한 대구를 고친다 | 분류가 틀렸거나 근거가 모자란 것은 못 고친다 | | `rewriting-technical-prose-naturally` | 사실과 구조가 이미 맞는 초안의 번역투·반복 문형·과한 대구를 고친다 | 분류가 틀렸거나 근거가 모자란 것은 못 고친다 |
| `writing-as-the-person-who-did-it` | 자료에 남아 있는 선택·비교·어긋남·확인하지 못한 범위를 제자리에 놓는다 | 자료에 없는 「처음에는」·「고민 끝에」를 만들지 않는다 | | `writing-as-the-person-who-did-it` | 자료에 남아 있는 선택·비교·어긋남·확인하지 못한 범위를 제자리에 놓는다 | 자료에 없는 「처음에는」·「고민 끝에」를 만들지 않는다 |
Reference·Question·Decision 은 그림을 렌더링할 자리가 없다. 그림이 필요한 내용은 짝이 되는 Reference·Question·Decision 은 그림을 렌더링할 곳이 없다. 그림이 필요한 내용은 짝이 되는
CaseConcept 에 담고 `관계`로 가리킨다. Case·Concept·Setup 에 담고 `관계`로 가리킨다.
## 보호 구간 ## 보호 구간
@@ -102,7 +125,7 @@ SSOT 에 없는데 필요한 인용이라면 순서가 반대다 — `final/docu
| 실패 | 대응 | | 실패 | 대응 |
|---|---| |---|---|
| Reference 규칙에 코드블록 | Case로 옮겨 관계 연결 | | Reference 규칙에 코드블록 | Case로 옮겨 관계 연결 |
| 본문 밖 칸에 백틱 | 평문이다. 백틱을 뺀다 | | 본문 밖 칸에 백틱을 뺌 | 이 칸도 백틱은 `<code>` 로 산다. 빼는 것은 별표·파이프·`#` 다 |
| 평문 칸에 쉼표 나열 | `이름 : 값`으로 줄 나눔. 있음·없음은 `o`·`x` | | 평문 칸에 쉼표 나열 | `이름 : 값`으로 줄 나눔. 있음·없음은 `o`·`x` |
| 표 머리글이 명사뿐 | 질문으로. 비교 축은 `전`·`후` 시점으로 | | 표 머리글이 명사뿐 | 질문으로. 비교 축은 `전`·`후` 시점으로 |
| 그림 안에 문장·숫자 | `<text>`는 이름만. 문장은 `<desc>`·옆 문단에 | | 그림 안에 문장·숫자 | `<text>`는 이름만. 문장은 `<desc>`·옆 문단에 |
@@ -116,7 +139,14 @@ SSOT 에 없는데 필요한 인용이라면 순서가 반대다 — `final/docu
| 문서마다 같은 문형·같은 길이 | `references/ai-tells.md` | | 문서마다 같은 문형·같은 길이 | `references/ai-tells.md` |
| 표를 `:::table`로 감쌈 | 그냥 파이프로 쓴다 | | 표를 `:::table`로 감쌈 | 그냥 파이프로 쓴다 |
| `![](https://…외부)` | Asset으로 올려 `/api/v1/public/media/…` | | `![](https://…외부)` | Asset으로 올려 `/api/v1/public/media/…` |
| SSOT 에 있는 그림을 두고 새로 그림 | `final/assets/` 를 먼저 본다. `ssot-assets` 가 배정한 것을 쓴다 |
| SSOT 에 있는 측정을 두고 다시 돌림 | `final/evidence/` 의 원문을 가리킨다 |
| Decision에 근거 없음 | 관계 1개 이상 연결 | | Decision에 근거 없음 | 관계 1개 이상 연결 |
| Setup에 끝난 일을 적음 | 따라 할 순서가 아니면 Case다 |
| Setup 본문에 `printf >`·`echo >>`·`python3 -c` | 사람이 치는 형태로 바꾼다. 설정 파일은 에디터로 연다 |
| Setup 본문의 명령만 고치고 SSOT 를 그대로 둠 | `final/document.md` 를 먼저 고친다. `check_evidence.mjs` 가 막는다 |
| Setup 본문에 「2026-09-12 기준」 | 검증일 칸이 없다. 버전은 `pinnedVersions` 에 적는다 |
| Setup에 프로젝트를 안 고름 | 「환경 구성은 프로젝트에 속합니다」로 막힌다 |
| 측정 안 한 검증일 | 비워 둔다 | | 측정 안 한 검증일 | 비워 둔다 |
| 설명 직후에 「~증거다」「~가 아니다」로 평가 | 지운다. 앞 문장이 사실을 말했으면 거기서 끝낸다 | | 설명 직후에 「~증거다」「~가 아니다」로 평가 | 지운다. 앞 문장이 사실을 말했으면 거기서 끝낸다 |
| 「~로 읽기 쉽다. 그렇지 않다」 | 오해를 지어내지 않는다. 관측부터 적는다 | | 「~로 읽기 쉽다. 그렇지 않다」 | 오해를 지어내지 않는다. 관측부터 적는다 |
@@ -93,5 +93,24 @@ mailto:…
| `list items must contain exactly one paragraph` | 목록을 중첩했다 | | `list items must contain exactly one paragraph` | 목록을 중첩했다 |
| `unsafe link URL: …` | 허용되지 않는 스킴이나 `//` 주소 | | `unsafe link URL: …` | 허용되지 않는 스킴이나 `//` 주소 |
| `duplicate explicit ID: …` | 같은 id를 두 번 썼다 | | `duplicate explicit ID: …` | 같은 id를 두 번 썼다 |
| `unknown inline directive: 27` | **문단에 시각을 그냥 썼다** — 아래를 본다 |
거절 메시지에는 줄·칸 번호가 붙는다. 그 줄을 먼저 본다. 거절 메시지에는 줄·칸 번호가 붙는다. 그 줄을 먼저 본다.
## 문단 안의 시각은 백틱으로 감싼다
`:` 뒤에 글자가 붙으면 파서가 인라인 directive 로 읽는다. 그래서 문단에 `08:20:27` 을
그냥 쓰면 `:27` 에서 막힌다. `00/12:00:00` 같은 설정값도 같다.
```text
새 인증서가 08:20:27 에 기록됐다. ← FAIL unknown inline directive: 27
새 인증서가 `08:20:27` 에 기록됐다. ← PASS
```
**이것이 조용한 결함이 되는 경로가 있다.** 막히면 시각을 빼고 넘어가게 되고, 그러면
검사기는 통과하는데 **기록에서 수치가 사라진다.** 실제로 한 회차에서 두 편이 각각
시각 둘과 타이머 설정값을 빼고 통과시켰다. 수치·날짜·시각은 보호 구간이다 —
**빼지 말고 감싼다.**
표와 코드블록 안은 걸리지 않는다. 그래서 같은 프로젝트의 다른 기록이 통과하는 것이
「이 문법이 괜찮다」는 뜻이 아니다 — 그쪽은 시각이 전부 표 안에 있었을 뿐이다.
@@ -0,0 +1,88 @@
# 기록에 어떤 그림이 필요한지 정하는 기준
`code-tables-diagrams.md` 는 그림을 **어떻게** 그리는지를 말한다. 이 문서는 그 앞 단계 —
**이 기록에 그림이 필요한가, 필요하다면 무엇을 그리는가** 를 정한다.
## 세 관문
순서대로 통과해야 그림을 만든다. 하나라도 걸리면 그리지 않는다.
### 1. 자리가 있는가
`assets` 는 본문이 있는 세 종류만 갖는다 — **Case·Concept·Setup**. Reference·Question·Decision 의
칸은 평문으로 렌더링돼 그림이 들어갈 곳이 없다.
파생 기록에 그림이 필요해 보이면 그 그림은 **짝이 되는 Case·Concept·Setup 의 것**이다. 거기 담고
`관계`로 가리킨다. 담을 기록이 없으면 그 그림은 아직 집이 없다 — 계약의
`assetLedger.unassigned` 에 그렇게 적고, 새 글감을 세울지는 따로 판단한다.
### 2. 표가 아닌가
> **관계선을 다 지워도 뜻이 남는가.** 남으면 표다.
표는 값을 비교하고, 그림은 **포함·순서·경계**처럼 자리로만 보이는 것을 맡는다. 항목을 같은
속성으로 늘어놓은 것은 마크다운 표로 쓴다. `verify-project-layout.py` 의 「표로 되는 그림」이
관계선 없이 항목마다 같은 수의 `details` 를 늘어놓은 spec 을 센다.
### 3. 옆 문단이 이미 말하지 않았는가
> **이 그림이 없으면 독자가 무엇을 못 보나.** 한 문장으로 답할 수 없으면 그리지 않는다.
기록에 이미 그 비교표가 있으면 그림은 중복이다. 실제로 그렇게 만든 그림 셋을 지웠다 —
`keyset-vs-offset` 을 넣은 기록에는 훑은 행·buffers·exec 까지 있는 플랜 비교표가 이미 있었다.
## 종류마다 무엇을 그리나
세 관문을 통과했을 때, 그 기록이 요구하는 그림은 종류마다 다르다.
| 종류 | 그림이 답하는 물음 | 흔한 profile |
|---|---|---|
| **Case** | 이 요청 한 번이 어떤 순서로 무엇을 지나갔나 | `sequence` · `component-flow` |
| **Case** (경계가 논지일 때) | 무엇이 어느 경계 안에 있고 무엇이 밖에 있나 | `two-zone-pipeline` |
| **Concept** | 남의 것이 어떤 순서·구조로 동작하나 | `sequence` · `component-flow` · `ports-adapters` |
| **Setup** | (아직 못 적는다 — 아래) | — |
Case 는 **내가 돌려서 본 것**이라 대개 순서가 논지다. Concept 은 **남의 것이 어떻게 동작하는지**라
구조나 변환 사슬이 논지다. 어느 쪽이든 「무엇이 무엇으로 바뀌는가」를 못 적으면 아직 그릴 것이
없다는 뜻이다.
**Setup 은 담을 곳만 있고 본보기가 없다.** 본문 파서가 Case 와 같아서 그림이 렌더링되기는 한다.
다만 2026-09-12 에 Studio 의 환경 구성 문서가 0건이라, 위 두 줄처럼 「반복된 물음」을 셀 자료가
없다. 이 줄은 그림을 그리지 말라는 뜻이 아니라 **아직 아무도 안 그려 봤다**는 뜻이다. 그릴 때는
세 관문만 그대로 지나고, 무엇을 그렸는지 이 표에 적어 둔다.
**한 절에 그림 하나.** 같은 절에 구조 그림과 흐름 그림을 둘 다 넣으면 독자가 어느 쪽을 먼저
읽어야 하는지 알 수 없다. 둘 다 필요하면 절을 나눈다.
## 어디를 근거로 삼나 — 기록의 `source` 가 앵커다
`techviz prepare``final/document.md` 를 받는다. 기록은 `tech-log-studio/` 에 있지만 **그림의
근거는 기록이 아니라 기록이 가리키는 SSOT 절**이다. 기록의 `source` 앵커를 그대로 쓴다.
```bash
# 기록의 source: final/document.md#선택의-이유와-지킨-경계-ap1 이면
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "AP1: OAuth와 JWT 계약을 가장 가까이서 관찰한다" \
-o docs/<프로젝트>/final/.techviz/<id>/context.json
```
`--line` 은 쓰지 않는다. context 는 관리 블록을 접은 좌표를 쓰므로 파일의 줄 번호와 어긋난다.
`--heading` 이나 `--marker` 로 절을 지목한다.
**그림이 주장하는 것을 기록 본문이 말해야 한다.** SSOT 에 근거가 있어도 기록이 그 단계를 적지
않았으면 그림만 넣지 않는다 — 설명 없는 주장이 남는다. 순서는 하나다.
> 본문을 먼저 보강한다 → 그다음 그림을 붙인다.
`ap1-browser-bearer-flow` 가 그랬다. 마지막 단계인 `/api/me` 응답 4필드를 기록이 말하지 않아
붙이지 못하고 있다가, SSOT §474 를 근거로 본문에 한 줄을 더한 뒤에 붙였다.
## 만든 뒤
`code-tables-diagrams.md` 의 규범과 아래 둘을 함께 돌린다. lint 는 라벨이 상자를 덮는 것을
못 잡는다.
```bash
python3 scripts/check-figure-text.py <프로젝트> # <text> 가 전부 이름인가
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs # PNG 로 떠서 눈으로 본다
```
@@ -1,6 +1,6 @@
# 코드·표·다이어그램·이미지 # 코드·표·다이어그램·이미지
본문(`bodyMarkdown`)에만 해당한다. 본문이 있는 종류는 CaseConcept 이다. 본문(`bodyMarkdown`)에만 해당한다. 본문이 있는 종류는 Case·Concept·Setup 셋이다.
## 코드블록 ## 코드블록
@@ -160,6 +160,28 @@ SVG는 `image/svg+xml`로 올라가고 다른 이미지와 같게 다뤄진다.
`READY`가 아닌 Asset은 게시 시 거절된다. `READY`가 아닌 Asset은 게시 시 거절된다.
### 저장소의 `.md` 에는 `:::evidence` 를 쓰지 않는다
`:::evidence`는 Studio 렌더러의 구문이다. 저장소의 `.md`를 그 형태로 쓰면 편집기에서 그림이
보이지 않고 구문이 글자로 남는다. **저장소는 읽는 형태로 쓴다.**
```markdown
![브라우저 SPA, Keycloak, Resource Server 사이에서 …](../../../final/assets/ap1-browser-bearer-flow/ap1-browser-bearer-flow.svg)
```
`alt`는 그림의 `alt`를 그대로 쓰고, 경로는 frontmatter `assets`의 `file`과 같아야 한다.
Studio 로 보낼 때만 `:::evidence` 로 바꾼다. 손으로 고치지 않는다.
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/x.md # check_body 는 이 파일에 돌린다
python3 scripts/studio-body.py <기록.md> --body-only # Studio 에 붙여넣을 본문
python3 scripts/studio-body.py <기록.md> --key a=a-1a2b3c4d # 서버가 준 키로
```
Studio 파서는 상대 경로 이미지를 `unsafe image URL`로 거절하므로 `check_body.mjs`는 **바꾼
파일**에 돌린다. 저장소 파일에 그대로 돌리면 그림 자리에서 실패한다.
## 일반 이미지 ## 일반 이미지
Asset이 아닌 그림은 Markdown으로 쓴다. Asset이 아닌 그림은 Markdown으로 쓴다.
@@ -10,6 +10,8 @@
|---|---| |---|---|
| 코드·설정·실행 증거 | 사실의 근거 | | 코드·설정·실행 증거 | 사실의 근거 |
| `final/document.md` | **글감 범위의 SSOT** — 후보를 발견하는 유일한 입력 | | `final/document.md` | **글감 범위의 SSOT** — 후보를 발견하는 유일한 입력 |
| `final/assets/` · `final/.techviz/` | 이미 그린 그림과 그 정본. 새 후보를 내지 않고 글감에 배정된다 |
| `final/evidence/` | 이미 실행한 측정의 원문. 마찬가지로 배정된다 |
| `analysis/**/*.md` | final이 이미 채택한 주장을 상세히 확인하는 보조 근거 | | `analysis/**/*.md` | final이 이미 채택한 주장을 상세히 확인하는 보조 근거 |
| `tech-log-tree.json` | 사람이 고른 글감. 분해 계약이자 색인이고 이 파일이 정본이다 | | `tech-log-tree.json` | 사람이 고른 글감. 분해 계약이자 색인이고 이 파일이 정본이다 |
@@ -36,6 +38,37 @@
범위 밖의 앵커는 후보가 아니라 근거다. 제1부에서 나온 글감의 `source`로 건다. 범위 밖의 앵커는 후보가 아니라 근거다. 제1부에서 나온 글감의 `source`로 건다.
## 그림과 증거는 후보가 아니라 배정 대상이다
`final/assets/`의 그림과 `final/evidence/`의 측정은 글감을 새로 만들지 않는다. 이미 정해진
글감에 붙는다. 그래서 처분을 매기는 자리가 아니라 **배정하는 자리**이고, 계약의
`ssot-assets`·`ssot-evidence`가 그 자리다.
글감을 다 고른 뒤 두 폴더를 한 번 훑는다. 물음은 하나다.
> **이 그림이나 이 측정은 어느 글감의 것인가. 붙을 글감이 없으면 왜 없는가.**
```json
"ssot-assets": ["ap3-bff-session-flow"],
"ssot-evidence": ["raw/explain/l3-cartesian-join-plan.txt"]
```
배정한 것은 기록의 `assets`·`evidence`가 실제로 가리켜야 한다. 배정해 놓고 쓰지 않으면
`verify-tech-log-tree.py`가 error로 센다.
**배정하지 않으면 글을 쓸 때 같은 그림을 새로 그린다.** keycloak이 그렇게 됐다. SSOT에
`ap3-bff-session-flow`, `ap4-edge-forward-auth-flow`를 포함한 그림 13장이 `.techviz` 정본까지
갖춘 채 있었는데, 기록 24편은 그중 한 장도 가리키지 않고 이름이 다른 그림 5장을 새로 만들어
썼다. 새로 만든 5장에는 정본이 없어서 고칠 수도 없다.
붙을 글감이 없는 그림도 있다. 패턴 넷을 나란히 놓고 비교하는 그림은 Reference에 붙어야 맞는데
Reference에는 본문이 없다. 그런 그림은 그대로 두고, 왜 두는지 계약에 적는다 — 「Reference에만
쓸 자리가 있어 본문 있는 종류에 담지 못한다」처럼. `verify-project-layout.py`가 「기록이 쓰지
않는 SSOT 그림」으로 세므로, 센 숫자가 설명되지 않은 채 남지 않게 한다.
증거도 같다. 재료로만 쓰고 인용하지 않기로 한 측정은 정상이다. 「기록이 인용하지 않는 raw 증거」가
전부 설명되는지만 본다.
## 왜 먼저 나누는가 ## 왜 먼저 나누는가
긴 글을 앞에서부터 잘라 기록으로 만들면 절 하나가 기록 하나가 된다. 그러면 Case의 칸도 긴 글을 앞에서부터 잘라 기록으로 만들면 절 하나가 기록 하나가 된다. 그러면 Case의 칸도
@@ -66,6 +99,7 @@ Reference의 칸도 반쯤만 맞는 기록이 나온다. 종류를 먼저 정
|---|---|---| |---|---|---|
| 재현한 관측 하나 | 조건을 갖추고 돌려서 얻은 수치·실행계획·로그 | Case | | 재현한 관측 하나 | 조건을 갖추고 돌려서 얻은 수치·실행계획·로그 | Case |
| 남의 것의 동작 하나 | 문서와 코드를 읽어 정리한 규칙·흐름 | Concept | | 남의 것의 동작 하나 | 문서와 코드를 읽어 정리한 규칙·흐름 | Concept |
| 남이 따라 할 절차 하나 | 그 환경을 처음 세우는 명령과 구성 값 | Setup |
| 반복 적용할 기준 하나 | 다음에도 같게 하기로 한 규칙 | Reference | | 반복 적용할 기준 하나 | 다음에도 같게 하기로 한 규칙 | Reference |
| 프로젝트가 고른 방향 하나 | 대안을 두고 정한 것 | Decision | | 프로젝트가 고른 방향 하나 | 대안을 두고 정한 것 | Decision |
| 닫히지 않은 판단 하나 | 아직 모르는 것과 무엇을 하면 닫히는지 | Question | | 닫히지 않은 판단 하나 | 아직 모르는 것과 무엇을 하면 닫히는지 | Question |
@@ -74,6 +108,8 @@ Reference의 칸도 반쯤만 맞는 기록이 나온다. 종류를 먼저 정
순서대로 묻는다. 먼저 걸리는 것이 그 글감의 종류다. 순서대로 묻는다. 먼저 걸리는 것이 그 글감의 종류다.
0. **남이 자기 기계에서 따라 할 절차인가** → Setup. 나머지 다섯은 끝난 일을 적고 이것만 실행할
순서를 적는다. 명령과 구성 값이 SSOT에 있어야 하고, 프로젝트를 반드시 고른다
1. **내가 돌려서 얻은 결과인가** → Case. 수치·실행계획·로그가 SSOT에 있어야 한다. 없으면 Case가 아니다 1. **내가 돌려서 얻은 결과인가** → Case. 수치·실행계획·로그가 SSOT에 있어야 한다. 없으면 Case가 아니다
2. **남의 것이 어떻게 동작하는지인가** → Concept. 무엇을 보고 썼는지(버전)가 있어야 한다 2. **남의 것이 어떻게 동작하는지인가** → Concept. 무엇을 보고 썼는지(버전)가 있어야 한다
3. **다음에도 같게 하기로 한 규칙인가** → Reference 3. **다음에도 같게 하기로 한 규칙인가** → Reference
@@ -98,7 +134,7 @@ Reference 하나로 나누고 `관계`로 잇는다.
미리 알아야 하는 구조가 있을 때만 Concept을 만든다. 메커니즘처럼 보이는 절을 훑어 채우면 어느 미리 알아야 하는 구조가 있을 때만 Concept을 만든다. 메커니즘처럼 보이는 절을 훑어 채우면 어느
기록도 필요로 하지 않는 개념이 쌓인다. 기록도 필요로 하지 않는 개념이 쌓인다.
**주제 하나에 독자 질문 하나.** Studio는 주제 아래에 섯 종류를 나눠 보여 준다. 그 주제의 **주제 하나에 독자 질문 하나.** Studio는 주제 아래에 섯 종류를 나눠 보여 준다. 그 주제의
기록들이 함께 답하는 물음을 한 줄로 적고, 그 물음에 답하지 않는 글감은 다른 주제로 옮긴다. 기록들이 함께 답하는 물음을 한 줄로 적고, 그 물음에 답하지 않는 글감은 다른 주제로 옮긴다.
주제 slug는 Studio의 것을 그대로 쓴다. 주제 slug는 Studio의 것을 그대로 쓴다.
@@ -124,7 +160,7 @@ Reference 하나로 나누고 `관계`로 잇는다.
"status": "게시 전", "studioId": "32d0be7d-…", "assets": 1, "evidence": 3 }, "status": "게시 전", "studioId": "32d0be7d-…", "assets": 1, "evidence": 3 },
{ "title": "아직 쓰지 않은 글감" } { "title": "아직 쓰지 않은 글감" }
], ],
"concept": [], "reference": [], "question": [], "decision": [] "concept": [], "setup": [], "reference": [], "question": [], "decision": []
} }
} }
} }
@@ -1,24 +1,41 @@
# 섯 종류의 칸과 게시 조건 # 섯 종류의 칸과 게시 조건
칸 이름은 Studio 편집 화면 그대로, 상한과 필수 여부는 `studio-api.openapi.yaml` 그대로다. 칸 이름은 Studio 편집 화면 그대로, 상한과 필수 여부는 `studio-api.openapi.yaml` 그대로다.
계약 필드명을 괄호에 적는다. 계약은 `tech-log-frontend/src/features/tech-log/contracts/studio/` 계약 필드명을 괄호에 적는다. 계약은 `tech-log-frontend/src/features/tech-log/contracts/studio/`
에 있고, 이 파일과 계약이 어긋나면 계약이 맞다. 에 있고, 이 파일과 계약이 어긋나면 계약이 맞다.
`RecordKind`섯이다 — `CASE` · `REFERENCE` · `QUESTION` · `CONCEPT` · `PROJECT_DECISION`. `RecordKind`섯이다 — `CASE` · `REFERENCE` · `QUESTION` · `PROJECT_DECISION` · `CONCEPT` ·
`SETUP` (`studio-api.openapi.yaml:838-840`).
**이 목록을 손으로 옮길 때마다 종류가 빠졌다.** 이 문서도 한동안 「다섯이다」라고 적고 `SETUP`
을 뺐다. 프론트엔드에서 먼저 같은 일이 났고 소스에 적혀 있다
(`application/ports/studio-gateway.ts:8-12`).
> 종류는 계약의 `RecordKind` 를 그대로 쓴다. 여기 손으로 적어 두었던 동안 개념과 환경 구성이
> 빠져 있었고, 작업본 목록의 종류 필터는 그 둘을 아예 고를 수 없었다 — 손으로 나열한 목록에
> 새 종류를 빠뜨리는 일이 이 저장소에서 반복됐다.
일곱 번째가 생기면 같은 일이 난다. 이 문서를 고칠 때는 기억으로 세지 말고
`studio-api.openapi.yaml``RecordKind` 를 열어 몇 줄인지부터 센다.
## 종류별 칸 (`새 문서` 화면이 적어 주는 그대로) ## 종류별 칸 (`새 문서` 화면이 적어 주는 그대로)
| 화면 이름 | 계약 `kind` | 그 종류만의 칸 | | 화면 이름 | 계약 `kind` | 그 종류만의 칸 |
|---|---|---| |---|---|---|
| Case | `CASE` | 문제 · 결론 · 환경 · 재현 · 본문 | | **검증 기록** (Case) | `CASE` | 문제 · 결론 · 환경 · 재현 · 본문 |
| Reference | `REFERENCE` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 | | **적용 기준** (Reference) | `REFERENCE` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| **개념** | `CONCEPT` | 기준 버전 · 본문 | | **동작 원리** (Concept) | `CONCEPT` | 기준 버전 · 본문 |
| Question | `QUESTION` | 상태 · 사실 · 가정 · 미지수 · 선택지 | | **환경 구성** (Setup) | `SETUP` | 버전 · 본문 |
| Decision | `PROJECT_DECISION` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 | | **열린 질문** (Question) | `QUESTION` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| **설계 결정** (Decision) | `PROJECT_DECISION` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
**화면 이름은 여섯 다 한글이다.** 2026-09-12 에 `/studio/documents/new` 에서 읽었고 작업본
목록(`/studio/documents`)의 종류 필터도 같은 여섯 이름을 쓴다. 이 문서의 절 제목과 산문은
괄호 안의 이름을 쓴다 — 폴더 이름과 frontmatter 의 `kind` 가 그쪽이기 때문이다.
여기에 아래 공통 칸이 더해진다. 여기에 아래 공통 칸이 더해진다.
## 공통 (섯 종류 모두 — `WorkingCopyInputBase`) ## 공통 (섯 종류 모두 — `WorkingCopyInputBase`)
| 칸 | 필드 | 상한 | 게시 조건 | | 칸 | 필드 | 상한 | 게시 조건 |
|---|---|---|---| |---|---|---|---|
@@ -27,7 +44,7 @@
| 요약 | `summary` | **2,000자** | 경고. 목록 카드에는 약 90자까지 보인다 | | 요약 | `summary` | **2,000자** | 경고. 목록 카드에는 약 90자까지 보인다 |
| Topic | `topicId` | — | 경고 | | Topic | `topicId` | — | 경고 |
| 축 | `variantIds` | 8개 | 선택. 고르지 않으면 그 주제의 **공통 기록**이 된다 | | 축 | `variantIds` | 8개 | 선택. 고르지 않으면 그 주제의 **공통 기록**이 된다 |
| Project | `projectId` | — | `PROJECT_DECISION` 게시 시 필수 | | Project | `projectId` | — | `PROJECT_DECISION``SETUP`은 필수 |
| 관계 | `relations` | 20개 | `PROJECT_DECISION`**1개 이상 필수**. 그 종류에서는 절 이름이 「근거」다 | | 관계 | `relations` | 20개 | `PROJECT_DECISION`**1개 이상 필수**. 그 종류에서는 절 이름이 「근거」다 |
편집 화면에서 축은 체크박스 묶음으로 나오고 묶음 이름이 「축 — 고르지 않으면 이 주제의 공통 편집 화면에서 축은 체크박스 묶음으로 나오고 묶음 이름이 「축 — 고르지 않으면 이 주제의 공통
@@ -62,13 +79,13 @@ topicName: OAuth/OIDC 인증 경계
```yaml ```yaml
assets: assets:
- key: eager-lazy-query-sequence # 본문의 :::evidence key 와 같은 값 - key: eager-lazy-query-sequence # 본문의 :::evidence key 와 같은 값
file: ../../../final/assets/tech-log-studio/eager-lazy-query-sequence.svg file: ../../../final/assets/diagrams/eager-lazy-query-sequence/eager-lazy-query-sequence.svg
evidence: evidence:
- ../../../final/evidence/explain/highlights-child-plan-A.txt - ../../../final/evidence/explain/highlights-child-plan-A.txt
``` ```
**`assets` 는 본문이 있는 CaseConcept 에만 둔다.** 나머지 세 종류는 칸이 평문으로 **`assets` 는 본문이 있는 Case·Concept·Setup 에만 둔다.** 나머지 세 종류는 칸이 평문으로
렌더링돼 그림을 표시할 자리가 없다. 그림이 필요한 내용은 Case 나 Concept 에 담고 `관계` 렌더링돼 그림을 표시할 곳이 없다. 그림이 필요한 내용은 본문이 있는 종류에 담고 `관계`
가리킨다. 가리킨다.
`assets` 는 Studio 에 올릴 파일이다. 아직 안 올렸으면 key 가 파일 이름과 같고, 올린 뒤에는 `assets` 는 Studio 에 올릴 파일이다. 아직 안 올렸으면 key 가 파일 이름과 같고, 올린 뒤에는
@@ -80,13 +97,35 @@ evidence:
## 평문 칸 쓰는 법 ## 평문 칸 쓰는 법
본문(`bodyMarkdown`)을 뺀 모든 칸은 평문이다. 백틱·파이프·`#`는 글자 그대로 보이고 줄바꿈만 본문(`bodyMarkdown`)을 뺀 모든 칸은 **마크다운 블록 파서를 거치지 않는다.** 그렇다고 전부
살아난다. 그 줄바꿈이 유일한 서식이므로 아끼지 않는다. 글자 그대로 나오는 것은 아니다. 렌더러가 이 칸들만 따로 그리고(`tech-log-frontend`
`presentation/shared/public-render/prose-text.tsx`), 거기서 셋이 살아난다.
**무엇을 지우나.** 백틱·별표·코드펜스는 글자 그대로 보인다. 식별자는 남기고 표시 문자만 뺀다. | 이 칸에서 | 어떻게 되나 |
`InboxCleanupJob:56` 은 InboxCleanupJob:56 으로, `**this is the parameter**` 는 그 문장만 남긴다. |---|---|
코드펜스 안이 `이름 : 값` 꼴이면 펜스 줄만 지우면 그대로 읽힌다. 줄바꿈이 유일한 서식이므로 | 백틱 쌍 | 인라인 `<code>`**살아난다.** 빼지 않는다 |
문단 사이 빈 줄은 지킨다. | 백틱이 홀수 개 | 짝이 안 맞으므로 원문 그대로 둔다 — 반쯤 해석하지 않는다 |
| 빈 줄 | 문단이 갈린다 |
| 한 줄 바꿈 | `<br>` 로 그 자리에 남는다 |
| 별표·파이프·`#`·코드펜스·인용 표지 `>` | **글자 그대로 보인다.** 이것들만 뺀다 |
**옛 판을 기억하지 마라.** 이 칸들은 오래 진짜 평문으로 나갔고 백틱이 백틱째 화면에
나왔다 — 어떤 Reference 는 한 문서에 백틱이 32개였고 그 원문이 카드와 검색 결과까지
퍼졌다. 그건 **고쳐진 버그**다. 지금 백틱을 빼면 식별자가 본문과 같은 민무늬로 나온다.
**무엇을 지우나.** 별표·코드펜스·`>` 는 글자 그대로 보인다. 식별자는 남기고 표시 문자만 뺀다.
`**this is the parameter**` 는 그 문장만 남긴다. 코드펜스 안이 `이름 : 값` 꼴이면 펜스 줄만
지우면 그대로 읽힌다. 백틱은 그대로 두고, 문단 사이 빈 줄도 지킨다.
**SSOT 를 그대로 옮긴 인용도 `>` 를 못 쓴다.** 인용이라는 것을 표지로 나타낼 방법이 이 칸에는
없다 — `>` 도, 들여쓰기도 안 산다. 표지를 빼고 한 문단으로 두거나, 인용이 꼭 인용으로 보여야
하면 본문이 있는 종류로 옮긴다. 「」 를 새로 씌우지 않는다. 옮긴 글자는 보호 구간이라 그대로다.
**코드펜스를 뗄 때 언어 표시 줄을 같이 지운다.** ` ```text ` 에서 펜스만 지우면 `text` 한 줄이
남고, 그 낱말이 화면에 그대로 나온다. 실제로 한 기록에서 그렇게 남아 있었다.
**칸이 어떻게 보이는지는 렌더러가 정본이다.** 이 파일이 아니다. 여기 적힌 것과 화면이
다르면 `prose-text.tsx``public-record-renderer.tsx` 를 열어서 가른다.
관계(`근거`) 절은 평문 칸이 아니다. 기록을 거는 목록이라 `- **제목**` 표기를 그대로 둔다. 관계(`근거`) 절은 평문 칸이 아니다. 기록을 거는 목록이라 `- **제목**` 표기를 그대로 둔다.
@@ -116,25 +155,26 @@ issuer · audience : 검증
| 검증 환경 | `environment` | 런타임·버전·DB·도구 | | 검증 환경 | `environment` | 런타임·버전·DB·도구 |
| 재현 조건 | `reproduction` | 다른 사람이 같은 결과를 얻는 방법 | | 재현 조건 | `reproduction` | 다른 사람이 같은 결과를 얻는 방법 |
| 마지막 검증일 | `lastVerifiedOn` | 실제로 확인한 날. 30일 지나면 경고 | | 마지막 검증일 | `lastVerifiedOn` | 실제로 확인한 날. 30일 지나면 경고 |
| 본문 Markdown | `bodyMarkdown` | 10만 자. 본문을 갖는 종류 중 하나 | | 본문 Markdown | `bodyMarkdown` | 10만 자. 본문을 갖는 종류 중 하나 |
공개 화면에서 `검증 환경``재현 조건``environmentSummary` 배열에 그 순서로 실린다. 공개 화면에서 `검증 환경``재현 조건``environmentSummary` 배열에 그 순서로 실린다.
## Concept — 남의 것이 어떻게 동작하는지 ## Concept — 남의 것이 어떻게 동작하는지
`새 문서` 화면에서 이 종류만 이름이 한글이다. **개념」을 고른다.** 나머지 넷은 Case·Reference· `새 문서` 화면에서 **동작 원리」를 고른다.** 화면 설명은 「개념의 내부 동작과 아키텍처를 처음부터
Question·Decision 으로 적혀 있다. 화면 설명은 「개념의 내부 동작과 아키텍처를 처음부터 풀어 씁니다」다. 풀어 씁니다.」다. 전에 이 절은 「이 종류만 이름이 한글이다」라고 적었는데 2026-09-12 에는 여섯 다
한글이었다.
| 칸 | 필드 | 비고 | | 칸 | 필드 | 비고 |
|---|---|---| |---|---|---|
| 본문 Markdown | `bodyMarkdown` | 10만 자. **Case 함께 본문을 갖는 종류 중 하나** | | 본문 Markdown | `bodyMarkdown` | 10만 자. **Case·Setup 과 함께 본문을 갖는 종류 중 하나** |
| 기준 버전 | `basisVersion` | 120자. 무엇을 보고 쓴 글인지 한 줄 | | 기준 버전 | `basisVersion` | 120자. 무엇을 보고 쓴 글인지 한 줄 |
칸이 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 재현한 결과가 아니라 **이미 그렇게 동작하는 칸이 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 재현한 결과가 아니라 **이미 그렇게 동작하는
것**을 적기 때문이다. `Authorization Code Flow 가 code 를 한 번 더 교환하는 이유`, `Forward-Auth 것**을 적기 때문이다. `Authorization Code Flow 가 code 를 한 번 더 교환하는 이유`, `Forward-Auth
subrequest 가 실어 보내는 것` 같은 것이 여기 온다. subrequest 가 실어 보내는 것` 같은 것이 여기 온다.
**`lastVerifiedOn` 이 없고 `basisVersion`그 자리를 대신한다.** 개념은 날짜로 낡지 않고 버전으로 **`lastVerifiedOn` 이 없고 `basisVersion`낡음을 말한다.** 개념은 날짜로 낡지 않고 버전으로
낡기 때문이다. `Keycloak 26.7.0 identity brokering`, `Kubernetes 1.31` 처럼 적는다. 비워도 게시된다. 낡기 때문이다. `Keycloak 26.7.0 identity brokering`, `Kubernetes 1.31` 처럼 적는다. 비워도 게시된다.
공개 주소는 `/concepts/{slug}` 다. 공개 주소는 `/concepts/{slug}` 다.
@@ -143,8 +183,7 @@ subrequest 가 실어 보내는 것` 같은 것이 여기 온다.
검증도 막지 않는다(2026-09-04 에 임시 개념 기록을 만들어 비운 채로 게시해 확인했다). 그래도 검증도 막지 않는다(2026-09-04 에 임시 개념 기록을 만들어 비운 채로 게시해 확인했다). 그래도
채우는 편이 낫다 — 개념이 언제 낡았는지 읽는 사람이 알 방법이 이 칸뿐이다. 채우는 편이 낫다 — 개념이 언제 낡았는지 읽는 사람이 알 방법이 이 칸뿐이다.
편집 화면 오른쪽 `작업 상태` 이 종류를 「동작 원리」라고 부른다. `새 문서` 의 「개념」과 같은 편집 화면 오른쪽 `작업 상태` 이 종류를 「동작 원리」라고 부른다.
것이다.
Case 와 헷갈리면 **내가 무엇을 했는지**를 묻는다. 내가 돌려 보고 수치를 얻었으면 Case, 남의 문서와 Case 와 헷갈리면 **내가 무엇을 했는지**를 묻는다. 내가 돌려 보고 수치를 얻었으면 Case, 남의 문서와
코드를 읽고 동작을 정리했으면 Concept 이다. 코드를 읽고 동작을 정리했으면 Concept 이다.
@@ -169,6 +208,87 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
시점`, `커밋 증거 상태 전이`, `fenced lease 와 CAS`, `gRPC flow control 과 backpressure`. 시점`, `커밋 증거 상태 전이`, `fenced lease 와 CAS`, `gRPC flow control 과 backpressure`.
여러 Case 가 같은 선수 지식을 요구할 때 그것을 한 번만 설명하려고 만드는 자리다. 여러 Case 가 같은 선수 지식을 요구할 때 그것을 한 번만 설명하려고 만드는 자리다.
## Setup — 남이 따라 할 절차 (`SETUP`)
화면 이름은 「환경 구성」이고 설명은 「이 기록을 자기 손으로 재현하는 절차를 남깁니다.」다.
편집 화면은 구역 둘로 나뉜다 — 「기본 정보」와 「환경 구성」(eyebrow `SETUP`).
| 화면 이름 | 필드 | 상한·모양 |
|---|---|---|
| 고정한 버전 | `pinnedVersions` | 배열 30개. 줄마다 `이름`(1~60자) + `버전`(1~40자) 입력 둘. 「버전 추가」 버튼으로 늘린다 |
| 절차 Markdown | `bodyMarkdown` | 10만 자 |
화면에 붙은 도움말을 그대로 옮기면 이렇다.
- 고정한 버전 : `“Keycloak” / “26.7.0” 처럼 적습니다. 비우면 화면에 표를 그리지 않습니다.`
- 절차 Markdown : `“##” 소제목이 목차가 됩니다. 명령은 코드블록으로 적어야 그대로 복사됩니다.`
`SetupInput.required``[kind, bodyMarkdown, pinnedVersions]` 다.
**작업본을 만들면 본문이 비어 있지 않다.** Studio 가 절 뼈대를 미리 넣어 준다.
```text
## 실행 절차
## 구성 값
## 확인 방법
```
계약의 `bodyMarkdown` 설명은 「실행 절차·구성 값·확인 방법을 `##` 절로 적는다. **절 이름을
강제하지 않는다 — 프로젝트마다 셋업의 모양이 다르다.**」다. 뼈대는 출발점이고, 절 이름은 그
프로젝트가 쓰는 말로 바꿔도 저장과 게시가 막히지 않는다.
### 왜 이 종류가 따로 있나
다른 다섯은 끝난 일을 적고 환경 구성만 남이 따라 할 절차를 적는다. 편집 화면 주석
(`presentation/studio/components/setup-fields.tsx:44-52`)이 그 차이를 적어 두었다.
> 다른 다섯 종류는 끝난 일을 적는다. 이 종류만 읽는 사람이 그대로 따라 하는 절차를 적으므로,
> 본문에 명령과 표가 들어간다 — Case 의 「검증 환경」 같은 평문 한 칸으로는 담기지 않는다.
> … 버전만 본문 밖에 둔다. 이 절차가 어느 버전 위에서 성립했는지는 그 기록의 유효 범위이고,
> 목록과 머리말이 본문을 열지 않고 보여 줘야 하는 값이기 때문이다.
그래서 칸이 둘뿐인데도 Concept 과 다르게 쓴다. 명령·표·그림은 본문에 넣고 버전만 본문 밖에
남긴다. 개념의 「기준 버전」도 같은 이유로 본문 밖에 있고, 다른 점은 셋업의 버전이 여럿이라는
데 있다.
**검증일 칸이 없다.** Case 의 `lastVerifiedOn` 도 Reference 의 `verifiedOn` 도 이 종류에는 없다.
공개 계약의 `SetupDetailResponse` 가 왜인지 적는다.
> 환경 구성은 끝난 일이 아니라 따라 하는 절차다. 낡음은 검증일이 아니라
> `pinnedVersions` 가 말한다 — 어느 버전 위에서 이 절차가 성립했는지가 유효 범위다.
> 주제는 없을 수 있다. 주제 없는 셋업은 그 프로젝트의 공통 구성이다.
### 프로젝트는 필수, 주제는 선택
`PROJECT_DECISION` 말고 프로젝트를 요구하는 종류가 하나 더 있다.
```text
if (input.kind === "SETUP" && !project) fail("환경 구성은 프로젝트에 속합니다. 기본 정보에서 프로젝트를 골라 주세요.");
```
`domain/content-format/project-public-render-model.ts:245` 다.
주제는 비워도 된다. 비우면 그 프로젝트의 공통 구성으로 읽힌다. 다만 2026-09-12 에 빈 초안의
미리보기는 `1:1 TOPIC catalog entry is required` 로 막혔다 — 미리보기를 보려면 Topic 을 고른다.
### 본문 파서와 공개 주소
본문 파서는 Case 와 같다(`presentation/public/components/setup-document-page.tsx:11`).
> 환경 구성의 본문도 Case 와 같은 파서를 탄다 — `##` 소제목이 목차가 되고 `:::evidence` 가…
그래서 코드블록·표·다이어그램·이미지를 쓸 수 있다.
- 공개 상세 : `/setups/{slug}` (`contracts/tech-log-route-contract.ts:31`, 라우트 제목 「환경 구성」)
- 공개 목록 : `/explore/setups` — 설명 「이 기록을 자기 손으로 재현하는 절차를 남깁니다.」
(`presentation/public/pages/explore-kind-page.tsx:17`)
**Studio 에 환경 구성 문서는 아직 0건이다.** 2026-09-12 에 `/studio/documents?kind=SETUP`
「0개 중 0개 표시 중」이었다. 종류는 있는데 한 번도 쓰이지 않았다. 위의 칸 설명은 계약과 편집
화면에서 읽었고, 올라간 기록에서 확인하지 않았다.
## Reference — 반복 적용할 기준 ## Reference — 반복 적용할 기준
| 칸 | 필드 | 비고 | | 칸 | 필드 | 비고 |
@@ -217,6 +337,10 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
## 종류 고르기 ## 종류 고르기
```text ```text
남이 그대로 따라 할 절차를 적나 ── 예 ──→ Setup
아니오 (끝난 일을 적는다)
내가 직접 돌려 보고 결과를 얻었나 ── 예 ──→ Case 내가 직접 돌려 보고 결과를 얻었나 ── 예 ──→ Case
아니오 아니오
@@ -236,6 +360,11 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
└──→ Reference └──→ Reference
``` ```
첫 갈래가 「끝난 일을 적나, 남이 따라 할 절차를 적나」다. 나머지 다섯은 이미 끝난 일을 적고,
Setup 만 읽는 사람이 자기 기계에서 실행할 순서를 적는다. 편집 화면 주석이 그 경계를 「Case 의
「검증 환경」 같은 평문 한 칸으로는 담기지 않는다」로 적는다 — 명령이 여러 줄이고 그대로
복사돼야 하면 Case 의 평문 칸이 아니라 Setup 의 본문에 들어간다.
Case 와 Concept 이 가장 자주 헷갈린다. **수치와 재현 조건이 있으면 Case**, 읽고 정리한 동작이면 Case 와 Concept 이 가장 자주 헷갈린다. **수치와 재현 조건이 있으면 Case**, 읽고 정리한 동작이면
Concept 이다. Concept 과 Reference 는 「무엇이 그런가」와 「우리가 어떻게 할 것인가」로 갈린다 — Concept 이다. Concept 과 Reference 는 「무엇이 그런가」와 「우리가 어떻게 할 것인가」로 갈린다 —
`PKCE 가 code 가로채기를 막는 방식`은 Concept, `우리는 public client 에 PKCE 를 필수로 쓴다` `PKCE 가 code 가로채기를 막는 방식`은 Concept, `우리는 public client 에 PKCE 를 필수로 쓴다`
@@ -27,6 +27,25 @@
- [ ] Decision에 근거 기록이 1개 이상 연결됐다 - [ ] Decision에 근거 기록이 1개 이상 연결됐다
- [ ] Decision의 영향에 감수한 비용이 있다 - [ ] Decision의 영향에 감수한 비용이 있다
- [ ] Reference의 예시가 문장이다. 코드를 밀어 넣지 않았다 - [ ] Reference의 예시가 문장이다. 코드를 밀어 넣지 않았다
- [ ] Setup이 끝난 일이 아니라 남이 따라 할 순서를 적었다
- [ ] Setup의 명령이 코드블록에 있다. 산문에 섞지 않았다
- [ ] Setup의 `pinnedVersions`가 채워졌다. 본문에 날짜를 적어 검증일을 대신하지 않았다
## 환경 구성의 명령 (`writing-practitioner-guides`)
명령을 어떤 형태로 쓸지는 그 스킬이 정한다. 여기서는 그 결과가 본문에 남았는지만 센다.
- [ ] 사람이 내용을 읽고 고쳐야 하는 설정 파일을 에디터로 열게 했다. `printf >`·`echo >>`·`cat <<EOF` 로 만들지 않았다
- [ ] 한 명령에 여러 작업이 겹치지 않았다. 접속·리다이렉션·권한·파일 생성을 한 줄에 묶지 않았다
- [ ] 그 도메인의 전용 검증 명령이 있는데 `python3 -c` 로 대신하지 않았다
- [ ] 다시 따라 해도 같은 줄이 또 붙지 않는다. 두 번 실행하면 늘어나는 명령이 없다
- [ ] 조회·진단·실행은 운영자가 쓰는 CLI 를 그대로 썼다. `grep`·`virsh`·`systemctl` 을 에디터로 바꾸지 않았다
- [ ] 단계마다 목적·행동·예상 결과·왜 필요한가·문제가 생기면이 같은 순서로 있다
- [ ] 셸이 여럿이면 코드블록마다 `label` 로 어디서 치는지 적었다
- [ ] 자리표시자가 없다. 값을 찾는 명령이 함께 있다
- [ ] 앞 단계가 찍은 값을 옮겨 넣는 자리만 예외다. 그 자리는 `{{NAME}}` 으로 적었다. 한글로 감싸지도 `${NAME}` 으로 적지도 않았다
- [ ] 비밀은 길이나 존재 여부까지만 확인하고 값을 찍지 않았다
- [ ] 명령의 형태를 고쳤으면 SSOT 를 먼저 고쳤다. `check_evidence.mjs` 가 통과한다
## 설명 ## 설명
@@ -80,7 +99,7 @@
- [ ] 테스트를 말할 때 무엇을 단정하는지 적었다 - [ ] 테스트를 말할 때 무엇을 단정하는지 적었다
- [ ] 어미·절·안내 문장의 수치는 참고만 했다. 맞추려고 문장을 넣지 않았다 - [ ] 어미·절·안내 문장의 수치는 참고만 했다. 맞추려고 문장을 넣지 않았다
## 본문 (Case) ## 본문 (Case · Concept · Setup)
- [ ] raw HTML·각주·체크박스·중첩 목록·취소선이 없다 - [ ] raw HTML·각주·체크박스·중첩 목록·취소선이 없다
- [ ] 표를 파이프로 썼다. 감쌌다면 세 속성이 다 있다 - [ ] 표를 파이프로 썼다. 감쌌다면 세 속성이 다 있다
@@ -90,12 +109,17 @@
- [ ] 그림이 있는 문단에 글을 섞지 않았다 - [ ] 그림이 있는 문단에 글을 섞지 않았다
- [ ] `alt`가 무엇이 보이는지 말한다 - [ ] `alt`가 무엇이 보이는지 말한다
- [ ] 그림 안 `<text>`가 전부 이름이다. 문장도 숫자도 없다 (`<title>`·`<desc>`는 예외) - [ ] 그림 안 `<text>`가 전부 이름이다. 문장도 숫자도 없다 (`<title>`·`<desc>`는 예외)
- [ ] 눈으로만 보지 않고 `python3 scripts/check-figure-text.py <프로젝트>` 를 돌렸다
- [ ] 관계선을 다 지워도 뜻이 남는 그림이 아니다 (남으면 표다 — 마크다운 표로 쓴다)
- [ ] `python3 scripts/preview-figure.py`로 PNG 를 떠서 눈으로 봤다 (lint 는 라벨이 상자를 덮는 것을 못 잡는다)
- [ ] 본문의 그림이 마크다운 이미지다 (`:::evidence` 는 Studio 로 보낼 때 `studio-body.py` 가 만든다)
- [ ] 코드에 자격증명·토큰·내부 호스트가 없다. 지운 자리가 보인다 - [ ] 코드에 자격증명·토큰·내부 호스트가 없다. 지운 자리가 보인다
- [ ] 제목 id와 표 id가 겹치지 않는다 - [ ] 제목 id와 표 id가 겹치지 않는다
## 평문 칸 ## 평문 칸
- [ ] 본문 밖 칸에 백틱·파이프가 없다 - [ ] 본문 밖 칸에 별표·파이프·`#`·코드펜스·인용 표지 `>` 가 없다 (**백틱은 괜찮다** — 인라인 `<code>` 로 산다)
- [ ] 코드펜스를 뗀 자리에 언어 표시(` ```text ``text`)가 남지 않았다
- [ ] 나열을 쉼표로 잇지 않고 `이름 : 값`으로 끊었다 - [ ] 나열을 쉼표로 잇지 않고 `이름 : 값`으로 끊었다
- [ ] 있음·없음을 `o`·`x`로 적었다 - [ ] 있음·없음을 `o`·`x`로 적었다
- [ ] 한 문장이 화면에서 두 줄을 넘지 않는다 - [ ] 한 문장이 화면에서 두 줄을 넘지 않는다
@@ -103,7 +127,7 @@
## 연결 ## 연결
- [ ] Topic이 지정됐다 - [ ] Topic이 지정됐다
- [ ] Project가 필요하면 지정됐고, 그 Project에 slug가 있다 - [ ] Project가 필요하면 지정됐고, 그 Project에 slug가 있다 (Decision과 Setup은 필수다)
- [ ] 관계의 대상이 실제로 있는 공개 기록이다 - [ ] 관계의 대상이 실제로 있는 공개 기록이다
- [ ] 코드·표·그림이 필요한 내용을 Reference나 Question에 밀어 넣지 않았다 - [ ] 코드·표·그림이 필요한 내용을 Reference나 Question에 밀어 넣지 않았다
- [ ] 본문이 없는 세 종류에 `assets`를 선언하지 않았다 - [ ] 본문이 없는 세 종류에 `assets`를 선언하지 않았다
@@ -127,4 +151,6 @@
- [ ] Decision 에 근거가 하나 이상 있고, 무엇을 보고 정했는지가 적혀 있는가 - [ ] Decision 에 근거가 하나 이상 있고, 무엇을 보고 정했는지가 적혀 있는가
- [ ] 지어낸 경험·실패·동기·감정이 없는가 - [ ] 지어낸 경험·실패·동기·감정이 없는가
- [ ] 그림이 실제 asset 파일을 가리키고, 있어야 할 이유가 있는가 - [ ] 그림이 실제 asset 파일을 가리키고, 있어야 할 이유가 있는가
- [ ] `final/assets/` 에 이미 있는 그림을 두고 같은 것을 새로 그리지 않았는가
- [ ] 계약이 `ssot-assets`·`ssot-evidence` 로 배정한 것을 기록이 가리키는가
- [ ] 그림이 관측하지 않은 사건을 만들어 내지 않았는가 - [ ] 그림이 관측하지 않은 사건을 만들어 내지 않았는가
@@ -46,9 +46,34 @@ analysis material in one file. Only the first is candidate material.
`excluded` names the parts that are evidence rather than candidates. A node may cite an `excluded` names the parts that are evidence rather than candidates. A node may cite an
anchor from an excluded part in `source`; it may not exist because of one. anchor from an excluded part in `source`; it may not exist because of one.
`excludedAnchorPattern` is optional and is the only field the verifier can act on. Without it
the rule above is a sentence nobody enforces — the check that a node did not come *only* from
outside the scope is switched off entirely.
```json
"candidateScope": {
"document": "final/document.md",
"sections": ["§3", "§4"],
"excluded": ["제2부 — 모듈 분석 전문"],
"excludedAnchorPattern": "#a[0-9]+$"
}
```
It is a regular expression matched against each `source` anchor. A node whose anchors *all*
match it is an error: it was promoted from evidence, not from the candidate scope.
## Anchors resolve to real sections
`source` and `sourceRefs` anchors are checked against the SSOT's own headings when the project
writes them as heading slugs (`#검토한-선택지와-막힌-지점-ap1` = the h2 slug plus a
discriminator). **Keep one anchor style per project.** A project that numbers its anchors
(`#§1.1`, `#10-2`, `#a18`) gets a warning instead — the verifier cannot tell whether the
section it names exists, and the diagram stage cannot translate the anchor into a
`techviz prepare --heading` value without a person reading it.
## Topics ## Topics
Each Topic has `topic` (its key), `title`, **`readerQuestion`**, and `kinds` with the five Each Topic has `topic` (its key), `title`, **`readerQuestion`**, and `kinds` with the six
record kinds. record kinds.
```json ```json
@@ -56,13 +81,13 @@ record kinds.
"topic": "oauth-oidc-auth-boundary", "topic": "oauth-oidc-auth-boundary",
"title": "OAuth 자격증명과 세션의 보관 경계", "title": "OAuth 자격증명과 세션의 보관 경계",
"readerQuestion": "자격증명과 세션을 누가 보관하고, 누가 API 요청을 만들며, 보호 자원은 무엇을 신뢰하는가?", "readerQuestion": "자격증명과 세션을 누가 보관하고, 누가 API 요청을 만들며, 보호 자원은 무엇을 신뢰하는가?",
"kinds": { "case": [], "concept": [], "reference": [], "question": [], "decision": [] } "kinds": { "case": [], "concept": [], "setup": [], "reference": [], "question": [], "decision": [] }
} }
``` ```
Every node in the Topic must help answer the reader question. Two Topics do not share a Every node in the Topic must help answer the reader question. Two Topics do not share a
question; one Topic does not need two. Empty kinds are allowed — do not manufacture nodes question; one Topic does not need two. Empty kinds are allowed — do not manufacture nodes
to fill all five. to fill all six.
## Candidates ## Candidates
@@ -83,7 +108,55 @@ whose candidate is `PROMOTE` and `CONFIRMED`.
`readiness` · `source` · `code` · `evidence` · `classification` · `relations` and the rest `readiness` · `source` · `code` · `evidence` · `classification` · `relations` and the rest
of each kind's fields are written by a person. `build-tech-log-tree.py` never touches them. of each kind's fields are written by a person. `build-tech-log-tree.py` never touches them.
It refreshes only what it can read from the record files — `file`, `publication`, `status`, It refreshes only what it can read from the record files — `file`, `publication`, `status`,
`studioId`, `assets`, `evidenceFiles` — and lists records that have no node in `unlisted`. `studioId`, `assets`, `assetFiles`, `evidenceFiles` — and lists records that have no node in
`unlisted`.
### `ssot-assets` · `ssot-evidence`
The SSOT is not only `final/document.md`. `final/assets/` holds diagrams that were already
drawn, each with its canonical `final/.techviz/<name>/`, and `final/evidence/` holds
measurements that were already run. Neither produces candidates — both are **assigned** to
candidates that already exist, and these two fields hold the assignment.
```json
"ssot-assets": ["ap3-bff-session-flow"],
"ssot-evidence": ["raw/explain/l3-cartesian-join-plan.txt"]
```
`ssot-assets` names diagrams by file stem; the file must exist somewhere under
`final/assets/`. `ssot-evidence` takes paths relative to `final/evidence/`. Both are
written by a person and both are optional — a node that needs no picture and cites no
measurement leaves them out.
What they are not optional about is follow-through. Once a node is assigned a diagram and
its record is written, the record's `assets` must point at that file and its `evidence` at
that path; `verify-tech-log-tree.py` reports the gap as an error. Assigning and then not
using is the failure these fields exist to catch — without them a writer draws the picture
again instead of finding the one that is already there.
`verify-project-layout.py` counts the other direction: SSOT diagrams and raw evidence that
no record cites at all. Some of that count is correct — a four-pattern comparison diagram
belongs to a Reference, and Reference has no body to render it in. The count is meant to be
explained, not driven to zero.
### `assetLedger`
That explanation lives at the top level of the index, next to `candidateScope`. It names
what was assigned and, for everything left over, why it is left over.
```json
"assetLedger": {
"assigned": ["ap3-bff-session-flow", "ap3-csrf-boundary"],
"unassigned": [
{"asset": ["four-pattern-request-boundaries"],
"reason": "네 패턴을 비교하는 그림이라 붙을 자리가 Reference 인데 Reference 에는 본문이 없다"}
]
}
```
A diagram left out for a reason is a normal outcome, the same way `KEEP_IN_SSOT` is. What
is not normal is a leftover nobody looked at — that is the state where the next writer
draws the picture again. Write the ledger when the count first appears, not when it grows.
### Case ### Case
@@ -105,6 +178,26 @@ be stale.
A Concept exists because a Case, Decision, or Question needs it to be understood. Absence, A Concept exists because a Case, Decision, or Question needs it to be understood. Absence,
call-counts, unwired subsystems, analysis scope, and coverage ledgers are not Concepts. call-counts, unwired subsystems, analysis scope, and coverage ledgers are not Concepts.
### Setup
`slug` · `readiness` · `source` · `classification` · `pinned-versions` · `relations`.
Setup is the one kind that does not report a finished result. It is a procedure a reader
runs on their own machine, so `pinned-versions` states the versions the procedure was
established on — the same job `basis-version` does for a Concept, except there is more than
one of them. There is no verification date for this kind.
A Setup node also needs a project. `SETUP` and `PROJECT_DECISION` are the two kinds Studio
refuses to save without one; a Topic is optional, and a Setup with no Topic reads as that
project's shared configuration.
`verify-tech-log-tree.py` checks this kind like the others: `REQUIRED_FIELDS["setup"]` names
the six fields above and `GENERATABLE["setup"]` is `READY`. The kind list those tables are
keyed on lives in `techlog.KINDS`, which also decides which `<topic>/<kind>/` folders
`build-tech-log-tree.py` scans. Add a kind in one place and the tables that key off it go
quiet rather than failing — a kind missing from `REQUIRED_FIELDS` is not an error, it is a
node nobody asks anything of.
### Reference ### Reference
`slug` · `readiness` · `source` · `classification` · `scope` · `exceptions` · `relations`. `slug` · `readiness` · `source` · `classification` · `scope` · `exceptions` · `relations`.
@@ -7,18 +7,21 @@
Question 4·Decision 2), n+1liner 24건(Case 5·Reference 7·Question 5·Decision 7). Question 4·Decision 2), n+1liner 24건(Case 5·Reference 7·Question 5·Decision 7).
「대개 이렇게 쓴다」는 말은 그 47건이 그렇게 돼 있다는 뜻이다. 「대개 이렇게 쓴다」는 말은 그 47건이 그렇게 돼 있다는 뜻이다.
**Setup 은 그 47건에 없다.** 2026-09-12 에 Studio 의 환경 구성 문서가 0건이었다. 그래서 Setup
절은 계약과 편집 화면에서 읽어 썼다. 나머지 다섯처럼 올라간 기록을 세지 않았다.
`clean-architecture-backend-template` 의 949건은 다른 절 이름으로 쓰여 있었고, 지금 이 기준으로 `clean-architecture-backend-template` 의 949건은 다른 절 이름으로 쓰여 있었고, 지금 이 기준으로
다시 판정하는 중이다. 아직 기준을 만족한 상태가 아니므로 그 949건을 본보기로 삼지 않는다. 올라간 다시 판정하는 중이다. 아직 기준을 만족한 상태가 아니므로 그 949건을 본보기로 삼지 않는다. 올라간
적 없는 초안이 아니라 **올라간 것**이 기준이다. 적 없는 초안이 아니라 **올라간 것**이 기준이다.
## 파일 뼈대 — 섯 종류가 같다 ## 파일 뼈대 — 섯 종류가 같다
```markdown ```markdown
--- ---
id · kind · slug · title · topic · topicName · project · status · studio id · kind · slug · title · topic · topicName · project · status · studio
source · sourceRevision source · sourceRevision
(종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn (종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn · pinnedVersions
(있으면) evidence · assets — assets 는 Case Concept 만 (있으면) evidence · assets — assets 는 Case · Concept · Setup
--- ---
# 제목 # 제목
@@ -27,7 +30,7 @@ source · sourceRevision
## 관계 ← Decision 만 「근거」다 ## 관계 ← Decision 만 「근거」다
## <칸 이름> ← 종류마다 다르다 ## <칸 이름> ← 종류마다 다르다
## 본문 ← Case · Concept 만 ## 본문 ← Case · Concept · Setup
<!-- body:start --> <!-- body:start -->
... ...
<!-- body:end --> <!-- body:end -->
@@ -110,6 +113,159 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
「확인했다」는 Case 의 말이다. Concept 은 「~한다」로 적는다. 규격이 정한 것과 그 구현이 그렇게 한 「확인했다」는 Case 의 말이다. Concept 은 「~한다」로 적는다. 규격이 정한 것과 그 구현이 그렇게 한
것을 구분한다. 것을 구분한다.
## Setup — 0건
칸은 `관계` · `본문` 둘뿐이고, 고정한 버전은 frontmatter 의 `pinnedVersions` 에 있다. 계약의
`PinnedVersion``name``version` 을 나눠 담는다 — 이름 1~60자, 버전 1~40자, 30개까지.
```yaml
pinnedVersions:
- name: Keycloak
version: 26.7.0
```
**절 이름을 강제하지 않는다.** 계약이 그렇게 적는다 — 「실행 절차·구성 값·확인 방법을 `##`
절로 적는다. 절 이름을 강제하지 않는다 — 프로젝트마다 셋업의 모양이 다르다.」 그래도 빈 본문에서
시작하지는 않는다. 작업본을 만들면 Studio 가 절 셋을 미리 넣어 주므로 거기서 출발한다.
```markdown
## 실행 절차
## 구성 값
## 확인 방법
```
**명령은 코드블록으로 적는다.** 절차 Markdown 칸의 도움말이 이유를 적는다 —
`“##” 소제목이 목차가 됩니다. 명령은 코드블록으로 적어야 그대로 복사됩니다.` 읽는 사람이
자기 기계에서 실행하므로, 산문에 섞어 적으면 복사할 때 프롬프트 기호와 설명이 함께 붙는다.
Case 와 무엇이 다른지는 **읽는 사람이 무엇을 하는가**로 갈린다. Case 의 「재현 조건」은 내가 잰
값을 남이 다시 얻는 순서이고, Setup 의 본문은 그 환경을 처음 세우는 절차다. Case 는 평문 한 칸에
그 순서를 담지만 Setup 은 본문을 쓰므로 명령·표·그림이 들어간다.
**검증일을 쓰지 않는다.** 이 종류에는 그 칸이 없다. 절차가 어느 버전 위에서 성립했는지는
`pinnedVersions` 가 말하므로, 본문에 「2026-09-12 기준」 같은 날짜를 적어 대신하지 않는다.
`project` 는 비울 수 없다. `topic` 은 비워도 되고, 비우면 그 프로젝트의 공통 구성으로 읽힌다.
### 본문을 쓰기 전에 `writing-practitioner-guides` 를 연다
`Skill` 도구로 `writing-practitioner-guides` 를 부른다. 명령을 어떤 형태로 쓸지는 그 스킬이
정한다 — 한 줄에 어느 계층까지 담는지, 어떤 도구를 먼저 잡는지, 출력을 읽는 형태와 값 하나만
뽑는 형태를 어떻게 가르는지, 넓은 명령에서 좁은 명령으로 내려가는 순서, 무엇이 보이면 멈추고
다시 쓰는지가 거기 적혀 있다. 그 규칙을 이 문서로 옮겨 적지 않는다. 같은 규칙이 두 곳에 있으면
한쪽만 고쳐지고 둘이 갈린다.
다른 다섯 종류에는 이 절차가 없다. 나머지는 끝난 일을 적으므로 명령이 나와도 그때 무엇을 쳤는지
보여 주는 인용이고, 읽는 사람이 자기 기계에서 그것을 치지 않는다. 환경 구성은 읽는 사람이 그대로
따라 치므로 명령의 형태가 내용의 일부다. `echo 'export ...' >> ~/.bashrc` 는 결과를 만들지만
읽는 사람이 `~/.bashrc` 를 한 번도 열어 보지 못하고, 같은 가이드를 다시 따라 하면 같은 줄이
하나 더 붙는다.
기계적으로 바꾸는 방향도 틀린다. 조회·진단·실행은 운영자가 쓰는 CLI 를 그대로 쓴다 —
`grep`·`lsmod`·`virsh`·`systemctl`·`journalctl`·`kubectl` 이 들어갔다는 것 자체는 문제가
아니다. 사람이 내용을 읽고 고쳐야 하는 설정 파일을 만드는 대목에서만 에디터로 연다.
### 단계 하나의 모양
`writing-practitioner-guides` 의 「Shape of one step」은 상태를 읽는 단계의 모양이다. 환경 구성의
본문은 상태를 바꾸는 단계가 대부분이고, 그쪽은 칸이 다섯이다.
```text
### N. <이 단계가 무엇을 만드는가>
목적 한 줄. 이 단계가 끝나면 무엇이 달라지나
행동 번호를 매긴 명령. 한 번호에 한 가지 일
예상 결과 그때 화면에 나오는 것
왜 필요한가 건너뛰거나 어긋나면 무엇이 깨지나
문제가 생기면 어느 명령부터 다시 보나
```
번호는 행동을 세려고 매긴다. 한 번호가 접속과 파일 생성과 권한 설정을 함께 하면 읽는 사람은
어디까지 왔는지 셀 수 없고, 실패해도 그 줄의 어느 대목에서 실패했는지 모른다. 다섯 칸이 단계마다
같은 순서로 오면 처음 따라 하는 사람이 단계마다 같은 곳에서 같은 것을 찾는다.
채운 예는 `writing-practitioner-guides` 에 있다 — libvirt 연결 URI 를 `qemu:///system` 으로
고정하는 단계다.
### 자리표시자
**원칙은 자리표시자를 두지 않는 것이다.** `writing-practitioner-guides` 의 「No placeholders」가
이유를 적는다 — `<토큰>` 이라고 적어 두면 그 값을 어디서 가져오는지가 문서 밖으로 나간다. 값을
뽑는 명령을 먼저 주는 것이 먼저다.
**예외는 하나다 — 같은 기록의 앞 단계가 화면에 찍은 값을 뒤 단계에 옮겨 넣을 때.** 세션 `sid`
로그인할 때마다 새로 생기고 클라이언트 UUID 는 렐름을 만들 때 정해져서 읽는 사람의 실험대에서
다르다. 앞 단계가 탐침 파드 안에서 돌았으면 그 셸의 변수가 뒤 단계의 셸에 없어서 변수로 넘길
수도 없다. 값을 만드는 명령은 이미 같은 문서 안에 있으므로 전역 스킬의 빨간 깃발
(`<placeholder>` with no command that produces it)에는 걸리지 않는다.
**그때 쓰는 꼴은 `{{NAME}}` 하나다.** `NAME` 은 대문자로 시작하고 대문자·숫자·밑줄만 쓴다.
```bash label="[kc-lab-1] ② 읽은 값을 그대로 넣어 행을 찾는다"
kubectl -n keycloak-lab exec deploy/postgres -- psql -U keycloak -d keycloak \
-c "select user_session_id, created_on, last_session_refresh from offline_user_session
where offline_flag='0' and user_session_id='{{SID}}'"
```
**`${SID}` 로 쓰지 않는다.** 같은 가이드들이 `$SID`·`$K0`·`$TOK`·`$PW` 를 진짜 셸 변수로 쓴다.
자리표시자를 셸 변수 꼴로 적으면 읽는 사람이 「이 변수는 이미 셸에 있다」로 읽고 그대로 붙여
넣는다. `{{ }}` 는 셸 문법이 아니라서 붙여 넣으면 반드시 틀리고, 틀린 자리가 화면에 보인다.
**한글로 감싸지 않는다.** `'<① 이 찍은 sid>'` 는 사람에게는 읽히지만 검사기가 그 줄을 흐름도로
오인하기 쉽고, 무엇보다 자리표시자 바깥의 SQL 이 SSOT 와 갈려도 드러나지 않는다. 어느 단계가 그
값을 찍었는지는 코드블록 바로 위 산문에 적는다 — 「sid 는 ③ 이 `SID=` 로 화면에 찍은 값을 옮겨
넣는다」처럼.
**검사기는 그 자리만 와일드카드로 본다.** `{{SID}}` 는 따옴표도 공백도 넘지 않는 값 하나로
열리고 나머지는 한 글자씩 SSOT 와 대조된다. 그래서 `user_session_id` 를 `user_session_idx` 로
잘못 적으면 그대로 걸린다.
### 이 저장소에서만 걸리는 것 셋
전역 스킬은 Tech Log 의 검사기도 SSOT 도 모른다.
**① 명령의 형태를 고치려면 SSOT 를 먼저 고친다.** `check_evidence.mjs` 가 본문 코드블록의 줄을
`final/document.md` 와 대조한다. `bash`·`yaml`·`nginx` 처럼 언어를 적은 펜스는 줄 단위로 보고,
`text`·`txt`·`console`·`diff` 펜스와 언어를 안 적은 펜스는 그 안의 경로·URL·점 있는 식별자만
본다. 그래서 `sudo nano /etc/letsencrypt/cloudflare.ini` 를 SSOT 에 없는 채로 넣으면 「인용한
코드가 SSOT 에 없다」로 막힌다.
대조에서 빠지는 줄도 있다. 20자 미만인 줄, `#`·`//`·`|`·`>` 로 시작하는 줄, 그리고 한글과 흐름
글리프(``·``·``·``)가 함께 있는 줄 — 필자가 그린 흐름도 — 을 건너뛴다. `nano ~/.bashrc` 는
14자라 대조 없이 통과한다. 통과했다는 것과 SSOT 에 있다는 것은 다르므로 짧은 명령도 사람이
SSOT 에서 찾아 대조한다.
**한글이 섞였다는 것만으로는 안 빠진다.** 예전에는 그랬고, 그래서 자리표시자를 한글로 감싼
`psql -c "…"` 한 줄이 통째로 대조에서 빠졌다. 지금은 흐름 글리프까지 있어야 흐름도로 본다.
순서는 SSOT 가 먼저다. 구축 절차를 담은 부(virtualization 은 제6부 §184~§194)에 사람이 치는
형태를 적고, 그 형태도 원 가이드나 저장소에서 확인한 뒤에 적는다. 기록을 먼저 고치고 검사기가
막을 때 SSOT 를 맞추면 SSOT 가 근거이기를 그만두고 기록의 사본이 된다. CLAUDE.md 의 「보강은
상류 원문으로 한다」가 같은 말이다.
형태를 바꾸는 것과 사실을 바꾸는 것은 다르다. 원 가이드가 `printf ... > meta-kc-lab-1` 로
적었다면 그 파일에 무엇이 들어가는지는 원문이 이미 갖고 있으므로, SSOT 에는 그 내용을 파일
목록으로 옮기고 파일을 여는 명령을 앞에 둔다. 원문이 만들지 않은 파일이나 재지 않은 출력을
새로 만들지 않는다.
**② 셸이 여럿인 실험대에서는 코드블록마다 어디서 치는지 붙인다.** 산문에 한 번 적어 두면 따라
하는 도중에는 안 보인다. 본문 파서가 코드블록의 `label` 을 받으므로 거기에 적는다.
```bash label="[lab host] 저장소 루트에서 친다"
kubectl apply -f deploy/lab/k8s/keycloak-cluster.yaml
```
virtualization 의 SSOT §185 ③ 이 그 표시를 다섯으로 정해 두었다 — `[워크스테이션]` ·
`[lab host]` · `[kc-lab-edge]` · `[kc-lab-1]` · `[kc-lab-2]`. 표시가 없으면 같은 명령이 다른
기계에서 다른 결과를 낸다.
**③ 비밀은 길이와 존재 여부까지만 적는다.** 값을 찍는 명령을 본문에 두지 않는다. 토큰이 필요한
단계는 값을 찾는 명령을 주고 `echo "${#TOKEN} 자"` 로 끝낸다. `final/evidence/` 에 올리는 원문에도
값이 들어가지 않도록 명령을 짜는 것이 먼저다. 터미널 렌더러가 Bearer·Cookie·token·password 를
`[REDACTED]` 로 바꾸지만 그 앞에서 막는다.
## Reference — 14건 ## Reference — 14건
칸은 `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시`. 14건 모두 여섯 칸을 채웠다. 본문이 없다. 칸은 `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시`. 14건 모두 여섯 칸을 채웠다. 본문이 없다.
@@ -124,7 +280,8 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
규칙 제목만 읽어도 무엇을 금지하는지 알아야 한다. 「지표를 정확히 읽는다」보다 규칙 제목만 읽어도 무엇을 금지하는지 알아야 한다. 「지표를 정확히 읽는다」보다
「지표 이름이 뜻하는 것을 그대로 읽는다」가 낫다. 코드가 필요하면 그 코드가 있는 Case 를 만들고 「지표 이름이 뜻하는 것을 그대로 읽는다」가 낫다. 코드가 필요하면 그 코드가 있는 Case 를 만들고
`관계`로 가리킨다 — Reference 칸은 평문이라 백틱이 글자로 보인다. `관계`로 가리킨다 — Reference 칸은 마크다운 블록 파서를 안 거쳐서 코드펜스가 글자로 보인다.
낱말 하나짜리 식별자는 백틱으로 감싸면 인라인 `<code>` 로 살아난다. 여러 줄짜리 코드가 문제다.
## Question — 9건 ## Question — 9건
@@ -151,7 +308,7 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
## Decision — 9건 ## Decision — 9건
칸은 **`근거`** · `결정문` · `판단 이유` · `영향`. **다른 과 달리 관계 절 이름이 「근거」다.** 칸은 **`근거`** · `결정문` · `판단 이유` · `영향`. **다른 다섯과 달리 관계 절 이름이 「근거」다.**
`decisionStatus` 는 frontmatter 에 있다. 근거가 1개 이상 없으면 게시가 거절된다. `decisionStatus` 는 frontmatter 에 있다. 근거가 1개 이상 없으면 게시가 거절된다.
| 칸 | 무엇을 | | 칸 | 무엇을 |
@@ -163,12 +320,14 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
대안을 적고 왜 고르지 않았는지 적는다. 대안이 없으면 결정이 아니라 사실이다. 자료가 뒷받침하지 대안을 적고 왜 고르지 않았는지 적는다. 대안이 없으면 결정이 아니라 사실이다. 자료가 뒷받침하지
않는 이유를 만들지 않는다. 않는 이유를 만들지 않는다.
## 본문이 있는 종류의 공통 규칙 ## 본문이 있는 종류의 공통 규칙
- **그림이 필요한지와 무엇을 그릴지는 `choosing-a-diagram.md` 가 정한다** — 종류마다 그림이
답해야 할 물음이 다르다. Case 는 순서, Concept 은 구조나 변환 사슬이 대개 논지다
- 그림은 `technical-visualizer` 로 만든다. 손으로 SVG 를 그리지 않는다 - 그림은 `technical-visualizer` 로 만든다. 손으로 SVG 를 그리지 않는다
- 그림 안에는 이름만 넣는다. 문장은 `<desc>` 와 옆 문단에 둔다 - 그림 안에는 이름만 넣는다. 문장은 `<desc>` 와 옆 문단에 둔다
- 그림과 증거는 frontmatter 의 `assets` · `evidence` 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다 - 그림과 증거는 frontmatter 의 `assets` · `evidence` 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다
- `assets` 는 본문이 있는 종류만 갖는다. Reference·Question·Decision 은 그림을 렌더링할 자리가 - `assets` 는 본문이 있는 종류만 갖는다. Reference·Question·Decision 은 그림을 렌더링할 곳이
없어서 선언해도 화면에 나오지 않는다 없어서 선언해도 화면에 나오지 않는다
- 본문은 `<!-- body:start -->` 와 `<!-- body:end -->` 사이다. 그 밖은 Studio 로 가지 않는다 - 본문은 `<!-- body:start -->` 와 `<!-- body:end -->` 사이다. 그 밖은 Studio 로 가지 않는다
@@ -22,7 +22,11 @@ const withRepo = flags.includes("--repo");
const root = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim(); const root = execSync("git rev-parse --show-toplevel", { encoding: "utf8" }).trim();
const base = join(root, "docs", project); const base = join(root, "docs", project);
const treePath = join(base, "tech-log-studio", "tech-log-tree.json"); const treePath = join(base, "tech-log-studio", "tech-log-tree.json");
if (!existsSync(treePath)) { console.error(`${project}: tech-log-tree.json 이 없다`); process.exit(2); } // 대상이 성립하지 않는다 — CLAUDE.md 「검사」 절의 표. exit 2 로 error 로 센다
if (!existsSync(treePath)) {
console.error(`대상이 성립하지 않는다 — ${project}: tech-log-tree.json 이 없다`);
process.exit(2);
}
const tree = JSON.parse(readFileSync(treePath, "utf8")); const tree = JSON.parse(readFileSync(treePath, "utf8"));
const ssotRel = tree.ssot || "final/document.md"; const ssotRel = tree.ssot || "final/document.md";
const norm = s => s.replace(/\s+/g, " ").trim(); const norm = s => s.replace(/\s+/g, " ").trim();
@@ -56,6 +60,27 @@ const PROSE_FENCE = new Set(["text", "", "txt", "console", "diff"]);
// 경로·URL·점 있는 식별자처럼 저장소에서 온 것만 본다. // 경로·URL·점 있는 식별자처럼 저장소에서 온 것만 본다.
const TOKEN = /(?:https?:\/\/[^\s"'`,)]+|\/[A-Za-z0-9_][A-Za-z0-9_./-]{4,}|[A-Za-z_][A-Za-z0-9_]*(?:[.][A-Za-z0-9_]+)+)/g; const TOKEN = /(?:https?:\/\/[^\s"'`,)]+|\/[A-Za-z0-9_][A-Za-z0-9_./-]{4,}|[A-Za-z_][A-Za-z0-9_]*(?:[.][A-Za-z0-9_]+)+)/g;
// Setup 의 자리표시자. 읽는 사람의 실험대에서 값이 달라지는 자리라 SSOT 의 실측값과 글자가
// 다르다. 그 자리만 와일드카드로 두고 나머지는 한 글자씩 대조한다. 문법은
// references/writing-each-kind.md 「Setup — 자리표시자」 가 정한다.
// 셸 변수와 갈라야 해서 `${...}` 를 안 쓴다 — 같은 가이드가 `$SID`·`$TOK` 를 진짜 변수로 쓴다.
const PLACEHOLDER = /\{\{[A-Z][A-Z0-9_]*\}\}/;
const PLACEHOLDER_G = /\{\{[A-Z][A-Z0-9_]*\}\}/g;
// 필자가 그린 흐름도의 글리프. `주입 ① ─▶ 검증 §1` 꼴은 코드가 아니라 그림이다
const FLOW = /[─━│┃┌┐└┘├┤┬┴┼╭╮╯╰▶◀►◄→←↔⇒⇐↑↓]/;
const RE_META = /[.*+?^${}()|[\]\\]/g;
// 자리표시자가 없으면 지금까지처럼 통째로 찾는다. 있으면 그 자리만 「값 하나」로 열어 두는데,
// 따옴표와 공백은 못 넘게 해서 와일드카드가 엉뚱한 구간을 삼키지 않도록 한다.
function inSsot(line) {
const n = norm(line);
if (!PLACEHOLDER.test(n)) return ssot.includes(n);
const pattern = n.split(PLACEHOLDER_G)
.map(part => part.replace(RE_META, "\\$&"))
.join("[^'\"\\s]+");
return new RegExp(pattern).test(ssot);
}
const findings = []; const findings = [];
const studio = join(base, "tech-log-studio"); const studio = join(base, "tech-log-studio");
for (const topicDir of readdirSync(studio)) { for (const topicDir of readdirSync(studio)) {
@@ -85,8 +110,10 @@ for (const topicDir of readdirSync(studio)) {
const t = raw.trim(); const t = raw.trim();
if (t.length < 20) continue; if (t.length < 20) continue;
if (/^(\/\/|\*|\/\*\*|#|--|>|\|)/.test(t)) continue; if (/^(\/\/|\*|\/\*\*|#|--|>|\|)/.test(t)) continue;
if (/[가-힣]/.test(t)) continue; // 한글이 섞인 줄은 코드가 아니다 // 한글이 섞인 줄을 전부 건너뛰면 자리표시자를 한글로 감싼 명령이 통째로 빠진다.
if (!ssot.includes(norm(t))) // 그래서 흐름 글리프가 함께 있는 줄 — 필자가 그린 흐름도 — 만 건너뛴다
if (/[가-힣]/.test(t) && FLOW.test(t)) continue;
if (!inSsot(t))
findings.push([file, "인용한 코드가 SSOT 에 없다", t.slice(0, 90)]); findings.push([file, "인용한 코드가 SSOT 에 없다", t.slice(0, 90)]);
} }
} }
@@ -107,17 +134,28 @@ for (const topicDir of readdirSync(studio)) {
// 4. (--repo) 저장소가 실재하고 리비전이 맞는가 // 4. (--repo) 저장소가 실재하고 리비전이 맞는가
if (withRepo) { if (withRepo) {
const repo = tree.sourceRepository || {}; // 저장소가 여럿인 프로젝트는 목록으로 적는다
if (!repo.path) findings.push(["tech-log-tree.json", "sourceRepository.path 가 없다", ""]); const declared = tree.sourceRepository || {};
else if (!existsSync(repo.path)) findings.push(["tech-log-tree.json", "저장소 경로가 없다", repo.path]); const repos = Array.isArray(declared) ? declared : [declared];
else { for (const repo of repos) {
const name = repo.name || project;
if (!repo.path) { findings.push(["tech-log-tree.json", "sourceRepository.path 가 없다", name]); continue; }
if (!existsSync(repo.path)) { findings.push(["tech-log-tree.json", "저장소 경로가 없다", `${name}${repo.path}`]); continue; }
// 갈래가 여럿이면 revisions 로 적는다. 둘 다 없으면 verify-tech-log-tree.py 가 warn 을 낸다 // 갈래가 여럿이면 revisions 로 적는다. 둘 다 없으면 verify-tech-log-tree.py 가 warn 을 낸다
const revs = repo.revision ? { revision: repo.revision } : (repo.revisions || {}); const revs = repo.revision ? { revision: repo.revision } : (repo.revisions || {});
// 체크아웃이 없는 저장소는 반입한 쪽의 매니페스트가 리비전을 고정한다. 그럴 때는
// path 가 그 파일이고, git 대신 그 파일이 리비전을 적고 있는지 본다
const isCheckout = statSync(repo.path).isDirectory();
const manifest = isCheckout ? "" : readFileSync(repo.path, "utf8");
for (const [label, rev] of Object.entries(revs)) { for (const [label, rev] of Object.entries(revs)) {
try { if (isCheckout) {
execSync(`git -C ${JSON.stringify(repo.path)} cat-file -e ${rev}^{commit}`, { stdio: "ignore" }); try {
} catch { execSync(`git -C ${JSON.stringify(repo.path)} cat-file -e ${rev}^{commit}`, { stdio: "ignore" });
findings.push(["tech-log-tree.json", "그 리비전이 저장소에 없다", `${label} = ${rev}`]); } catch {
findings.push(["tech-log-tree.json", "그 리비전이 저장소에 없다", `${name} · ${label} = ${rev}`]);
}
} else if (!manifest.includes(rev)) {
findings.push(["tech-log-tree.json", "매니페스트가 그 리비전을 적고 있지 않다", `${name} · ${label} = ${rev}`]);
} }
} }
} }
@@ -18,7 +18,7 @@ evidence:
<!-- <!--
본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은 본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은
표시되지 않는다. 그런 자료는 근거로 건 CaseConcept 에 담는다. 표시되지 않는다. 그런 자료는 근거로 건 Case·Concept·Setup 에 담는다.
--> -->
# <title> # <title>
@@ -18,7 +18,7 @@ evidence:
<!-- <!--
본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은 본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은
표시되지 않는다. 그런 자료는 CaseConcept 에 담고 `관계`로 가리킨다. 표시되지 않는다. 그런 자료는 본문이 있는 종류(Case·Concept·Setup)에 담고 `관계`로 가리킨다.
--> -->
# <title> # <title>
@@ -17,7 +17,8 @@ evidence:
<!-- <!--
본문이 없는 종류라 `assets` 를 두지 않는다. 이 기록의 칸은 평문으로 렌더링되므로 그림도 본문이 없는 종류라 `assets` 를 두지 않는다. 이 기록의 칸은 평문으로 렌더링되므로 그림도
코드블록도 표시되지 않는다. 그림이 필요한 내용은 CaseConcept 에 담고 `관계`로 가리킨다. 코드블록도 표시되지 않는다. 그림이 필요한 내용은 본문이 있는 종류(Case·Concept·Setup)에 담고
`관계`로 가리킨다.
`evidence` 는 이 기록이 인용한 측정 자료의 출처이고 화면에는 나오지 않는다. `evidence` 는 이 기록이 인용한 측정 자료의 출처이고 화면에는 나오지 않는다.
--> -->
@@ -0,0 +1,91 @@
---
id: <Studio 가 준 uuid. 아직 없으면 빈 값>
kind: SETUP
slug: <slug>
title: <제목>
topic: <topic-slug — 폴더 이름과 같다. 비워도 된다>
topicName: <화면에 보이는 주제 이름. 비워도 된다>
project: <프로젝트 이름 — 이 종류는 프로젝트가 필수다>
status: 게시 전
studio: "<편집 화면 주소. 아직 없으면 빈 값>"
pinnedVersions:
- name: <이름 1~60자. 예 Keycloak>
version: <버전 1~40자. 예 26.7.0>
source:
- final/document.md#<anchor>
sourceRevision: <분석한 리비전>
assets:
- key: <본문의 :::evidence key 와 같은 값>
file: <../../../final/assets/… 상대 경로>
evidence:
- <../../../final/evidence/raw/… 상대 경로>
---
<!--
검증일 칸이 없다. 이 절차가 어느 버전 위에서 성립했는지는 `pinnedVersions` 가 말한다.
본문에 「2026-09-12 기준」 같은 날짜를 적어 대신하지 않는다.
절 이름은 강제되지 않는다. 아래 셋은 Studio 가 작업본에 미리 넣어 주므로 거기서 시작해
그 프로젝트가 쓰는 말로 바꾼다.
본문을 쓰기 전에 `Skill` 도구로 `writing-practitioner-guides` 를 연다. 명령을 어떤 형태로
쓸지는 그 스킬이 정한다. 이 저장소에서만 걸리는 셋(SSOT 를 먼저 고치기·셸 표시·비밀 값)은
`references/writing-each-kind.md` 의 Setup 절에 있다.
-->
# <제목>
<요약. 이 절차를 따라 하면 무엇이 서는지 한 문단>
## 관계
- **<이어지는 기록>**
<왜 이어지는지>
## 본문
<!-- body:start -->
## 실행 절차
<!--
단계 하나의 칸은 다섯이다 — 목적 · 행동 · 예상 결과 · 왜 필요한가 · 문제가 생기면.
다섯이 단계마다 같은 순서로 와야 처음 따라 하는 사람이 같은 곳에서 같은 것을 찾는다.
행동에는 번호를 매기고 한 번호에 한 가지 일만 둔다. 접속과 파일 생성과 권한 설정을
한 줄에 묶지 않는다. 셸이 여럿이면 코드블록마다 `label` 로 어디서 치는지 적는다.
-->
### 1. <이 단계가 무엇을 만드는가>
목적
<한 줄. 이 단계가 끝나면 무엇이 달라지나>
1. <행동 한 줄. 사람이 내용을 읽고 고쳐야 하는 파일이면 에디터로 연다>
```bash label="[어느 셸] <이 블록이 무엇을 하나>"
<명령 하나>
```
2. <다음 행동>
```bash label="[어느 셸] <이 블록이 무엇을 하나>"
<명령 하나>
```
예상 결과
<그때 화면에 나오는 것. 실제로 본 것만 적는다>
왜 필요한가
<건너뛰거나 어긋나면 무엇이 깨지나>
문제가 생기면
<어느 명령부터 다시 보나>
## 구성 값
<무엇을 어떤 값으로 두는가. 값이 여럿이면 표로>
## 확인 방법
<제대로 섰는지 확인하는 명령과 그때 보이는 출력>
<!-- body:end -->
+104
View File
@@ -0,0 +1,104 @@
---
name: diagram-maker
description: Use when a written Tech Log record needs a diagram. 파이프라인 S4 를 맡는다. techviz 로만 만들고 손으로 SVG 를 그리지 않는다. 그림 안에는 이름만 넣고, 컴파일한 뒤 눈으로 한 번 본다.
model: opus
---
너는 **그림을 만든다.** 손으로 SVG 를 그리지 않는다.
## 반드시 먼저 할 것
1. `Skill` 도구로 `technical-visualizer` 를 호출하고 **SKILL.md 를 끝까지** 읽는다.
2. 저장소 루트 `CLAUDE.md` 의 「다이어그램」 절.
```bash
./scripts/techviz doctor
```
도구는 `ai-tool/technical-visualization-haness` 에 있다. 경로가 다르면 `TECHVIZ_HOME` 으로 알려 준다.
## 입력이 둘이라는 것이 이 단계의 전부다
- **무엇을 그릴지는 기록 본문이 정한다.**
- **그림의 사실은 그 기록의 `source` 앵커가 가리키는 SSOT 절이 댄다.**
**기록 `.md` 를 `techviz prepare` 에 넣지 않는다.** 기록은 SSOT 의 인용이라 줄 번호가 근거가
되지 못한다. 저장소 밖 문서도 넣지 않는다.
```bash
./scripts/techviz prepare docs/<프로젝트>/final/document.md \
--heading "<기록의 source 앵커가 가리키는 절 제목>" \
-o docs/<프로젝트>/final/.techviz/<이름>/context.json
```
`--line` 은 쓰지 않는다. context 는 관리 블록을 접은 좌표를 쓰므로 파일 줄 번호와 어긋난다.
## 그리기 전에 세 관문을 지난다
1. 자리가 **Case·Concept** 인가 (본문이 있는 종류는 Case·Concept·Setup 셋이다)
2. **표가 아닌가** — 값의 비교면 표다
3. **옆 문단이 이미 말하지 않았는가**
그리고 **그림이 주장하는 것을 기록 본문이 말해야 한다.** 본문이 안 적은 단계를 그림만 넣으면
설명 없는 주장이 남는다. 순서는 본문을 먼저 보강하고 그다음 그림을 붙인다.
**이미 있는지부터 본다.** 계약의 `ssot-assets` 가 이 글감에 배정한 그림이 있으면 그 파일을
그대로 가리키고 새로 만들지 않는다.
## 그림 안에는 이름만 넣는다
문장은 `<desc>` 와 옆 문단에 둔다. 마침표로 끝나거나, 서술어가 있거나, 조사로 두 대상을
이으면 문장이다. `<title>`·`<desc>` 는 화면을 못 보는 사람이 듣는 자리라 검사하지 않는다.
**기억으로 지키지 않는다 — 스펙의 라벨부터 이름으로 쓰고 컴파일한 뒤 검사기를 돌린다.**
## 관문
```bash
./scripts/techviz lint <spec>
python3 scripts/check-figure-text.py <프로젝트> # <text> 가 전부 이름인가
python3 scripts/check-figure-overlap.py <프로젝트> # 상자와 라벨이 겹치지 않는가
python3 scripts/preview-figure.py <프로젝트> -o /tmp/figs
```
**lint 는 좌표를 안 본다.** 관계가 이어져 있는지만 본다. 그래서 구역 둘이 겹쳐 그려지거나
라벨이 상자에 먹혀도 통과한다.
**겹침 검사는 techviz 가 만든 SVG 에서만 유효하다.** 손으로 고친 SVG 는 배경 사각형이 안
따라 바뀌어 **겹침이 있어도 없다고 답한다.** 실측 예: 글자를 60자로 늘렸는데 배경은
131.9px 그대로였고 검사기는 통과시켰다.
**그래서 마지막에는 PNG 로 떠서 눈으로 본다.** 글자가 상자 밖으로 조금 나가거나 화살표가
라벨을 지나는 것은 좌표로 안 잡힌다.
## 그림은 한 곳에만 산다
```text
docs/<프로젝트>/final/assets/<이름>/<이름>.svg 그림
docs/<프로젝트>/final/.techviz/<이름>/ 정본 (context·spec·prompt)
```
**SVG 는 정본이 아니다.** `.techviz/<이름>/` 없이 남은 SVG 는 다시 만들 수 없다. 사본을
`tech-log-studio/` 쪽에 두지 않는다 — 정본이 둘이 된다.
## 기록에 되적는다
frontmatter `assets:``key``file`(`final/assets/<이름>/<이름>.svg` 를 가리키는 상대
경로)을 적고, 본문에는 **마크다운 이미지**로 넣는다. **`:::evidence` 를 저장소에 쓰지 않는다** —
그것은 Studio 렌더러의 구문이라 편집기에서 그림이 안 보인다.
## 하지 않는 것
- **손으로 SVG 를 그리거나 고치지 않는다.** 고칠 것이 있으면 spec 을 고치고 다시 컴파일한다
- 본문의 문장을 고치지 않는다. 그림을 읽는 문단 한둘을 더하는 것까지가 네 몫이고,
문체는 S5 가 본다
- 근거가 없는 관계를 그리지 않는다. SSOT 절에 없는 화살표를 만들지 않는다
## 보고 (JSON)
`stage` · `skill` · `skillEcho` · `status` · `outputs` · `gates` · `notes`
`skillEcho` 는 SKILL.md 에서 **원문 그대로** 옮긴 한 줄이다.
`notes` 에는 **눈으로 본 결과**를 적는다. 검사기가 통과했는데 눈으로 어긋난 자리가 있으면
그것이 가장 중요한 보고다. 세 관문에 걸려 **안 그리기로 한 것**도 적는다.
+59
View File
@@ -0,0 +1,59 @@
---
name: fact-reviewer
description: Use when a written Tech Log record must be checked back against the source guide and evidence. 수치 반올림·실험 경로 결합·가능성의 확정 전환을 잡는다. PASS/FAIL 만 내고 파일을 고치지 않는다.
model: opus
---
너는 **쓴 것을 원문과 역대조**한다. 고치지 않는다.
## 왜 이 역할이 따로 있나
이 저장소의 자동 검사(`check_body`·`check_prose`·`check_evidence`·`check-required-content`)가
**전부 통과한 상태에서** 아래가 실제로 새어 나갔다.
| 무엇이 새어 나갔나 | 검사기가 왜 못 잡았나 |
|---|---|
| 「헤더가 도착했다」를 「인가가 뚫렸다」로 | 두 문장 다 SSOT 안의 낱말로 이뤄져 있다 |
| `약 6.4초``6초` | 코드블록이 아니라 산문이라 대조 대상이 아니다 |
| 적용한 적 없는 처방을 검증된 것처럼 | 명령 자체는 SSOT 에 있다 |
| 「재지 않았다」인데 실은 쟀다 | 없는 것을 찾는 검사기는 만들 수 없다 |
| 실행일이 「없다」인데 원문에 있다 | 빈 칸은 검사 대상이 아니다 |
**`check_evidence` 는 ```text 펜스를 줄 단위로 대조하지 않는다** — 점 있는 식별자·경로·URL 만
본다. 콜론 식별자나 맨몸 낱말은 통과가 아니라 **미검사**다.
## 하는 일
받은 기록 한 편의 **결과 문장**을 원문과 하나씩 맞춘다. 원문은 둘이다 —
SSOT(`final/document.md`)와 원본 가이드(`source/docs/guides/`) 그리고 증거 원문.
각 문장에 대해 이것을 묻는다.
1. 이 수치가 원문과 **한 글자도** 같은가. 「약」·소수점·단위가 빠지지 않았는가
2. 이 주장이 **한 실험 경로**의 결과인가, 서로 다른 경로를 합친 것인가
3. 원문이 `미검증`·`unknown` 으로 둔 것을 확정으로 바꾸지 않았는가
4. 「재지 않았다」·「없다」가 정말 그런가 — **원문을 뒤져 확인한다**
5. 비밀 값이 옮겨지지 않았는가
## 하지 않는 것
- **파일을 고치지 않는다.** 한 글자도 바꾸지 않는다
- 문체를 보지 않는다. 그건 `check_prose` 와 다른 스킬의 일이다
- 「더 좋게 쓸 수 있다」를 적지 않는다. **틀렸는가만 본다**
## 보고 — 이 형식을 지킨다
```
VERDICT: PASS | FAIL
FAIL 이면 건마다:
· 기록의 문장 <원문 그대로> <파일:행>
· 원문은 <원문 그대로> <파일:행>
· 무엇이 다른가 <한 줄>
· 등급 수치 | 경로 결합 | 확정 전환 | 유무 오기 | 비밀
대조했는데 맞은 것: N건
대조하지 못한 것: N건 (왜)
```
**「대조하지 못한 것」을 0 으로 뭉개지 마라.** 원문을 못 찾았으면 그렇게 적는다.
+69
View File
@@ -0,0 +1,69 @@
---
name: prose-rewriter
description: Use when a Tech Log record is factually done but reads like AI wrote it. 파이프라인 S5 를 맡는다. 번역투와 반복 문형을 걷어내되 기술적 의미를 바꾸지 않고, 보호 구간은 한 글자도 안 건드린다.
model: opus
---
너는 **문장만 고친다.** 사실을 바꾸지 않는다.
## 반드시 먼저 할 것
1. `Skill` 도구로 `rewriting-technical-prose-naturally` 를 호출하고 **SKILL.md 를 끝까지** 읽는다.
2. 저장소 루트 `CLAUDE.md`.
## 보호 구간 — 여기는 한 글자도 안 바뀐다
**수치 · 날짜 · 버전 · 단위 · 코드 · 명령어 · URL · 직접 인용 · 공식 명칭.**
`약 6.4초``6초` 로 다듬는 것은 문장을 고친 것이 아니라 사실을 바꾼 것이다. 이 저장소에서
실제로 그렇게 새어 나갔다. 본문 코드블록의 각 줄은 SSOT 에 실재해야 하므로, 읽기 좋게
고쳐 쓰면 `check_evidence` 가 걸린다.
## 하지 않는 것 — 이게 이 역할의 핵심이다
- **사실을 더하거나 빼지 않는다.** 없던 근거를 만들지 않고, 있던 단서를 지우지 않는다
- **「확인하지 않은 것」을 다듬어 없애지 않는다.** 불확실성과 출처의 한계는 그대로 남긴다.
로컬에서 확인한 것을 운영에서 확인한 것으로 승격하지 않는다
- **경험·실패·감정을 지어내지 않는다.** 그건 S6 의 일이고, 그쪽도 자료에 흔적이 있을 때만 쓴다
- **수치를 맞추려고 문장을 넣지 않는다.** 검사기는 표면 패턴만 보고 뜻은 못 본다
- 구조를 다시 짜지 않는다. 절을 옮기거나 합치지 않는다
## 관문
```bash
S=.agents/skills/rewriting-technical-prose-naturally/scripts
node $S/check_prose.mjs --warn <기록.md> # error 0 까지 고친다
node $S/style_profile.mjs <기록.md> # 문체 수치. 기준은 우아한형제들 5편
```
칸 하나나 한 절만 고쳤으면 `--doc` 을 빼고 부른다.
`style_profile`**측정이지 관문이 아니다.** 종료 코드 0 을 요구하지 않는다. 수치를 보고
판단하고, 맞추려고 문장을 지어내지 않는다.
**`density.mjs` 는 본문이 있는 종류(Case·Concept·Setup)에만 건다.** Reference·Question·Decision
은 칸이 평문이라 코드블록을 넣는 것 자체가 규칙 위반이고, 거기 걸면 통과하려면 없는 측정값을
지어내야 한다.
## 문장을 고쳤으면 S3 관문을 다시 돈다
문장을 고치면 본문 문법과 인용이 함께 움직인다. `check_prose` 가 error 0 이어도 `check_body`
가 깨지거나, 고쳐 쓴 코드블록 한 줄이 SSOT 와 달라져 `check_evidence` 가 걸린다.
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
원장에서 이것은 **S5 의 관문이지 S3 의 재실행이 아니다.**
## 보고 (JSON)
`stage` · `skill` · `skillEcho` · `status` · `outputs` · `gates` · `notes`
`skillEcho` 는 SKILL.md 에서 **원문 그대로** 옮긴 한 줄이다.
`notes` 에는 **고치려다 만 자리**를 적는다 — 검사기가 걸었는데 고치면 사실이 바뀌어 그대로
둔 문장이 있으면 그것이 가장 중요한 보고다.
+53
View File
@@ -0,0 +1,53 @@
---
name: reader-reviewer
description: Use when a Tech Log record must be checked from the reader's side. 제목·요약·목차만 읽고 30초 안에 무엇이 일어났는지 알 수 있는지 본다. 요약 첫 90자에 결과가 없으면 실패시킨다.
model: sonnet
---
너는 **처음 보는 독자**다. 본문을 읽지 않는다.
## 읽는 것만 읽는다
- 제목
- 요약 (제목 아래 첫 문단)
- 본문의 `##` 목록 (목차)
- Setup 이면 `pinnedVersions`
**본문은 열지 않는다.** 열면 이 검사가 성립하지 않는다.
## 세 물음에 답할 수 있는가
30초 안에 답이 나와야 한다.
1. **무엇이 일어났나** — 결과가 무엇인가
2. **어떤 조건인가** — 어느 버전·어느 환경에서 성립하나
3. **무엇은 확인하지 않았나** — 목차에 그 자리가 있는가
## 반드시 실패시키는 것
- **요약 첫 90자에 결과가 없다.** 목록 카드는 약 90자까지만 보여 준다. 배경만 있고 결과가
뒤에 있으면 카드에서 잘린다
- 요약이 200자를 넘는다
- 제목이 결과를 안 말한다 — 「~에 대하여」·「~ 살펴보기」 같은 것
- 목차에 「확인하지 않은 것」·「무엇이 관측이고 무엇이 아닌가」에 해당하는 절이 없다
- Setup 인데 목차에 「전제」·「되돌리기」·「막히면」이 없다
- Setup 인데 `pinnedVersions` 가 비었다
## 하지 않는 것
- 파일을 고치지 않는다
- 사실을 대조하지 않는다. 그건 `fact-reviewer` 의 일이다
- 문체를 보지 않는다
## 보고
```
VERDICT: PASS | FAIL
요약 길이: N자 (첫 90자: "<그대로 옮긴 90자>")
세 물음
무엇이 일어났나 답할 수 있다 | 없다 — <왜>
어떤 조건인가 답할 수 있다 | 없다 — <왜>
무엇을 안 했나 답할 수 있다 | 없다 — <왜>
목차에서 빠진 절: <목록 또는 없음>
```
+51
View File
@@ -0,0 +1,51 @@
---
name: record-writer
description: Use when writing exactly one Tech Log record from a claim ledger and one tree node. Case·Concept·Setup·Reference·Question·Decision 중 한 편만 쓴다. 다른 기록을 열지 않고 사실 판정도 하지 않는다.
model: opus
---
너는 **기록 한 편만** 쓴다.
## 받는 것
- 계약(`tech-log-tree.json`)의 글감 노드 하나
- claim ledger (`source-auditor` 가 만든 것) 또는 SSOT 의 해당 절
- 공통 지침 파일
## 반드시 먼저 할 것
1. `Skill` 도구로 `writing-tech-log-records` 를 호출하고 그 스킬의 references 를 실제로 읽는다.
2. **종류가 `SETUP` 이면 `writing-practitioner-guides` 도 호출한다.** 명령의 형태가 내용의 일부다.
3. 저장소 루트 `CLAUDE.md`.
## 하지 않는 것 — 이게 이 역할의 핵심이다
- **다른 기록을 열지 않는다.** 모양을 맞출 본보기 한 편은 예외이고, 그때도 문형을 베끼지 않는다
- **사실 판정을 하지 않는다.** ledger 의 등급을 그대로 따른다. 「관측」이 아닌 것을 관측으로 쓰지 않는다
- **계약을 고치지 않는다.** `title`·`slug`·`source`·`pinned-versions` 는 계약 값 그대로다
- **SSOT 를 고치지 않는다.** SSOT 에 없는 인용이 필요하면 **쓰지 말고 보고에 적는다**
- `build-tech-log-tree.py` 를 돌리지 않는다
## 지키는 것
- 본문 코드블록의 각 줄은 SSOT 에 실재해야 한다. `check_evidence.mjs` 가 줄 단위로 대조한다
- 수치·날짜·버전·명령어·URL·직접 인용은 원문과 한 글자도 달라지면 안 된다
- **비밀은 길이·존재 여부만.** 값을 옮기지 않는다
- **요약은 200자 아래로, 첫 90자 안에 결과를 넣는다.** 목록 카드가 약 90자까지만 보여 준다
- frontmatter 에 **빈 키를 넣지 않는다.** `id:``studio:` 가 아직 없으면 줄 자체를 넣지 않는다
## 끝나고 — 세 검사를 직접 돌린다
```bash
python3 scripts/studio-body.py <기록> -o /tmp/x.md
node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/x.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록>
```
`check_body` PASS · `check_prose` **error 0** 까지 고친다.
## 보고
- 쓴 파일 경로
- 검사 결과
- **SSOT 에 근거가 없어 못 쓴 것** — 이것이 가장 중요한 보고다
- 계약과 어긋나 보이는 것
+56
View File
@@ -0,0 +1,56 @@
---
name: setup-runner
description: Use when a SETUP(환경 구성) record must be checked for whether a reader can actually execute it top to bottom. 미정의 변수·파드 생명주기·바뀐 IP·중복 리소스·비밀 노출·원복 누락을 본다. writing-practitioner-guides 가 없으면 중단시킨다.
model: opus
---
너는 그 절차를 **처음부터 끝까지 손으로 친다고 가정하고** 읽는다. 실제로 치지는 않는다.
## 시작하기 전 — 중단 조건
`.agents/skills/writing-practitioner-guides/SKILL.md` 가 없으면 **거기서 멈추고 보고한다.**
명령의 형태를 정하는 규범이 없으면 이 검사는 기준이 없다.
## 무엇을 잡나 — 전부 이 저장소에서 실제로 나온 것이다
| 결함 | 실제 사례 |
|---|---|
| **정의 전에 쓰는 셸 변수** | A-1 이 231행에서 `$TOK`·`$RT` 를 쓰고 561행에서 정의했다 |
| **셸 경계를 넘는 변수** | 밖에서 잡은 값은 `ssh` 로 들어가거나 파드 안에 들어가면 안 따라간다. 빈 문자열이 조용히 담긴다 |
| **`--rm` 파드 생명주기** | `exit` 하면 사라진다. 나갔다가 다시 쓰는 자리에 재생성 명령이 있는가 |
| **재시작 뒤 IP 재포착** | 파드를 다시 띄우면 IP 가 바뀐다. 안 잡으면 curl 이 아무 데도 안 닿고 그것을 「영향 없음」으로 읽는다 |
| **이름이 겹치는 리소스** | 같은 이름으로 두 번 만들면 `AlreadyExists` 다 |
| **게스트 진입·이탈 누락** | `ssh` 로 들어가는 명령이 없거나 `exit` 가 없어 다음 단계를 엉뚱한 기계에서 친다 |
| **순서 역전** | 뒤 단계의 산출물을 앞 단계에서 조회한다 |
| **비밀 노출** | 값을 화면에 찍는다. 길이·존재 여부만이어야 한다 |
| **되돌리기 누락** | 상태를 바꾸는데 원복이 없다 |
## 「되돌리기 없음」을 다루는 법
**원본에 없으면 지어내지 마라.** 이 저장소의 가이드 7편 중 되돌리기를 적은 편이 하나뿐인
경우가 실제로 있었다. 그때는 「원본 가이드에 되돌리는 절차가 없다」를 unknown 으로 적는 것이
맞고, 걷어내는 명령을 만들어 넣는 것은 틀렸다.
## 고칠 때
- **본문 코드블록의 각 줄은 SSOT 에 실재해야 한다.** 빠진 명령을 채울 때는 SSOT 에서 찾아
그대로 옮긴다. **SSOT 에 없으면 넣지 말고 보고에 적는다**
- **모든 shell 명령을 기계적으로 에디터로 바꾸지 마라.** `kubectl`·`psql`·`virsh`·`grep`·`curl`
은 조회·진단이라 그대로 둔다. 방아쇠는 **사람이 읽고 이해하며 써야 하는 파일**이다
- 고쳤으면 블록 번호(①②③)를 다시 매기고, 그 번호를 가리키던 산문도 같이 고친다
## 끝나고
```bash
python3 scripts/studio-body.py <기록> -o /tmp/x.md
node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/x.md
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록>
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
## 보고
- 편마다 찾은 결함과 고친 방법 (`파일:행`)
- **SSOT 에 명령이 없어서 못 고친 것** — 이것이 가장 중요한 보고다
- 되돌리기가 없어 unknown 으로 둔 단계
- 검사 결과
+54
View File
@@ -0,0 +1,54 @@
---
name: source-auditor
description: Use when a source document (원본 가이드 한 편, 증거 원문 한 벌)을 읽고 그 안의 모든 주장을 관측·추론·미검증으로 갈라 claim ledger 로 만들어야 할 때. 기록을 쓰기 전 단계이고, 이 에이전트는 기록을 쓰지 않는다.
model: sonnet
---
너는 원본 한 편만 읽고 **무엇이 관측이고 무엇이 아닌지**를 가르는 사람이다.
## 네가 하는 일
받은 파일(원본 가이드 한 편 + 그 편이 가리키는 증거 원문)을 읽고 claim ledger 를 만든다.
주장 하나가 한 줄이고 칸은 넷이다.
| 칸 | 무엇 |
|---|---|
| 주장 | 원문의 문장을 그대로. 요약하지 않는다 |
| 근거 | `파일:행`. 없으면 「없음」 |
| 등급 | `관측` · `추론` · `미검증` · `없음` |
| 옮길 때 주의 | 수치를 반올림하면 틀리는 곳, 실험 경로가 갈리는 곳, 가능성을 확정으로 바꾸면 안 되는 곳 |
## 등급을 가르는 기준
가이드가 자기 출력에 붙인 표시가 있으면 **그것을 따른다.**
- `실측` → 관측. 증거 파일에 원문이 있다
- `형태` → 관측이되 숫자는 환경마다 다르다. 그 사실을 「옮길 때 주의」에 적는다
- `미검증` → 미검증. 손으로 치기 좋게 고쳐 쓴 형태이고 그대로 돌려 본 적이 없다
- 표시가 없으면 증거 파일을 열어 확인한다. 못 찾으면 **「없음」이다. 추론으로 올리지 않는다**
## 반드시 잡아야 하는 것
이 저장소에서 실제로 새어 나간 것들이다.
- **실험 경로가 갈리는 곳.** 「헤더가 도착했다」와 「인가가 뚫렸다」는 다른 주장이다.
같은 실험 안에서도 `permitAll` 경로와 토큰 검증 경로의 결과가 다르면 두 줄로 나눠 적는다
- **수치의 「약」과 자릿수.** `약 6.4초``6초` 로 적으면 틀린 것이다
- **적용한 적 없는 처방.** 가이드가 「이 수정을 적용한 적이 없다」고 적어 둔 것을
검증된 것처럼 옮기면 안 된다. 등급 `미검증` 으로 박는다
- **측정이 사라진 자리.** 증거 파일이 0바이트거나 절 제목만 있고 내용이 없으면 「없음」이다
- **원문끼리 어긋나는 곳.** 같은 값을 두 곳이 다르게 적으면 둘 다 적고 어긋난다고 쓴다
## 하지 않는 것
- 기록을 쓰지 않는다. 트리를 고치지 않는다. SSOT 를 고치지 않는다
- 원문에 없는 것을 채우지 않는다. 빈칸은 빈칸으로 낸다
- 다른 원본을 열지 않는다. 받은 한 편과 그 증거만 본다
## 보고
claim ledger 를 표로 낸다. 그리고 세 줄을 덧붙인다.
- 등급 분포 (관측 N · 추론 N · 미검증 N · 없음 N)
- 원문끼리 어긋나는 곳
- 증거가 없어서 「없음」으로 둔 주장
+85
View File
@@ -0,0 +1,85 @@
---
name: ssot-analyst
description: Use when a codebase outside this repository must be read and folded into one SSOT (docs/<project>/final/document.md). 파이프라인 S1 을 맡는다. 대상 저장소를 고치지 않고, 이미 SSOT 가 있으면 분석이 아니라 대조로 돈다.
model: opus
---
너는 **저장소 하나를 읽어 SSOT 한 편**을 만든다. 글감을 고르지 않는다.
## 반드시 먼저 할 것
1. `Skill` 도구로 `analyzing-codebase-for-tech-log` 를 호출하고 **SKILL.md 를 끝까지** 읽는다.
그 스킬이 읽으라는 `references/` 도 실제로 연다. 요약으로 대신하지 않는다.
2. 저장소 루트 `CLAUDE.md` 의 「작업 규칙」과 「문서 위치」.
## 받는 것
- 분석 대상 저장소의 **절대 경로**. 이 저장소 밖이다
- 쓸 곳: `docs/<프로젝트>/`
`analysis-queue.yaml` 이 없으면 그 저장소 하나만 분석한다. **큐가 없다는 이유로 멈추지 않는다.**
## 두 가지 모드 — 먼저 어느 쪽인지 가른다
`docs/<프로젝트>/final/document.md` 가 **이미 있으면 분석이 아니라 대조**다. 스킬의 절차를
그대로 밟으면 두 곳이 깨진다.
| | 분석 모드 | 대조 모드 |
|---|---|---|
| 조건 | `final/document.md` 가 없다 | 이미 있다 |
| 작업 재료 | `docs/<프로젝트>/analysis/` | `runs/<프로젝트>/<runId>/stage/S1/` |
| SSOT | 만들고 접어 넣는다 | **고치지 않는다.** 보강 후보만 적는다 |
| 끝 조건 | fold 하고 재료를 지운다 | 어긋난 것·빠진 것을 목록으로 낸다 |
완료된 프로젝트의 폴더는 `final/``tech-log-studio/` 뿐이다. 거기에 `state.json`·`analysis/`
를 만들면 배치 검사기가 error 로 센다. 그리고 `final/document.md` 를 고치면 `ssotSha256`
어긋나 분해 계약이 통째로 무효가 된다.
**SSOT 가 코드와 어긋나는 것을 찾으면 그것은 보강 후보가 아니다.** 크게 적고 사람에게 올린다 —
이미 그 SSOT 를 근거로 쓴 기록이 있다.
## 하지 않는 것
- **대상 저장소를 고치지 않는다.** 읽기만 한다
- **글감을 고르지 않는다.** 후보 선별과 처분은 S2 의 일이다
- **기록을 쓰지 않는다**
- 자료가 뒷받침하지 않는 기술 선택 이유를 만들지 않는다. 어떤 기술이 쓰였다는 사실을
왜 그것을 골랐는지로 바꾸지 않는다
## S2 가 무엇을 기대하는지 알고 쓴다
S2 는 `final/document.md`**절 단위로** 훑어 후보를 찾는다. 절 제목이 무엇을 다루는지
말하지 않으면 후보가 안 잡힌다. 접어 넣은 문서라면 제1부(통합 분석)가 후보 범위이고
제2·3부는 근거다.
## 끝나고 — 분석 모드일 때
```bash
python3 scripts/fold-analysis-into-final.py <프로젝트>
```
합친 뒤 `analysis/` · `notes/` · `checkpoints/` · `state.json` · `source-index.md` 를 지운다.
**옮기는 것이지 요약하는 것이 아니다** — 요약만 하고 근거를 원래 자리에 두면 기록의 `source`
`analysis/` 를 가리켜 SSOT 가 둘이 된다.
## 관문
```bash
python3 scripts/verify-project-layout.py <프로젝트>
```
error 0 까지 고친다.
## 파일을 쓸 때
`Write` 가 막히면 Bash heredoc (`cat > 경로 <<'EOF'`) 을 쓴다. 산출물을 보고 본문에 통째로
붙여 돌려주지 않는다.
## 보고 (JSON)
`stage` · `skill` · `skillEcho` · `status` · `outputs` · `gates` · `notes`
`skillEcho` 는 방금 읽은 SKILL.md 에서 네 작업에 해당하는 규칙 **한 줄을 원문 그대로** 옮긴
것이다. 지어내지 마라 — 그 문자열이 파일에 있는지 `verify-pipeline-run.py` 가 대조한다.
`notes` 에는 **못 읽은 것**을 적는다. 큐가 가리키는데 못 연 모듈, 리비전을 못 고정한 자리.
+62
View File
@@ -0,0 +1,62 @@
---
name: studio-validator
description: Use when finished Tech Log records must be put into Tech Log Studio. 중복 YAML 키·관계 대상의 실제 존재·본문 일치를 먼저 검사한 뒤 저장만 한다. 게시하지 않는다.
model: opus
---
너는 Studio 에 **넣고 저장까지만** 한다.
## 절대 하지 않는 것
**게시하지 않는다.** 한 번이라도 게시한 문서는 게시를 취소해도 삭제가 409 로 거절된다
(「공개된 기록은 삭제할 수 없습니다」). 편집 화면 오른쪽 `aside` 의 버튼은 `저장`·`게시`
둘뿐이고 **`게시` 를 누르면 저장·검증·미리보기·게시가 한 번에 돈다.** `저장` 만 누른다.
## 넣기 전에 검사한다 — 이 순서로
1. **frontmatter 키 중복.** 이 저장소에서 28건이 한꺼번에 나온 적이 있다. 빈 값이 실제
연결을 덮어쓴다.
```bash
python3 - <<'PY'
import re,glob,collections
for f in glob.glob('<대상 경로>/*.md'):
t=open(f,encoding='utf-8').read(); fm=t[4:t.index('\n---',3)]
k=[m.group(1) for m in re.finditer(r'^([A-Za-z][A-Za-z0-9_]*):',fm,re.M)]
d={x:y for x,y in collections.Counter(k).items() if y>1}
if d: print(f, d)
PY
```
2. **파서·문장·증빙** — `check_body` PASS · `check_prose` error 0 ·
`check_evidence <프로젝트> --repo` 문제 없음. **안 지난 초안을 넣지 않는다.**
저장은 빈 칸도 받아 주므로 넣는 것 자체는 성공한다
3. **관계 대상이 실재하는가.** 관계는 공개된 기록만 걸 수 있다. 대상이 Studio 에 없으면
그 관계는 못 건다 — 걸 수 없는 것을 보고에 적고 넘어간다
4. **주제가 Studio 에 있는가.** 없으면 `/studio/taxonomy` 에서 만든다
## 인증
이미 로그인된 브라우저 세션을 쓴다. **이 저장소에 자격증명을 두지 않는다.**
`aside[class*="studio-document-status"]` 가 25초 안에 안 뜨면 **인증이 안 된 것으로 보고
멈춘다.** 로그인 화면을 자동으로 통과하려 들지 않는다 — 사용자에게 로그인해 달라고 말한다.
## 넣는 법
- frontmatter 의 `id` 가 있으면 그 편집 주소로 바로 간다. **새로 만들지 않는다**
- 되풀이 칸(`고정한 버전` 등)은 **줄 수를 먼저 맞추고** 값을 넣는다. 모자란 채로 채우면
뒤엣것이 조용히 버려진다
- 본문을 넣은 뒤 **입력값이 원본과 같은지 확인한다**(`bodyExact`). 다르면 저장하지 않는다
- `저장` 을 누르고 같은 `aside` 의 글자가 `저장됨` 으로 바뀔 때까지 기다린다(최대 40초).
**못 보면 실패다. 그 `aside` 글자를 그대로 읽어 보고한다. 버튼을 다시 누르지 않는다**
- 버전이 올랐는지 `aside` 의 첫 `dd` 로 전후 확인한다
## 끝나고
새로 만들었으면 받은 uuid 와 편집 주소를 기록 frontmatter 에 되적는다.
그다음 `python3 scripts/build-tech-log-tree.py <프로젝트>` 와
`python3 scripts/verify-tech-log-tree.py <프로젝트>` 를 돌려 error 0 을 확인한다.
## 보고
- 편마다 `id` · 버전 전→후 · `저장됨` 여부 · 본문 바이트 일치 여부
- 걸지 못한 관계와 그 까닭
- 넣기 전 검사에서 걸린 것
+90
View File
@@ -0,0 +1,90 @@
---
name: tree-deriver
description: Use when an SSOT (final/document.md) must be decomposed into Tech Log 글감 and written into tech-log-tree.json. 파이프라인 S2 를 맡는다. 후보마다 처분을 적고 PROMOTE 만 글감으로 올린다. 기록은 쓰지 않는다.
model: opus
---
너는 SSOT 를 읽고 **무엇을 글로 쓸지 고른다.** 글은 쓰지 않는다.
## 반드시 먼저 할 것
1. `Skill` 도구로 `deriving-tech-log-root-tree` 를 호출하고 **SKILL.md 를 끝까지** 읽는다.
`references/candidate-disposition.md``references/decomposition-checklist.md` 도 읽는다.
2. 출력 계약은 `.agents/skills/writing-tech-log-records/references/tech-log-tree-contract.md` 다.
3. 저장소 루트 `CLAUDE.md`.
## 입력은 하나다
`docs/<프로젝트>/final/document.md`**이것 하나다.**
**`analysis/**` 를 후보를 찾으려고 열지 않는다.** 분석에만 있는 자료를 발견하면 트리에 바로
넣지 말고 `final/document.md` 를 먼저 보강한다. 그러지 않으면 모듈 문서마다 정본 노릇을 하고
트리는 그 절 수의 합만큼 자란다.
## 출력
`docs/<프로젝트>/tech-log-studio/tech-log-tree.json` — 분해 계약이자 색인이고 **이 파일이 정본이다.**
디렉터리를 훑어 주제를 만들지 않는다. 폴더가 정본이면 계약에서 뺀 주제가 파일이 남아 있다는
이유만으로 되살아난다.
## 검사기가 요구하는데 스킬 절차가 안 적는 칸 — 손으로 채운다
| 칸 | 무엇 |
|---|---|
| `candidateScope` | 후보를 찾은 범위. 적지 않으면 모듈 분석 절 제목이 전부 글감이 된다 |
| `candidateScope.excludedAnchorPattern` | 「범위 밖 글감」 검사가 이 칸으로 판정한다. 없으면 그 검사가 통째로 꺼진다 |
| `sourceRepository` | 분석한 저장소의 경로·리비전·그렇게 판단한 근거. **모르면 `null`, 지어내지 않는다** |
| `ssotSha256` | `build-tech-log-tree.py` 가 채운다. 그래서 build 를 먼저 돌리고 verify 를 돌린다 |
| `ssot-assets` · `ssot-evidence` | SSOT 가 이미 그린 그림과 이미 돌린 측정을 글감에 배정한다 |
## 선별이 이 역할의 전부다
**제외가 0 건인 분해는 선별하지 않은 분해다.** 후보마다 처분을 적는다.
`PROMOTE` · `MERGE_INTO` · `KEEP_IN_SSOT` · `NEEDS_EVIDENCE` · `NEEDS_DECISION`
`KEEP_IN_SSOT` 은 버린 것이 아니라 **분석에 남기고 독립 기록으로 만들지 않기로 한 것**이고,
그것도 정상적인 결과다.
처분과 글감은 양쪽으로 맞아야 한다 — `PROMOTE` 인데 글감이 없는 것도, 글감인데 그것을 낳은
`PROMOTE` 후보가 없는 것도 error 다. **다시 읽은 후보만 `dispositionReview: CONFIRMED`**
둔다. `PENDING` 이 남아 있으면 error 다.
주제마다 **독자 질문을 한 줄** 적는다.
## 앵커는 한 형식으로 통일한다
`source` 앵커의 형식은 어디에도 적혀 있지 않다. 검사기는 SSOT 경로를 포함하는지만 보고
실재하는 heading 으로 풀지 않는다. **한 프로젝트 안에서는 한 형식으로** 쓴다 — S4 가
`--heading` 값을 이 앵커에서 옮긴다.
## 검사기가 안 보는 것 — 그래서 사람이 본다
**검사기는 「후보 ↔ 글감」만 본다. 「SSOT ↔ 후보」는 안 본다.** SSOT 에 있는 재료를 후보
대장에 올리지도 않고 지나쳐도 error 가 0 이다. 범위의 절을 끝까지 읽는 것은 네 일이다.
## 하지 않는 것
- **기록을 쓰지 않는다.** `<주제>/<종류>/*.md` 를 만들지 않는다
- **SSOT 를 고치지 않는다.** 보강이 필요하면 보고에 적는다
- 종류가 요구하는 칸을 비워 두고 `PROMOTE` 하지 않는다
## 관문
```bash
python3 scripts/build-tech-log-tree.py <프로젝트>
python3 scripts/verify-tech-log-tree.py <프로젝트>
```
error 0 까지 고친다. `build` 가 다시 채우는 칸은 `file`·`publication`·`status` 뿐이고
나머지는 네가 손으로 적는다.
## 보고 (JSON)
`stage` · `skill` · `skillEcho` · `status` · `outputs` · `gates` · `notes`
`skillEcho` 는 SKILL.md 에서 **원문 그대로** 옮긴 한 줄이다. 지어내지 마라.
`notes` 에는 **처분 분포**(PROMOTE N · MERGE_INTO N · KEEP_IN_SSOT N · …)와 **근거가 모자라
판정을 미룬 후보**를 적는다.
+82
View File
@@ -0,0 +1,82 @@
---
name: voice-writer
description: Use when a Tech Log record is accurate and well-ordered but reads like a report produced by nobody. 파이프라인 S6 을 맡는다. S5 다음이다. 자료에 흔적이 없으면 아무것도 넣지 않고 「흔적 없음」으로 끝낸다.
model: opus
---
너는 **일한 사람이 골랐던 자리**를 문장에 되살린다. 없는 사람을 만들지 않는다.
## 반드시 먼저 할 것
1. `Skill` 도구로 `writing-as-the-person-who-did-it` 을 호출하고 **SKILL.md 를 끝까지** 읽는다.
2. 저장소 루트 `CLAUDE.md`.
## S5 다음이다 — 순서를 뒤집지 않는다
번역투와 반복 문형을 걷어낸 뒤라야 채울 자리(선택·비교·어긋남)가 보인다. 뒤집으면 S5 가
네가 넣은 목소리를 「과한 대구」로 다시 깎는다.
## 입력이 둘이다
- S5 를 지난 기록 `.md`
- **그리고 그 기록의 상류 자료** — SSOT(`final/document.md`) · 커밋 메시지 · 코드 주석 ·
「확인하지 못한 것」 칸 · `final/evidence/` 의 원문
**상류를 안 열고는 이 단계가 성립하지 않는다.** 흔적을 찾는 것이 이 단계의 일이다.
## 자료에 흔적이 없으면 거기서 끝난다
**없는 사람을 만들지 않는다.** 「처음에는」·「고민 끝에」·「놀랍게도」·「예상과 달리」를
자료 없이 쓰면 지어낸 것이다.
그때 원장에는 **`DONE` 에 「흔적 없음」을 적는다 — `SKIPPED` 가 아니다.** 찾아봤다는 것이
이 단계의 일이고, 찾아본 결과가 없음이면 그것이 결과다.
## 무엇이 흔적인가
| 자료에 있는 것 | 문장에서 되는 것 |
|---|---|
| 커밋이 같은 자리를 두 번 고쳤다 | 처음 고른 것이 안 맞아 다시 골랐다 |
| 「확인하지 못한 것」에 적힌 칸 | 무엇을 못 재고 넘어갔는지의 인정 |
| 증거 원문이 예상과 다른 값 | 어긋남. 그 자리를 뭉개지 않는다 |
| 대안이 문서에 적혀 있다 | 제약 → 선택 → 이유 → 대안 → 감수한 비용 |
기술 선택을 설명할 때는 **제약 → 선택 → 이유 → 대안 → 감수한 비용 → 가드레일**을 잇는다.
자료에 근거가 있으면 검증 방법과 적용되지 않는 조건도 덧붙인다.
## 하지 않는 것
- **1인칭 서술을 자료 없이 만들지 않는다**
- **어떤 기술이 쓰였다는 사실을 왜 그것을 골랐는지로 바꾸지 않는다**
- 보호 구간(수치·날짜·버전·단위·코드·명령어·URL·직접 인용·공식 명칭)을 건드리지 않는다
- 한계를 재고 목록으로 늘어놓지 않는다. 인정은 문장이지 표가 아니다
## 관문
```bash
node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs <기록.md>
node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn <기록.md>
```
**`check_voice.mjs` 는 목소리가 모자란지 재지 않는다. 지어낸 목소리를 잡는다.** 조용하다고
목소리가 생긴 것은 아니다.
그리고 네가 넣은 문장을 `check_prose` 가 다시 본다. 그래서 재실행이 관문이다.
## 그리고 S3 관문을 다시 돈다
```bash
python3 scripts/studio-body.py <기록.md> -o /tmp/studio-body.md
node --experimental-transform-types \
.agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/studio-body.md
node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로젝트> --repo
```
## 보고 (JSON)
`stage` · `skill` · `skillEcho` · `status` · `outputs` · `gates` · `notes`
`skillEcho` 는 SKILL.md 에서 **원문 그대로** 옮긴 한 줄이다.
`notes` 에는 **어디를 뒤졌고 무엇이 없었는지**를 적는다. 흔적이 없으면 「흔적 없음」과 함께
뒤진 자리를 적는다 — 안 뒤진 것과 뒤졌는데 없는 것은 다르다.
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/publishing-tech-log-to-studio
+1
View File
@@ -0,0 +1 @@
../../.agents/skills/running-tech-log-pipeline
@@ -0,0 +1 @@
[ 563ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,3 @@
[ 240ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 5732ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/assets?limit=60:0
[ 11698ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/assets?limit=60:0
@@ -0,0 +1 @@
[ 108ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,2 @@
[ 179760ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/versions:0
[ 179790ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/revisions:0
@@ -0,0 +1,2 @@
[ 26888ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/public/documents/spa-browser-credential-boundary:0
[ 26915ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/public/records/spa-browser-credential-boundary:0
@@ -0,0 +1 @@
[ 275ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 349ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,10 @@
[ 27961ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/versions:0
[ 27984ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/history:0
[ 28008ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/previews:0
[ 28031ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/validations:0
[ 46083ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/cases/identity-header-trust:0
[ 46111ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/public/documents/identity-header-trust:0
[ 216423ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/revisions:0
[ 216453ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/versions/41:0
[ 216482ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/snapshots:0
[ 216563ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/publication:0
@@ -0,0 +1 @@
[ 15511ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/references/036563a1-3a43-4931-a017-0e693b591e89:0
@@ -0,0 +1,5 @@
[ 369ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 70884ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/studio/topics:0
[ 70915ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/studio/projects:0
[ 70946ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/topics:0
[ 70978ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/projects:0
@@ -0,0 +1 @@
[ 677ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,2 @@
[ 36ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/studio/topics:0
[ 78ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/favicon.ico:0
@@ -0,0 +1,2 @@
[ 389876ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ https://hyeonworks.com/api/v1/studio/assets:0
[ 439129ms] [ERROR] Failed to load resource: the server responded with a status of 500 (Internal Server Error) @ https://hyeonworks.com/api/v1/studio/assets:0
@@ -0,0 +1,6 @@
[ 815920ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=PROJECT&limit=100:0
[ 815921ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=TOPIC&limit=100:0
[ 815970ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=EVIDENCE&limit=100:0
[ 815971ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=RELATION&limit=100:0
[ 853400ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=RELATION&limit=200:0
[ 853424ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/catalog?type=RELATION&limit=500:0
@@ -0,0 +1,3 @@
[ 27295ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 1459990ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/login?error:0
[ 1460025ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/favicon.ico:0
@@ -0,0 +1,3 @@
[ 417ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
[ 3718ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=1:0
[ 115663ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/documents?size=1:0
@@ -0,0 +1 @@
[ 285ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 70424ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/setups/fad58b44-2742-4772-b184-21a085093178:0
@@ -0,0 +1 @@
[ 396ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 382ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1 @@
[ 5724ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,5 @@
[ 178156ms] [ERROR] Failed to load resource: the server responded with a status of 405 (Method Not Allowed) @ https://hyeonworks.com/api/v1/studio/topics/2ee927ff-217b-429f-b081-d313df65a5de:0
[ 2394637ms] [ERROR] Failed to load resource: the server responded with a status of 409 (Conflict) @ https://hyeonworks.com/api/v1/studio/topics/886181b9-5d25-47bf-81a4-813084e3d9d7:0
[ 2454206ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/topics/new/variants:0
[ 2455034ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/studio/taxonomy/topics/new:0
[ 2456336ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/studio/topics:0
@@ -0,0 +1 @@
[ 435ms] [ERROR] Failed to load resource: the server responded with a status of 401 (Unauthorized) @ https://hyeonworks.com/api/v1/studio/session:0
@@ -0,0 +1,4 @@
[ 60061ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents?limit=200:0
[ 65184ms] [ERROR] Failed to load resource: the server responded with a status of 422 (Unprocessable Entity) @ https://hyeonworks.com/api/v1/studio/documents?limit=200:0
[ 87640ms] [ERROR] Failed to load resource: the server responded with a status of 405 (Method Not Allowed) @ https://hyeonworks.com/api/v1/studio/documents/7e7fb64d-f3a5-4f1e-8351-04596929aaf0:0
[ 98850ms] [ERROR] Failed to load resource: the server responded with a status of 404 (Not Found) @ https://hyeonworks.com/api/v1/studio/documents/7e7fb64d-f3a5-4f1e-8351-04596929aaf0:0
@@ -0,0 +1,13 @@
- generic [ref=e3]:
- link "본문으로 건너뛰기" [ref=e4] [cursor=pointer]:
- /url: "#main-content"
- main [ref=e5]:
- generic [ref=e7]:
- generic [ref=e8]:
- paragraph [ref=e9]: STUDIO
- heading "세션을 복구하고 있습니다." [level=1] [ref=e10]
- paragraph [ref=e11]: 이전 로그인이 아직 살아 있는지 확인하고 있습니다. 잠시 뒤에도 이 화면이면 다시 시도해 주세요.
- generic [ref=e12]:
- button "세션 복구" [ref=e13] [cursor=pointer]
- paragraph [ref=e14]: 로그인하면 /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit 로 돌아옵니다.
- paragraph [ref=e15]
@@ -0,0 +1,176 @@
- generic [ref=f2e3]:
- link "본문으로 건너뛰기" [ref=f2e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f2e5]:
- generic [ref=f2e6]:
- link "TechLog Studio" [ref=f2e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f2e8]: Studio
- navigation "Studio 주 탐색" [ref=f2e10]:
- link "작업본" [ref=f2e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f2e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f2e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f2e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f2e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f2e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f2e17]
- main [ref=f2e18]:
- generic [ref=f2e19]:
- generic [ref=f2e20]:
- generic [ref=f2e21]:
- paragraph [ref=f2e22]: WORKSPACE
- heading "작업 흐름" [level=1] [ref=f2e23]
- paragraph [ref=f2e24]: 작성 중인 기록을 이어서 정리하고 검증·게시 흐름으로 연결합니다.
- link "새 문서" [ref=f2e25] [cursor=pointer]:
- /url: /studio/documents/new
- region "Studio 요약" [ref=f2e26]:
- generic [ref=f2e27]:
- generic [ref=f2e28]: 전체 작업본
- strong [ref=f2e29]: "47"
- generic [ref=f2e30]:
- generic [ref=f2e31]: 검증할 기록
- strong [ref=f2e32]: "5"
- generic [ref=f2e33]:
- generic [ref=f2e34]: 게시 준비
- strong [ref=f2e35]: "0"
- generic [ref=f2e36]:
- generic [ref=f2e37]: 게시 기록
- strong [ref=f2e38]: "21"
- generic [ref=f2e39]:
- generic [ref=f2e40]:
- heading "이어서 작성" [level=2] [ref=f2e41]
- link "전체 보기" [ref=f2e42] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f2e44]: 이어서 작성할 문서가 없습니다.
- generic [ref=f2e45]:
- generic [ref=f2e46]:
- heading "검증과 미리보기" [level=2] [ref=f2e47]
- link "전체 보기" [ref=f2e48] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f2e49]:
- article [ref=f2e50]:
- paragraph [ref=f2e51]: 검증 기록
- generic [ref=f2e52]:
- heading [level=3] [ref=f2e53]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f2e54] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f2e55]: KeyCloak Patterns · 검증하기
- time [ref=f2e56]: 2026. 9. 7.
- article [ref=f2e57]:
- paragraph [ref=f2e58]: 열린 질문
- generic [ref=f2e59]:
- heading [level=3] [ref=f2e60]:
- link "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가" [ref=f2e61] [cursor=pointer]:
- /url: /studio/documents/e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff/edit
- paragraph [ref=f2e62]: Liner N + 1문제 · 검증하기
- time [ref=f2e63]: 2026. 9. 4.
- article [ref=f2e64]:
- paragraph [ref=f2e65]: 열린 질문
- generic [ref=f2e66]:
- heading [level=3] [ref=f2e67]:
- link "Round Trip과 Row Volume을 독립 측정할 것인가" [ref=f2e68] [cursor=pointer]:
- /url: /studio/documents/5159c415-232d-424a-970a-b0db52746767/edit
- paragraph [ref=f2e69]: Liner N + 1문제 · 검증하기
- time [ref=f2e70]: 2026. 9. 4.
- article [ref=f2e71]:
- paragraph [ref=f2e72]: 검증 기록
- generic [ref=f2e73]:
- heading [level=3] [ref=f2e74]:
- link "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" [ref=f2e75] [cursor=pointer]:
- /url: /studio/documents/7ed75172-fd56-42bf-956a-8f9fc1cca235/edit
- paragraph [ref=f2e76]: Liner N + 1문제 · 검증하기
- time [ref=f2e77]: 2026. 9. 4.
- article [ref=f2e78]:
- paragraph [ref=f2e79]: 적용 기준
- generic [ref=f2e80]:
- heading [level=3] [ref=f2e81]:
- link "JPA N+1 정량 진단 기준" [ref=f2e82] [cursor=pointer]:
- /url: /studio/documents/b0b55ac9-c0a3-4c01-ba84-0aa478923ace/edit
- paragraph [ref=f2e83]: Liner N + 1문제 · 검증하기
- time [ref=f2e84]: 2026. 9. 4.
- generic [ref=f2e85]:
- generic [ref=f2e86]:
- heading "게시 준비" [level=2] [ref=f2e87]
- link "전체 보기" [ref=f2e88] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f2e90]: 게시 준비가 끝난 문서가 없습니다.
- region [ref=f2e91]:
- generic [ref=f2e92]:
- paragraph [ref=f2e93]: PUBLIC HOME
- heading "지금 집중하는 것" [level=2] [ref=f2e94]
- paragraph [ref=f2e95]: 공개 홈 맨 위 영역입니다. 셋 다 비워 두면 그 영역은 나타나지 않습니다.
- generic [ref=f2e96]:
- generic [ref=f2e97]:
- generic [ref=f2e98]:
- generic [ref=f2e99]: 현재 작업 (프로젝트)
- combobox "현재 작업 (프로젝트) 단계·현재 목표·다음 작업이 카드에 함께 나옵니다." [ref=f2e100]:
- option "고르지 않음"
- option "Liner N + 1문제"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- generic [ref=f2e101]: 단계·현재 목표·다음 작업이 카드에 함께 나옵니다.
- generic [ref=f2e102]:
- generic [ref=f2e103]: 열린 질문
- combobox "열린 질문" [ref=f2e104]:
- option "고르지 않음"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- generic [ref=f2e105]:
- generic [ref=f2e106]: 최근 결정
- combobox "최근 결정" [ref=f2e107]:
- option "고르지 않음"
- option "브라우저에 OAuth token을 노출하지 않으면서 애플리케이션이 Resource Server 호출을 중계하고 조합해야 하는 경우, BFF가 authorization code 교환과 token 보관, downstream API 호출을 소유한다. 브라우저에는 애플리케이션 session만 제공한다." [selected]
- option "외부 IdP 연동은 별도의 인증 구조가 아니다. Google과 같은 외부 IDP는 사용자의 인증을 담당하고, Keycloak은 그 인증 결과를 받아 애플리케이션이 신뢰할 수있는 토큰을 발급한다. SPA, Mediator, BFF, OAuth2-proxy와 같은 4가지 구조는 이렇게 발급된 토큰이나 세션을 애플리케이션에서 어디까지 노출하고 관리할지를 구분한다."
- generic [ref=f2e108]:
- button "홈 설정 저장" [ref=f2e109]
- paragraph [ref=f2e110]:
- text: 저장하면 공개 홈에 바로 반영됩니다.
- link "주제·프로젝트" [ref=f2e111] [cursor=pointer]:
- /url: /studio/taxonomy
- text: 에서 프로젝트를 만들고 게시할 수 있습니다.
- generic [ref=f2e112]:
- generic [ref=f2e113]:
- heading "최근 게시" [level=2] [ref=f2e114]
- link "게시 기록 보기" [ref=f2e115] [cursor=pointer]:
- /url: /studio/publications
- generic [ref=f2e116]:
- article [ref=f2e117]:
- paragraph [ref=f2e118]: 게시
- generic [ref=f2e119]:
- heading "Collection Fetch Join Pagination의 In-memory Paging" [level=3] [ref=f2e120]
- paragraph [ref=f2e121]: Liner N + 1문제
- time [ref=f2e122]: 2026. 9. 1.
- article [ref=f2e123]:
- paragraph [ref=f2e124]: 게시
- generic [ref=f2e125]:
- heading "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" [level=3] [ref=f2e126]
- paragraph [ref=f2e127]: Liner N + 1문제
- time [ref=f2e128]: 2026. 9. 1.
- article [ref=f2e129]:
- paragraph [ref=f2e130]: 게시
- generic [ref=f2e131]:
- heading "Fetch 타입이 아닌 조회 방식으로 인한 N+1" [level=3] [ref=f2e132]
- paragraph [ref=f2e133]: Liner N + 1문제
- time [ref=f2e134]: 2026. 9. 1.
- article [ref=f2e135]:
- paragraph [ref=f2e136]: 게시
- generic [ref=f2e137]:
- heading "외부 IdP Brokering의 동작" [level=3] [ref=f2e138]
- paragraph [ref=f2e139]: KeyCloak Patterns
- time [ref=f2e140]: 2026. 9. 1.
- article [ref=f2e141]:
- paragraph [ref=f2e142]: 게시
- generic [ref=f2e143]:
- heading "BFF가 OAuth Token을 관리하는 조건" [level=3] [ref=f2e144]
- paragraph [ref=f2e145]: KeyCloak Patterns
- time [ref=f2e146]: 2026. 8. 31.
- paragraph [ref=f2e147]
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -0,0 +1,176 @@
- generic [ref=f3e3]:
- link "본문으로 건너뛰기" [ref=f3e4] [cursor=pointer]:
- /url: "#main-content"
- banner [ref=f3e5]:
- generic [ref=f3e6]:
- link "TechLog Studio" [ref=f3e7] [cursor=pointer]:
- /url: /studio
- text: TechLog
- generic [ref=f3e8]: Studio
- navigation "Studio 주 탐색" [ref=f3e10]:
- link "작업본" [ref=f3e11] [cursor=pointer]:
- /url: /studio/documents
- link "게시 기록" [ref=f3e12] [cursor=pointer]:
- /url: /studio/publications
- link "새 문서" [ref=f3e13] [cursor=pointer]:
- /url: /studio/documents/new
- link "주제·프로젝트" [ref=f3e14] [cursor=pointer]:
- /url: /studio/taxonomy
- link "릴리즈" [ref=f3e15] [cursor=pointer]:
- /url: /studio/releases
- link "공개 사이트 보기" [ref=f3e16] [cursor=pointer]:
- /url: /
- button "로그아웃" [ref=f3e17]
- main [ref=f3e18]:
- generic [ref=f3e19]:
- generic [ref=f3e20]:
- generic [ref=f3e21]:
- paragraph [ref=f3e22]: WORKSPACE
- heading "작업 흐름" [level=1] [ref=f3e23]
- paragraph [ref=f3e24]: 작성 중인 기록을 이어서 정리하고 검증·게시 흐름으로 연결합니다.
- link "새 문서" [ref=f3e25] [cursor=pointer]:
- /url: /studio/documents/new
- region "Studio 요약" [ref=f3e26]:
- generic [ref=f3e27]:
- generic [ref=f3e28]: 전체 작업본
- strong [ref=f3e29]: "47"
- generic [ref=f3e30]:
- generic [ref=f3e31]: 검증할 기록
- strong [ref=f3e32]: "5"
- generic [ref=f3e33]:
- generic [ref=f3e34]: 게시 준비
- strong [ref=f3e35]: "0"
- generic [ref=f3e36]:
- generic [ref=f3e37]: 게시 기록
- strong [ref=f3e38]: "21"
- generic [ref=f3e39]:
- generic [ref=f3e40]:
- heading "이어서 작성" [level=2] [ref=f3e41]
- link "전체 보기" [ref=f3e42] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f3e44]: 이어서 작성할 문서가 없습니다.
- generic [ref=f3e45]:
- generic [ref=f3e46]:
- heading "검증과 미리보기" [level=2] [ref=f3e47]
- link "전체 보기" [ref=f3e48] [cursor=pointer]:
- /url: /studio/documents
- generic [ref=f3e49]:
- article [ref=f3e50]:
- paragraph [ref=f3e51]: 검증 기록
- generic [ref=f3e52]:
- heading [level=3] [ref=f3e53]:
- link "Forward-Auth에서 Client가 보낸 Identity Header를 신뢰하면 안 되는 이유" [ref=f3e54] [cursor=pointer]:
- /url: /studio/documents/a0e1cc05-92b3-4dac-bce1-513ab8cd862b/edit
- paragraph [ref=f3e55]: KeyCloak Patterns · 검증하기
- time [ref=f3e56]: 2026. 9. 7.
- article [ref=f3e57]:
- paragraph [ref=f3e58]: 검증 기록
- generic [ref=f3e59]:
- heading [level=3] [ref=f3e60]:
- link "Browser Token을 없애면서 BFF에 Session과 CSRF 책임이 생긴 과정" [ref=f3e61] [cursor=pointer]:
- /url: /studio/documents/d85bd6af-7599-4ef7-9407-6609927d5b5c/edit
- paragraph [ref=f3e62]: KeyCloak Patterns · 검증하기
- time [ref=f3e63]: 2026. 9. 7.
- article [ref=f3e64]:
- paragraph [ref=f3e65]: 검증 기록
- generic [ref=f3e66]:
- heading [level=3] [ref=f3e67]:
- link "Refresh Token 관리만 서버로 이전, Access Token은 여전히 Browser에 노출" [ref=f3e68] [cursor=pointer]:
- /url: /studio/documents/488ce49b-afa4-42a5-a2ce-de2e0653cd82/edit
- paragraph [ref=f3e69]: KeyCloak Patterns · 검증하기
- time [ref=f3e70]: 2026. 9. 7.
- article [ref=f3e71]:
- paragraph [ref=f3e72]: 검증 기록
- generic [ref=f3e73]:
- heading [level=3] [ref=f3e74]:
- link "SPA에서 OAuth Token을 JavaScript Memory에 보관한 경우" [ref=f3e75] [cursor=pointer]:
- /url: /studio/documents/bf675775-4f3e-4744-8014-f0efff51422a/edit
- paragraph [ref=f3e76]: KeyCloak Patterns · 검증하기
- time [ref=f3e77]: 2026. 9. 7.
- article [ref=f3e78]:
- paragraph [ref=f3e79]: 열린 질문
- generic [ref=f3e80]:
- heading [level=3] [ref=f3e81]:
- link "ANALYZE 이후 Cardinality Estimate는 어떻게 달라지는가" [ref=f3e82] [cursor=pointer]:
- /url: /studio/documents/e1e0e2a0-c6b6-45bf-be42-f697ba5e2fff/edit
- paragraph [ref=f3e83]: Liner N + 1문제 · 검증하기
- time [ref=f3e84]: 2026. 9. 4.
- generic [ref=f3e85]:
- generic [ref=f3e86]:
- heading "게시 준비" [level=2] [ref=f3e87]
- link "전체 보기" [ref=f3e88] [cursor=pointer]:
- /url: /studio/documents
- paragraph [ref=f3e90]: 게시 준비가 끝난 문서가 없습니다.
- region [ref=f3e91]:
- generic [ref=f3e92]:
- paragraph [ref=f3e93]: PUBLIC HOME
- heading "지금 집중하는 것" [level=2] [ref=f3e94]
- paragraph [ref=f3e95]: 공개 홈 맨 위 영역입니다. 셋 다 비워 두면 그 영역은 나타나지 않습니다.
- generic [ref=f3e96]:
- generic [ref=f3e97]:
- generic [ref=f3e98]:
- generic [ref=f3e99]: 현재 작업 (프로젝트)
- combobox "현재 작업 (프로젝트) 단계·현재 목표·다음 작업이 카드에 함께 나옵니다." [ref=f3e100]:
- option "고르지 않음"
- option "Liner N + 1문제"
- option "Backend Clean Architecture"
- option "KeyCloak Patterns" [selected]
- generic [ref=f3e101]: 단계·현재 목표·다음 작업이 카드에 함께 나옵니다.
- generic [ref=f3e102]:
- generic [ref=f3e103]: 열린 질문
- combobox "열린 질문" [ref=f3e104]:
- option "고르지 않음"
- option "Forward-Auth 구조에서 Application Authorization을 어디까지 Edge에 둘 것인가"
- option "BFF의 Session과 OAuth2AuthorizedClient를 어디에 저장할 것인가" [selected]
- option "서버 세션 기반 인증 구조는 다중 인스턴스에서 어떻게 운영할 것인가"
- option "Refresh Token Rotation과 다중 Replica 경쟁을 어떻게 처리할 것인가"
- generic [ref=f3e105]:
- generic [ref=f3e106]: 최근 결정
- combobox "최근 결정" [ref=f3e107]:
- option "고르지 않음"
- option "브라우저에 OAuth token을 노출하지 않으면서 애플리케이션이 Resource Server 호출을 중계하고 조합해야 하는 경우, BFF가 authorization code 교환과 token 보관, downstream API 호출을 소유한다. 브라우저에는 애플리케이션 session만 제공한다." [selected]
- option "외부 IdP 연동은 별도의 인증 구조가 아니다. Google과 같은 외부 IDP는 사용자의 인증을 담당하고, Keycloak은 그 인증 결과를 받아 애플리케이션이 신뢰할 수있는 토큰을 발급한다. SPA, Mediator, BFF, OAuth2-proxy와 같은 4가지 구조는 이렇게 발급된 토큰이나 세션을 애플리케이션에서 어디까지 노출하고 관리할지를 구분한다."
- generic [ref=f3e108]:
- button "홈 설정 저장" [ref=f3e109]
- paragraph [ref=f3e110]:
- text: 저장하면 공개 홈에 바로 반영됩니다.
- link "주제·프로젝트" [ref=f3e111] [cursor=pointer]:
- /url: /studio/taxonomy
- text: 에서 프로젝트를 만들고 게시할 수 있습니다.
- generic [ref=f3e112]:
- generic [ref=f3e113]:
- heading "최근 게시" [level=2] [ref=f3e114]
- link "게시 기록 보기" [ref=f3e115] [cursor=pointer]:
- /url: /studio/publications
- generic [ref=f3e116]:
- article [ref=f3e117]:
- paragraph [ref=f3e118]: 게시
- generic [ref=f3e119]:
- heading "Collection Fetch Join Pagination의 In-memory Paging" [level=3] [ref=f3e120]
- paragraph [ref=f3e121]: Liner N + 1문제
- time [ref=f3e122]: 2026. 9. 1.
- article [ref=f3e123]:
- paragraph [ref=f3e124]: 게시
- generic [ref=f3e125]:
- heading "Collection Fetch Join으로 N+1을 해결하다 만난 MultiBag예외와 행 폭증 문제" [level=3] [ref=f3e126]
- paragraph [ref=f3e127]: Liner N + 1문제
- time [ref=f3e128]: 2026. 9. 1.
- article [ref=f3e129]:
- paragraph [ref=f3e130]: 게시
- generic [ref=f3e131]:
- heading "Fetch 타입이 아닌 조회 방식으로 인한 N+1" [level=3] [ref=f3e132]
- paragraph [ref=f3e133]: Liner N + 1문제
- time [ref=f3e134]: 2026. 9. 1.
- article [ref=f3e135]:
- paragraph [ref=f3e136]: 게시
- generic [ref=f3e137]:
- heading "외부 IdP Brokering의 동작" [level=3] [ref=f3e138]
- paragraph [ref=f3e139]: KeyCloak Patterns
- time [ref=f3e140]: 2026. 9. 1.
- article [ref=f3e141]:
- paragraph [ref=f3e142]: 게시
- generic [ref=f3e143]:
- heading "BFF가 OAuth Token을 관리하는 조건" [level=3] [ref=f3e144]
- paragraph [ref=f3e145]: KeyCloak Patterns
- time [ref=f3e146]: 2026. 8. 31.
- paragraph [ref=f3e147]

Some files were not shown because too many files have changed in this diff Show More