Files
document-haness/.agents/skills/writing-tech-log-records/references/writing-each-kind.md
T
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

21 KiB

종류마다 무엇을 어떤 순서로 쓰나

칸 목록과 상한은 record-kinds.md, 문장 규칙은 explaining.md, 문서군의 리듬은 ai-tells.md에 있다. 이 문서는 그 칸을 무엇으로 채우는가다.

Studio 에 실제로 올라간 47건에서 뽑았다. keycloak 23건(Case 4·Concept 6·Reference 7· Question 4·Decision 2), n+1liner 24건(Case 5·Reference 7·Question 5·Decision 7). 「대개 이렇게 쓴다」는 말은 그 47건이 그렇게 돼 있다는 뜻이다.

Setup 은 그 47건에 없다. 2026-09-12 에 Studio 의 환경 구성 문서가 0건이었다. 그래서 Setup 절은 계약과 편집 화면에서 읽어 썼다. 나머지 다섯처럼 올라간 기록을 세지 않았다.

clean-architecture-backend-template 의 949건은 다른 절 이름으로 쓰여 있었고, 지금 이 기준으로 다시 판정하는 중이다. 아직 기준을 만족한 상태가 아니므로 그 949건을 본보기로 삼지 않는다. 올라간 적 없는 초안이 아니라 올라간 것이 기준이다.

파일 뼈대 — 여섯 종류가 같다

---
id · kind · slug · title · topic · topicName · project · status · studio
source · sourceRevision
(종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn · pinnedVersions
(있으면) evidence · assets — assets 는 Case · Concept · Setup 만
---

# 제목

리드 문단. 이것이 Studio 의 `요약` 칸이다.        ← 47건 모두 있다

## 관계          ← Decision 만 「근거」다
## <칸 이름>     ← 종류마다 다르다
## 본문          ← Case · Concept · Setup 만
<!-- body:start -->
...
<!-- body:end -->

frontmatter 는 메타데이터, ## 는 Studio 의 칸, 제목 아래 첫 문단은 요약이다. 47건 모두 이 모양이다.

## 요약 이라는 절을 만들지 않는다. 요약은 제목 바로 아래 문단이다. 절로 만들면 Studio 에 그런 칸이 없어서 통째로 사라진다.

## 출처 도 칸이 아니다. 원본 분석 문서나 파일 경로는 frontmatter 의 source 에 적는다. 본문 마지막에 두면 게시된 글에 저장소 내부 경로가 그대로 실린다.

source:
  - final/document.md#a05-adapter-outbound-persistence-jpa#L354
sourceRevision: 21234e38cdb9a926cbc92bb97a2aee2e4a7d2916

source 는 SSOT 의 앵커다. 분석을 접어 넣은 프로젝트에는 analysis/ 가 없으므로 그 경로를 적으면 가리키는 파일이 없다.

관계 항목은 굵은 제목 한 줄 + 이유 한 줄이다.

- **Keyset Pagination 설계 기준**
  이 결정을 규칙으로 편 기준이다.

Case — 9건

칸은 관계 · 문제 · 결론 · 검증 환경 · 재현 조건 · 본문. 9건 모두 여섯 칸을 채웠다.

무엇을
문제 무엇이 어떠해야 했는데 어떻게 됐나. 요구를 먼저, 실제를 다음에
결론 재현해서 확정한 것. 수치를 그대로. 「~일 것이다」가 아니라 「~였다」
검증 환경 런타임·버전·DB·측정 도구. 이름 : 값으로 줄을 나눈다
재현 조건 다른 사람이 같은 값을 얻는 순서. 번호를 매긴다

본문의 절은 하나의 주장과 그 근거가 이어지는 단위로 나눈다. 47건에서는 대개 여섯 절 안팎이었지만 절 수는 작성 조건이 아니다. 숫자를 맞추려고 절을 쪼개거나 붙이면 문서마다 같은 모양이 된다. 아래는 그 47건에서 실제로 반복된 순서다.

  • 첫 절은 무대를 세운다. 잰 코드나 구조를 먼저 보여 준다 — 「측정한 loadFeed 구현」, 「무대 — 페이징 한 줄만 추가」, 「가시성을 얹기 전 — 인덱스로 커서 이후만」, 「Mediator에서 Access Token과 Refresh Token을 관리하는 위치」
  • 가운데는 측정값(표) → 그 값을 어떻게 읽나 → 실행계획이나 로그 순서다
  • 마지막 절은 범위나 다음이다. 9건 중 4건이 확인 범위(「증명하지 않는 것」, 「현재 자동 테스트로 확인한 범위」, 「Redirect URI와 CORS에서 아직 확인하지 않은 부분」), 2건이 「다음 선택」, 나머지가 지표 읽는 법이나 남긴 이유다. 재지 않은 것을 적지 않고 닫는 Case 는 없다

그 마지막 절은 본문 안이다. 칸으로 빼면 Studio 에 그런 칸이 없어 사라진다.

코드블록에는 라벨로 무엇인지 적는다. 표는 머리글이 무엇을 묻는지 말하게 하고, 그 표를 어떻게 읽는지 설명하는 문장(「이렇게 갈린다」「함께 읽어야 한다」)은 두지 않는다. 예시는 한 규모로 고정한다 — 표가 전체 계열을 이미 보여 준다.

Concept — 6건

칸은 관계 · 본문 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 잰 결과가 아니라 이미 그렇게 동작하는 것을 적기 때문이다.

basisVersion 은 frontmatter 에 있고 본 것을 · 로 잇는다.

basisVersion: Keycloak 26.7.0 · oidc-client-ts 3.3.0
basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request

절의 개수를 정해 두지 않는다. 설명해야 할 참여자와 단계가 몇 개인지가 정한다. 47건에서 반복된 순서는 이렇다.

  • 첫 절은 무엇이 무엇을 주고받는지다 — 「두 개의 OAuth 왕복이 이어진다」, 「Resource Server가 받는 입력」, 「요청 하나가 두 번 평가된다」
  • 가운데는 단계마다 실제로 일어나는 일이다
  • 마지막 절은 막지 않는 것이나 확인한 범위다 — 「PKCE가 막지 않는 것」, 「CSRF가 XSS를 대신하지 않는다」, 「현재 검증한 범위」, 「지금 구성이 보여 주지 않는 것」

「확인했다」는 Case 의 말이다. Concept 은 「~한다」로 적는다. 규격이 정한 것과 그 구현이 그렇게 한 것을 구분한다.

Setup — 0건

칸은 관계 · 본문 둘뿐이고, 고정한 버전은 frontmatter 의 pinnedVersions 에 있다. 계약의 PinnedVersionnameversion 을 나눠 담는다 — 이름 160자, 버전 140자, 30개까지.

pinnedVersions:
  - name: Keycloak
    version: 26.7.0

절 이름을 강제하지 않는다. 계약이 그렇게 적는다 — 「실행 절차·구성 값·확인 방법을 ## 절로 적는다. 절 이름을 강제하지 않는다 — 프로젝트마다 셋업의 모양이 다르다.」 그래도 빈 본문에서 시작하지는 않는다. 작업본을 만들면 Studio 가 절 셋을 미리 넣어 주므로 거기서 출발한다.

## 실행 절차

## 구성 값

## 확인 방법

명령은 코드블록으로 적는다. 절차 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」은 상태를 읽는 단계의 모양이다. 환경 구성의 본문은 상태를 바꾸는 단계가 대부분이고, 그쪽은 칸이 다섯이다.

### N. <이 단계가 무엇을 만드는가>

목적            한 줄. 이 단계가 끝나면 무엇이 달라지나
행동            번호를 매긴 명령. 한 번호에 한 가지 일
예상 결과       그때 화면에 나오는 것
왜 필요한가     건너뛰거나 어긋나면 무엇이 깨지나
문제가 생기면   어느 명령부터 다시 보나

번호는 행동을 세려고 매긴다. 한 번호가 접속과 파일 생성과 권한 설정을 함께 하면 읽는 사람은 어디까지 왔는지 셀 수 없고, 실패해도 그 줄의 어느 대목에서 실패했는지 모른다. 다섯 칸이 단계마다 같은 순서로 오면 처음 따라 하는 사람이 단계마다 같은 곳에서 같은 것을 찾는다.

채운 예는 writing-practitioner-guides 에 있다 — libvirt 연결 URI 를 qemu:///system 으로 고정하는 단계다.

자리표시자

원칙은 자리표시자를 두지 않는 것이다. writing-practitioner-guides 의 「No placeholders」가 이유를 적는다 — <토큰> 이라고 적어 두면 그 값을 어디서 가져오는지가 문서 밖으로 나간다. 값을 뽑는 명령을 먼저 주는 것이 먼저다.

예외는 하나다 — 같은 기록의 앞 단계가 화면에 찍은 값을 뒤 단계에 옮겨 넣을 때. 세션 sid 는 로그인할 때마다 새로 생기고 클라이언트 UUID 는 렐름을 만들 때 정해져서 읽는 사람의 실험대에서 다르다. 앞 단계가 탐침 파드 안에서 돌았으면 그 셸의 변수가 뒤 단계의 셸에 없어서 변수로 넘길 수도 없다. 값을 만드는 명령은 이미 같은 문서 안에 있으므로 전역 스킬의 빨간 깃발 (<placeholder> with no command that produces it)에는 걸리지 않는다.

그때 쓰는 꼴은 {{NAME}} 하나다. NAME 은 대문자로 시작하고 대문자·숫자·밑줄만 쓴다.

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_iduser_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 을 받으므로 거기에 적는다.

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건

칸은 관계 · 목적 · 규칙 · 적용 조건 · 예외 · 예시. 14건 모두 여섯 칸을 채웠다. 본문이 없다.

무엇을
목적 이 기준이 무엇을 막는가. 막으려는 실패를 먼저
규칙 제목은 무엇을 하는지/하지 않는지로. 본문에 왜인지
적용 조건 언제 이 기준이 걸리는가
예외 걸리지 않는 경우. 이 칸이 비면 규칙이 과잉 적용된다
예시 짧은 문장. 코드가 아니다

규칙 제목만 읽어도 무엇을 금지하는지 알아야 한다. 「지표를 정확히 읽는다」보다 「지표 이름이 뜻하는 것을 그대로 읽는다」가 낫다. 코드가 필요하면 그 코드가 있는 Case 를 만들고 관계로 가리킨다 — Reference 칸은 마크다운 블록 파서를 안 거쳐서 코드펜스가 글자로 보인다. 낱말 하나짜리 식별자는 백틱으로 감싸면 인라인 <code> 로 살아난다. 여러 줄짜리 코드가 문제다.

Question — 9건

칸은 관계 · 사실 · 가정 · 미지수 · 제약 · 선택지 · 다음 검증. 9건 모두 일곱 칸을 채웠다. questionStatus 는 frontmatter 에 있다.

가정을 사실 칸에 넣지 않는 것이 이 종류의 전부다. 사실은 확인한 것, 가정은 확인하지 않고 전제한 것이다.

다음 검증은 실행할 수 있는 문장으로 적는다. 「더 알아본다」로는 닫히지 않는다 — 「seed(1000) 뒤 ANALYZE highlights 를 돌리고 Plan B 를 다시 잰다」처럼 적는다.

그 아래 닫는 조건을 한 줄 붙인다. 어떤 결과가 나오면 이 질문을 닫거나 Decision 으로 넘기는지 적지 않으면 검증을 마쳐도 질문이 그대로 열려 있다. 계약의 decision-criterion 이 이 줄이다.

## 다음 검증

1. seed(1000) 뒤 `ANALYZE highlights` 를 돌리고 Plan B 를 다시 잰다

닫는 조건 : Plan B 의 실제 행 수가 추정치의 2배 안에 들어오면 닫고, 벗어나면 통계 갱신 주기를
정하는 Decision 으로 넘긴다

Decision — 9건

칸은 근거 · 결정문 · 판단 이유 · 영향. 다른 다섯과 달리 관계 절 이름이 「근거」다. decisionStatus 는 frontmatter 에 있다. 근거가 1개 이상 없으면 게시가 거절된다.

무엇을
결정문 「~한다」로 끝나는 문장. 조건이 있으면 한 문단 더
판단 이유 무엇을 보고 그렇게 정했나. 근거로 건 기록을 가리킨다
영향 감수한 비용을 포함한다. 좋아진 것만 적지 않는다

대안을 적고 왜 고르지 않았는지 적는다. 대안이 없으면 결정이 아니라 사실이다. 자료가 뒷받침하지 않는 이유를 만들지 않는다.

본문이 있는 세 종류의 공통 규칙

  • 그림이 필요한지와 무엇을 그릴지는 choosing-a-diagram.md 가 정한다 — 종류마다 그림이 답해야 할 물음이 다르다. Case 는 순서, Concept 은 구조나 변환 사슬이 대개 논지다
  • 그림은 technical-visualizer 로 만든다. 손으로 SVG 를 그리지 않는다
  • 그림 안에는 이름만 넣는다. 문장은 <desc> 와 옆 문단에 둔다
  • 그림과 증거는 frontmatter 의 assets · evidence 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다
  • assets 는 본문이 있는 세 종류만 갖는다. Reference·Question·Decision 은 그림을 렌더링할 곳이 없어서 선언해도 화면에 나오지 않는다
  • 본문은 <!-- body:start --><!-- body:end --> 사이다. 그 밖은 Studio 로 가지 않는다

관계를 어디서 가져오나

관계는 다른 기록을 가리키는 링크다. 지어내지 않는다. 분해 계약(tech-log-tree.json)이 노드마다 relations 를 적어 두면 그것을 그대로 옮긴다.

계약이 관계를 적지 않은 노드는 한 가지 규칙만 쓸 수 있다 — Reference 의 근거 사건은 같은 source 리프의 판정이 소유한다. 그래서 같은 분석 문서에서 나온 Reference 와 Case 는 서로 걸 수 있다. 그 밖의 짝은 읽고 정해야 한다. 「같은 모듈이다」는 관계가 아니라 분류다.

## 관계

- **SQL 실패가 재시도 불가로 분류된다**
  이 규칙의 근거는 같은 분석 리프의 판정이 소유한다.

Studio 는 Decision 만 관계를 1개 이상 요구한다. 나머지는 비어도 게시되지만, 관계 없는 기록은 다른 기록에서 도달할 수 없다.

Studio 파서가 거절하는 세 가지

check_body.mjs 가 잡는다. 셋 다 글자를 바꾸지 않고 고칠 수 있다.

쓴 것 파서가 읽는 것 고치는 법
`:11`~`:14` ~…~ 를 취소선으로 `:11`\~`:14`
afterPropertiesSet(:43) :43 을 인라인 디렉티브로 (\:43) 또는 백틱으로 감싼다
:::note 안에 문단 둘 거절 문단 하나만 담거나 절 제목 아래 평문으로 푼다

## 확인하지 못한 것 처럼 절 제목이 이미 무엇인지 말하는 자리에서는 :::note 로 다시 감싸지 않는다. 제목과 콜아웃이 같은 말을 두 번 한다.