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>
This commit is contained in:
DongHyeonka
2026-09-17 11:02:02 +09:00
co-authored by Claude Opus 5
parent 2109f726fe
commit ab59130196
1524 changed files with 3160026 additions and 8369 deletions
@@ -1,6 +1,6 @@
---
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"
@@ -77,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).
5. Pick **Questions** from the unresolved section (§11).
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
`dispositionReview` to `CONFIRMED` only for the ones a person actually re-read.
8. Group `PROMOTE` candidates into Topics. Write one reader question per Topic.
@@ -89,6 +91,20 @@ They do not hold candidates, and in a folded project they are gone.
`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.
@@ -141,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
verification and a closing criterion.
- **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
> 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
denominator justifies a long `final/document.md`, not a long tree.
@@ -46,6 +46,7 @@
| Reference | 다음 프로젝트에도 적용할 규칙이며 적용 조건과 예외가 있다 | Case 결론을 선언문으로 바꾼 것 |
| Question | 답이 아직 없고, 답에 따라 설계가 달라지며, 다음 검증과 종료 기준이 있다 | 실행하지 않은 테스트 목록, 막연한 "다른 방법은?" |
| Decision | 대안 중 프로젝트가 실제 방향을 정했고 근거와 감수한 비용이 있다 | 기술이 존재한다는 사실, 권장사항, 아직 정하지 않은 방향 |
| Setup | 남이 자기 기계에서 실행할 명령과 구성 값이 있고 프로젝트가 정해져 있다 | 글쓴이만 다시 돌릴 재현 순서(그 Case 의 재현 조건이다), 명령 없는 구성 설명 |
## Case 를 언제 합치나
@@ -40,6 +40,13 @@
- [ ] 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.
## 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
- [ ] The rule is reusable beyond the originating incident.
@@ -102,7 +102,8 @@
"classification": "대안을 두고 프로젝트가 실제로 고른 방향이다",
"relations": ["case:eager-to-one-n-plus-one"]
}
]
],
"setup": []
}
}
},
@@ -149,4 +150,5 @@
- 후보 셋 중 하나만 글감이 됐다. `MERGE_INTO``KEEP_IN_SSOT` 이 없는 분해는 선별하지 않은 분해다.
- Concept 은 Case 를 먼저 고른 뒤에 그것을 읽는 데 필요해서 더했다.
- Question 에 `decision-criterion` 이 있다. 무엇이 나오면 닫는지를 적지 않으면 검증을 마쳐도 열려 있다.
- 섯 종류를 억지로 채우지 않아도 된다. 여기서 다섯이 다 있는 은 실제로 다섯이 있었기 때문이다.
- 섯 종류를 억지로 채우지 않아도 된다. 여기서 다섯이 다 있는 까닭은 실제로 다섯이 있었기
때문이고, 여섯 번째인 `setup` 은 비어 있다 — 이 프로젝트에는 남이 따라 할 절차가 없었다.
@@ -90,8 +90,14 @@ python3 scripts/studio-body.py <기록.md> --key <저장소 key>=<서버가 준
줄이 모자란 채로 채우면 뒤엣것이 조용히 버려진다. 셀렉터와 배치 실행 방법은
[references/playwright-recipes.md](references/playwright-recipes.md).
**본문이 없는 세 종류(Reference·Question·Decision)의 칸은 평문으로 렌더링된다.** 백틱과
파이프가 글자 그대로 보이고, 줄바꿈은 `<br>` 로만 살아난다.
**본문이 없는 세 종류(Reference·Question·Decision)의 칸은 마크다운 블록 파서를 안 거친다.**
그래도 전부 글자로 나오지는 않는다 — 렌더러(`tech-log-frontend`
`presentation/shared/public-render/prose-text.tsx`)가 **백틱 쌍은 인라인 `<code>` 로** 살리고,
빈 줄은 문단으로, 한 줄 바꿈은 `<br>` 로 남긴다. **글자 그대로 나오는 것은 별표·파이프·`#`·
코드펜스·인용 표지 `>` 다.** 백틱을 빼지 않는다 — 빼면 식별자가 민무늬로 나온다.
**환경 구성에도 되풀이 칸이 하나 있다.** `고정한 버전` 은 줄마다 `이름`·`버전` 입력 둘이고
「버전 추가」 버튼으로 늘린다. 여기도 줄 수를 먼저 맞추고 값을 넣는다.
### 4. 저장한다
@@ -128,16 +134,26 @@ python3 scripts/verify-tech-log-tree.py <프로젝트>
## 시험용 초안을 남기지 않는다
확인하려고 만든 작업본은 지운다. **섯 종류 전부 삭제 경로가 있다**
`tech-log-backend` @ `a000f87``ManagementDocumentController` 에서 센 것이다.
확인하려고 만든 작업본은 지운다. **섯 종류 전부 삭제 경로가 있다.** 앞의 다섯은
`tech-log-backend` @ `a000f87``ManagementDocumentController` 에서 셌고, 환경 구성은
`tech-log-frontend` @ `9e5642c``management-api.openapi.yaml:606-628`(`deleteSetupDraft`)
에서 읽었다. 경로 앞머리가 줄마다 다른 것은 두 문서가 각자 적는 대로 옮겼기 때문이다.
| 종류 | 경로 |
|---|---|
| Case | `DELETE /v1/studio/cases/{id}` (`:72`) |
| Reference | `DELETE /v1/studio/references/{id}` (`:81`) |
| Concept | `DELETE /v1/studio/concepts/{id}` (`:95`) |
| Question | `DELETE /v1/studio/questions/{id}` (`:114`) |
| Decision | `DELETE /v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) |
| 종류 | 경로 | 본문 |
|---|---|---|
| Case | `DELETE /v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
| Reference | `DELETE /v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
| Concept | `DELETE /v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
| Setup | `DELETE /api/v1/studio/setups/{id}` | `{"expectedVersion": <저장 버전>}` |
| Question | `DELETE /v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
| Decision | `DELETE /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 은 계약에 삭제 경로가 없다」고 적혀 있었고 그것은 틀렸다.**
확인 없이 적힌 문장이 옮겨 다녔다 — 이 배치에서 그 문장을 코드 주석과 보고서로 다시 옮긴
@@ -17,19 +17,23 @@
종류를 라디오로 고르고 `작업본 만들기` 를 누른다. 라디오의 이름은 화면에 보이는 그대로다.
| 종류 | 라디오 이름 | 화면이 적어 놓은 칸 |
| 계약 `kind` | 라디오 이름 | 화면이 적어 놓은 칸 |
|---|---|---|
| Case | `Case` | 문제 · 결론 · 환경 · 재현 · 본문 |
| Reference | `Reference` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| Concept | **`개념`** | 기준 버전 · 본문 |
| Question | `Question` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| Decision | `Decision` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
| `CASE` | `검증 기록` | 문제 · 결론 · 환경 · 재현 · 본문 |
| `REFERENCE` | `적용 기준` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| `CONCEPT` | `동작 원리` | 기준 버전 · 본문 |
| `SETUP` | `환경 구성` | 버전 · 본문 |
| `QUESTION` | `열린 질문` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| `PROJECT_DECISION` | `설계 결정` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
**Concept 만 라디오 이름이 한글(`개념`)이다.** 나머지 넷은 영어다.
**여섯 다 한글이다.** 이 표는 전에 「Concept 만 라디오 이름이 한글(`개념`)이다. 나머지 넷은
영어다」라고 적고 있었고 그것은 낡았다 — 2026-09-12 에 `/studio/documents/new` 를 열어 여섯
라디오의 이름을 그대로 읽었다. **화면 문구는 이렇게 조용히 바뀐다.** 표를 외워서 넣지 말고
스냅샷으로 읽은 이름을 쓴다.
```js
await page.goto('https://hyeonworks.com/studio/documents/new');
await page.getByRole('radio', { name: /^Reference/ }).check();
await page.getByRole('radio', { name: /^환경 구성/ }).check();
await page.getByRole('button', { name: '작업본 만들기' }).click();
// 주소가 /studio/documents/<uuid>/edit 로 바뀐다. 그 uuid 를 기록 frontmatter 에 적는다
```
@@ -17,12 +17,32 @@
|---|---|---|
| **Case** | `관계` · `문제` · `결론` · `검증 환경` · `재현 조건` · `본문` | 있음 |
| **Concept** | `관계` · `본문` | 있음 |
| **Setup** | `관계` · `본문` — 본문 안의 `##` 는 칸이 아니다 | 있음 |
| **Reference** | `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시` | 없음 |
| **Question** | `관계` · `사실` · `가정` · `미지수` · `제약` · `선택지` · `다음 검증` | 없음 |
| **Decision** | **`근거`** · `결정문` · `판단 이유` · `영향` | 없음 |
**Decision 만 관계 절 이름이 `근거` 다.** 그리고 근거가 1개 이상 없으면 게시가 거절된다.
## 환경 구성은 `##` 를 칸으로 세지 않는다
다른 다섯은 `## <이름>` 하나가 칸 하나다. 환경 구성은 화면 칸이 `고정한 버전`
`절차 Markdown` 둘뿐이고, **`## 실행 절차` · `## 구성 값` · `## 확인 방법` 은 그 `절차 Markdown`
안의 소제목**이다. 계약이 `bodyMarkdown` 설명에 「절 이름을 강제하지 않는다 — 프로젝트마다
셋업의 모양이 다르다」고 적는다.
그래서 넣는 법이 다르다. 기록의 `## 본문` 아래 전체가 `bodyMarkdown` 한 칸으로 들어가고, 화면
칸에 따로 옮길 값은 `고정한 버전` 하나뿐이다.
| 기록 `.md` | 어디로 |
|---|---|
| frontmatter `pinnedVersions[]` | `고정한 버전` — 줄마다 `이름`·`버전` 입력 둘 |
| `## 본문``<!-- body:start -->`~`<!-- body:end -->` | `절차 Markdown` 통째로 |
| `## 관계` | `관계` |
작업본을 만들면 `절차 Markdown` 이 비어 있지 않다. Studio 가 위의 절 셋을 미리 넣어 두므로,
본문을 넣기 전에 그 내용을 지운다.
## 화면의 라벨은 기록의 절 이름과 다르다
**이것이 이 문서에서 가장 자주 틀리는 자리다.** 위 표는 기록 `.md` 가 쓰는 이름이고,
@@ -40,8 +60,9 @@ Reference 에서 실제로 확인한 대응이다.
| `예시` | `예시` |
| `관계` | `관계` |
종류 이름도 자리마다 다르다. 상태 레일 Reference 를 **`적용 기준`** 이라고 부르고,
`새 문서` 화면의 라디오는 `Reference` 다.
종류 이름도 화면마다 달랐다. 상태 레일 Reference 를 **`적용 기준`** 이라고 부르는 동안
`새 문서` 화면의 라디오는 `Reference` 다. 2026-09-12 에는 `새 문서` 쪽도 여섯 다 한글이다 —
검증 기록 · 적용 기준 · 동작 원리 · 환경 구성 · 열린 질문 · 설계 결정.
**화면을 먼저 스냅샷으로 읽고 그 라벨을 쓴다.** 이 표를 외워서 넣지 않는다 — 화면이 바뀌면
표가 먼저 낡는다.
@@ -65,6 +86,7 @@ Reference 에서 실제로 확인한 대응이다.
| `slug` · `title` | 화면 위쪽의 슬러그·제목 칸 |
| `topic` · `topicName` · `project` | 주제·프로젝트 선택 |
| `basisVersion` (Concept) | 기준 버전 칸 |
| `pinnedVersions` (Setup) | 고정한 버전 칸. `name` · `version` 이 한 줄 |
| `questionStatus` (Question) · `decisionStatus` (Decision) | 상태 선택 |
| `assets[].file` | 올릴 Asset 파일 |
| `assets[].key` | 본문 `:::evidence key` 의 저장소 쪽 이름. 올리면 서버 키로 바뀐다 |
@@ -82,7 +104,7 @@ Decision 의 도메인 `ACCEPTED` 가 화면에서는 `ADOPTED` 로 보인다.
- 줄바꿈은 `<br>` 로만 살아난다
- 나열은 쉼표로 잇지 말고 `이름 : 값` 으로 줄을 나눈다
코드·표·그림이 필요하면 짝이 되는 CaseConcept 에 담고 `관계` 로 가리킨다.
코드·표·그림이 필요하면 짝이 되는 Case·Concept·Setup 에 담고 `관계` 로 가리킨다.
## 본문을 넣기 전에
@@ -103,8 +103,18 @@ const ACRONYM_OK = new Set([
'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) {
return src
return stripFrontMatter(src)
.replace(/```[\s\S]*?```/g, (m) => m.replace(/[^\n]/g, ' '))
.replace(/`[^`\n]*`/g, (m) => ' '.repeat(m.length))
// 표와 인용은 「쓰지 않는다」 예시가 사는 자리다. 규칙을 적은 문서가 그 규칙을 어긴 것으로
@@ -36,19 +36,50 @@ metadata:
## 일곱 단계
| # | 단계 | 스킬 | 산출물 |
|---|---|---|---|
| S1 | 코드베이스 → SSOT | `analyzing-codebase-for-tech-log` | `docs/<프로젝트>/final/document.md` |
| S2 | SSOT → 분해 계약 | `deriving-tech-log-root-tree` | `docs/<프로젝트>/tech-log-studio/tech-log-tree.json` |
| S3 | 글감 → 기록 | `writing-tech-log-records` | `.../<주제>/<종류>/<기록>.md` |
| S4 | 기록 → 그림 | `technical-visualizer` | `final/assets/<이름>/` · `final/.techviz/<이름>/` |
| S5 | AI 티 제거 | `rewriting-technical-prose-naturally` | 같은 기록 파일 (제자리 수정) |
| S6 | 일한 사람의 목소리 | `writing-as-the-person-who-did-it` | 같은 기록 파일 (제자리 수정) |
| S7 | Studio 저장 | `publishing-tech-log-to-studio` | Studio 작업본 + `studio:` URL. **게시하지 않는다** |
| # | 단계 | 스킬 | 에이전트 | 산출물 |
|---|---|---|---|---|
| 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 을 쓴 뒤. 손으로 끝까지 칠 수 있는지 읽는다 | 실제로 치지는 않는다 |
## 순서가 고정된 곳
세 자리는 바꾸면 결과가 틀어진다.
@@ -108,8 +139,15 @@ python3 scripts/verify-pipeline-run.py --init runs/<프로젝트>/<runId>/run.js
`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) 의 것을 쓴다.
프롬프트에 **반드시** 들어가야 하는 넷이 있다.
@@ -135,6 +173,12 @@ error 0 이어야 런이 끝난 것이다. 이 검사기가 보는 것은 결과
준수**다 — 단계가 빠졌는지, 스킬 영수증이 그 스킬의 실제 문장인지, 관문이 돌았고 종료 코드가
0 이었는지, 적어 낸 산출물이 디스크에 있는지.
영수증은 셋이 아니라 **넷으로 갈린다.** 지금 SKILL.md 에 있으면 통과, 그 스킬의 과거
커밋에만 있으면 warn(그 뒤에 스킬이 고쳐졌다 — 어느 커밋에 있었는지 함께 적는다), 어느
판에도 없으면 error, 과거를 볼 수 없었으면(git 이 없다 · 이력 상한에 걸렸다) 또 다른
warn 이다. **warn 은 통과가 아니다** — 요약 줄이 「대조 못 한 영수증」을 따로 센다.
지난 런의 영수증이 warn 으로 바뀌었다고 원장을 고쳐 쓰지 않는다. 그것은 영수증이다.
### 3. 프로젝트 검사기를 돌린다
```bash
@@ -161,3 +205,4 @@ python3 scripts/check-figure-text.py <프로젝트>
| SSOT 에 없는 인용이 있다 | S3 이 앞 기록에서 코드를 옮겨 적었다 | `check_evidence` 종료 코드 ≠ 0 |
| 기록이 색인에 없다 | S2 를 건너뛰고 S3 을 했다 | S2 가 `SKIPPED` 인데 사유가 없다 |
| Studio 에서 그림이 안 보인다 | S7 이 Asset 을 올리기 전에 본문을 넣었다 | S7 관문에 미리보기 확인이 없다 |
| 스킬은 열었는데 결과가 그 역할 같지 않다 | 일반 에이전트를 띄웠다 | `runBy` 가 계약 이름이 아니다 |
@@ -1,6 +1,12 @@
# 단계 계약
단계마다 을 정한다 — **입력 · 스킬 · 관문 · 산출물.** 원장에 적히는 것도 이 넷이다.
단계마다 다섯을 정한다 — **입력 · 스킬 · 에이전트 · 관문 · 산출물.** 원장에 적히는 것도
이 다섯이다.
**에이전트는 그때그때 만들지 않는다.** `.claude/agents/<이름>.md` 가 그 단계의 「하는 일 ·
안 하는 일 · 관문 · 보고」를 적고 있고, `Agent` 도구의 `subagent_type` 에 그 이름을 준다.
배정의 정본은 `scripts/verify-pipeline-run.py``STAGES` 이고, 원장의 `runBy` 가 그 이름을
담는다 — 검사기가 계약과 대조한 뒤 그 `.md` 가 실재하는지까지 본다.
관문은 종료 코드가 0 이어야 지난 것이다. 0 이 아니면 그 단계는 `FAILED` 이고 다음 단계로
넘어가지 않는다.
@@ -12,6 +18,7 @@
| | |
|---|---|
| 스킬 | `analyzing-codebase-for-tech-log` |
| 에이전트 | `ssot-analyst``.claude/agents/ssot-analyst.md` |
| 입력 | 분석 대상 저장소 경로(사용자가 준다) · 그 저장소의 `AGENTS.md`(있으면) |
| 산출물 | `docs/<프로젝트>/final/document.md` |
| 관문 | `python3 scripts/verify-project-layout.py <프로젝트>` |
@@ -62,6 +69,7 @@ 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) |
@@ -100,6 +108,7 @@ SSOT 가 코드와 **어긋나는** 것을 찾으면 그것은 보강 후보가
| | |
|---|---|
| 스킬 | `writing-tech-log-records` |
| 에이전트 | `record-writer``.claude/agents/record-writer.md` |
| 입력 | `tech-log-tree.json` 의 노드 하나 · 그 노드의 `source` 앵커가 가리키는 SSOT 절 |
| 산출물 | `docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md` |
| 관문 | 아래 셋 |
@@ -127,6 +136,7 @@ node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs <프로
| | |
|---|---|
| 스킬 | `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` 로 눈 확인 |
@@ -173,6 +183,7 @@ python3 scripts/check-figure-overlap.py --file 그림.svg
| | |
|---|---|
| 스킬 | `rewriting-technical-prose-naturally` |
| 에이전트 | `prose-rewriter``.claude/agents/prose-rewriter.md` |
| 입력 | S3·S4 를 지난 기록 `.md` (제자리 수정) |
| 산출물 | 같은 파일 |
| 관문 | `check_prose.mjs` error 0 · `style_profile.mjs` · S3 관문 재실행 |
@@ -202,6 +213,7 @@ node $S/style_profile.mjs <기록.md>
| | |
|---|---|
| 스킬 | `writing-as-the-person-who-did-it` |
| 에이전트 | `voice-writer``.claude/agents/voice-writer.md` |
| 입력 | S5 를 지난 기록 `.md` · **그리고 그 기록의 상류 자료** (SSOT · 커밋 메시지 · 주석 · `확인하지 못한 것` 칸) |
| 산출물 | 같은 파일 |
| 관문 | `check_voice.mjs` · `check_prose.mjs` 재실행 · S3 관문 재실행 |
@@ -228,6 +240,7 @@ node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs
| | |
|---|---|
| 스킬 | `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` |
@@ -243,12 +256,12 @@ Asset 을 본문보다 먼저 올린다. 순서를 뒤집으면 미리보기가
## 관문 요약
| 단계 | 명령 |
|---|---|
| S1 | `verify-project-layout.py <프로젝트>` |
| S2 | `build-tech-log-tree.py``verify-tech-log-tree.py` (error 0) |
| S3 | `studio-body.py``check_body.mjs` · `check_prose.mjs` · `check_evidence.mjs --repo` |
| S4 | `techviz lint` · `check-figure-text.py` · `check-figure-overlap.py` · `preview-figure.py` (눈 확인) |
| S5 | `check_prose.mjs` (error 0) · `style_profile.mjs` · S3 관문 |
| S6 | `check_voice.mjs` · `check_prose.mjs` · S3 관문 |
| S7 | `저장됨` 확인 · `build-tech-log-tree.py``verify-tech-log-tree.py` |
| 단계 | 에이전트 | 명령 |
|---|---|---|
| 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` |
@@ -2,6 +2,17 @@
단계마다 에이전트를 하나 띄운다. 아래를 그대로 쓰고 `<...>` 만 바꾼다.
**에이전트를 새로 만들지 않는다.** 단계마다 맡을 에이전트가 `.claude/agents/` 에 있고,
`Agent` 도구의 `subagent_type` 에 그 이름을 준다. 절마다 첫 줄에 적혀 있다.
```
Agent(subagent_type="record-writer", prompt=<아래 S3 프롬프트>)
```
에이전트 정의가 그 단계의 「하는 일 · 안 하는 일 · 관문 · 보고」를 이미 적고 있다. 그래서
프롬프트는 **이번 런의 입력 경로와 글감**을 준다 — 아래 것을 그대로 쓰되, 정의와 어긋나는
지시를 프롬프트로 덮어쓰지 않는다.
## 모든 프롬프트에 들어가는 넷
1. **스킬 이름과 「SKILL.md 를 끝까지 먼저 읽어라」.** 요약을 주지 않는다. 요약을 주면
@@ -42,6 +53,8 @@ S1 처럼 산출물이 `.md` 인 단계는 그래서 한 줄을 더한다.
## S1 — 코드베이스 → SSOT
**에이전트:** `ssot-analyst``subagent_type="ssot-analyst"`
```
너는 Tech Log 파이프라인의 1단계를 맡는다.
@@ -72,6 +85,8 @@ skillEcho 는 방금 읽은 SKILL.md 에서 네 작업에 해당하는 규칙
## S2 — SSOT → 분해 계약
**에이전트:** `tree-deriver``subagent_type="tree-deriver"`
```
너는 Tech Log 파이프라인의 2단계를 맡는다.
@@ -104,6 +119,8 @@ error 0 까지 고쳐라.
## S3 — 글감 → 기록
**에이전트:** `record-writer``subagent_type="record-writer"`
```
너는 Tech Log 파이프라인의 3단계를 맡는다.
@@ -112,6 +129,11 @@ error 0 까지 고쳐라.
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 인지 먼저 확인해라. 아니면 쓰지 마라.
@@ -138,6 +160,8 @@ error 0 까지 고쳐라.
## S4 — 기록 → 그림
**에이전트:** `diagram-maker``subagent_type="diagram-maker"`
```
너는 Tech Log 파이프라인의 4단계를 맡는다.
@@ -178,6 +202,8 @@ error 0 까지 고쳐라.
## S5 — AI 티 제거
**에이전트:** `prose-rewriter``subagent_type="prose-rewriter"`
```
너는 Tech Log 파이프라인의 5단계를 맡는다.
@@ -212,6 +238,8 @@ document-skeleton.md · article-shape.md · korean-tech-blog-register.md.
## S6 — 일한 사람의 목소리
**에이전트:** `voice-writer``subagent_type="voice-writer"`
```
너는 Tech Log 파이프라인의 6단계를 맡는다.
@@ -246,6 +274,8 @@ check_voice.mjs 는 목소리가 모자란지 재지 않는다. 지어낸 목소
## S7 — Studio 저장
**에이전트:** `studio-validator``subagent_type="studio-validator"`
```
너는 Tech Log 파이프라인의 7단계를 맡는다.
@@ -1,5 +1,5 @@
{
"schemaVersion": 1,
"schemaVersion": 2,
"runId": "<YYYY-MM-DD-HHMM>",
"project": "<프로젝트>",
"record": "<docs/<프로젝트>/tech-log-studio/<주제>/<종류>/<기록>.md — 이 런이 만드는 기록>",
@@ -10,10 +10,11 @@
"id": "S1",
"name": "코드베이스 → SSOT",
"skill": "analyzing-codebase-for-tech-log",
"runBy": "subagent",
"runBy": "ssot-analyst",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -23,10 +24,11 @@
"id": "S2",
"name": "SSOT → 분해 계약",
"skill": "deriving-tech-log-root-tree",
"runBy": "subagent",
"runBy": "tree-deriver",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -36,10 +38,11 @@
"id": "S3",
"name": "글감 → 기록",
"skill": "writing-tech-log-records",
"runBy": "subagent",
"runBy": "record-writer",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -49,10 +52,11 @@
"id": "S4",
"name": "기록 → 그림",
"skill": "technical-visualizer",
"runBy": "subagent",
"runBy": "diagram-maker",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -62,10 +66,11 @@
"id": "S5",
"name": "AI 티 제거",
"skill": "rewriting-technical-prose-naturally",
"runBy": "subagent",
"runBy": "prose-rewriter",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -75,10 +80,11 @@
"id": "S6",
"name": "일한 사람의 목소리",
"skill": "writing-as-the-person-who-did-it",
"runBy": "subagent",
"runBy": "voice-writer",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -88,10 +94,11 @@
"id": "S7",
"name": "Studio 저장",
"skill": "publishing-tech-log-to-studio",
"runBy": "subagent",
"runBy": "studio-validator",
"status": "PENDING",
"skipReason": "",
"skillEcho": "",
"skillRevision": null,
"inputs": [],
"outputs": [],
"gates": [],
@@ -0,0 +1,410 @@
---
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.
## 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 |
## 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
## 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
Tech Log Studio에 올릴 기록을 쓰는 스킬. Case · Concept · Reference · Question · Decision 다섯 종류의
종류 선택, 칸 채우기, Case 본문 작성, 게시 전 대조를 다룬다.
Tech Log Studio에 올릴 기록을 쓰는 스킬. Case · Concept · Setup · Reference · Question · Decision
여섯 종류의 종류 선택, 칸 채우기, 본문 작성, 게시 전 대조를 다룬다.
## 파일
| 파일 | 무엇 |
|---|---|
| `SKILL.md` | 진입점. 종류 선택과 절차 |
| `references/record-kinds.md` | 섯 종류의 칸·상한·게시 조건 |
| `references/record-kinds.md` | 섯 종류의 칸·상한·게시 조건 |
| `references/from-ssot-to-records.md` | 긴 글에서 글감을 뽑는 기준과 `tech-log-tree.json` |
| `references/writing-each-kind.md` | 종류마다 무엇을 어떤 순서로 쓰나 |
| `templates/*.md` | 종류별 빈 틀. 복사해서 채운다 |
@@ -56,5 +56,5 @@ FAIL 초안.md:3:1 unsupported block syntax: html
## 알아둘 제약
코드·표·다이어그램·이미지는 **본문에만** 들어간다. 본문이 있는 종류는 CaseConcept 이다. Reference·Question·Decision의 모든
칸은 평문으로 렌더링된다. 설계상 그렇다 — 본문을 가진 종류는 Case뿐이다.
코드·표·다이어그램·이미지는 **본문에만** 들어간다. 본문이 있는 종류는 Case·Concept·Setup 셋이고,
Reference·Question·Decision의 모든 칸은 평문으로 렌더링된다.
@@ -1,6 +1,6 @@
---
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:
version: "1.1.0"
language: "ko-KR"
@@ -12,21 +12,26 @@ metadata:
## 개요
Studio는 섯 종류를 구분한다. 종류를 잘못 고르면 채울 칸이 맞지 않는다.
Studio는 섯 종류를 구분한다. 종류를 잘못 고르면 채울 칸이 맞지 않는다.
| 종류 | 쓰는 때 | 본문 |
|---|---|---|
| **Case** | 내가 재현하고 검증해 결론을 냈다 | 있음 |
| **Concept** | 남의 것이 어떻게 동작하는지 읽고 정리했다 | 있음 |
| **Setup** | 남이 자기 손으로 따라 할 절차를 남긴다 (`SETUP`, 화면 이름 「환경 구성」) | 있음 |
| **Reference** | 반복 적용할 기준을 굳혔다 | 없음 |
| **Question** | 아직 판단이 안 끝났다 | 없음 |
| **Decision** | 프로젝트가 방향을 정했다 (`PROJECT_DECISION`) | 없음 |
**Setup 만 끝난 일을 적지 않는다.** 나머지 다섯은 이미 일어난 일을 적고, Setup 은 읽는 사람이
자기 기계에서 실행할 순서를 적는다. 그래서 본문에 명령이 들어가고, 버전만 본문 밖의
`pinnedVersions` 에 남는다. 프로젝트가 필수이고 주제는 비워도 된다.
## 절대 규칙
**코드·표·다이어그램·이미지는 본문에만 들어간다. 본문이 있는 종류는 CaseConcept 둘뿐이다.**
본문이 없는 세 종류의 칸은 평문으로 렌더링돼 백틱과 파이프가 글자 그대로 보인다. 그런 자료는 Case 나
Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
**코드·표·다이어그램·이미지는 본문에만 들어간다. 본문이 있는 종류는 Case·Concept·Setup 셋이다.**
본문이 없는 세 종류(Reference·Question·Decision)의 칸은 평문으로 렌더링돼 백틱과 파이프가 글자
그대로 보인다. 그런 자료는 본문이 있는 종류에 담고 `관계`로 가리킨다. `references/record-kinds.md`
## 필수 절차
@@ -37,17 +42,22 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
사람이 다시 읽지 않았다는 뜻이라, 그 위에 쓴 글은 과분류를 그대로 물려받는다. 계약 밖에서 쓴
기록은 색인에 `unlisted` 로 남는다. 그 노드의 `ssot-assets`·`ssot-evidence` 도 함께 본다 —
SSOT 가 이미 그린 그림과 이미 돌린 측정 가운데 이 글감에 배정된 것이 거기 적혀 있다.
1. **종류 선택** — 위 표. 애매하면 "재현했나"를 묻는다.
1. **종류 선택** — 위 표. 애매하면 「끝난 일을 적나, 남이 따라 할 절차를 적나」를 먼저 묻고,
끝난 일이면 "재현했나"를 묻는다.
2. **칸 채우기** — 칸과 게시 조건은 `references/record-kinds.md`.
3. **본문 작성**(Case·Concept) — **종류마다 무엇을 어떤 순서로 쓰는지는
`references/writing-each-kind.md`.** 문법은 `references/body-syntax.md`, 표·코드·그림은
3. **본문 작성**(Case·Concept·Setup) — **종류마다 무엇을 어떤 순서로 쓰는지는
`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/ai-tells.md`.** 이 둘은 첫 초안부터 적용한다 — AI 티를
남겨 두고 나중에 걷어내는 순서가 아니다. **문체를 손보기 전에 문장을 고른다** — 설명이 끝난
뒤에 붙은 평가·예고·되풀이·독자 오해 가정을 먼저 뺀다(`ai-tells.md` 첫 절). 문체 규칙의
정본은 `ai-tells.md` 다.
**그림이 필요한지, 필요하면 무엇을 그릴지는 `references/choosing-a-diagram.md`.** 자리(Case·
Concept 만) · 표가 아닌지 · 옆 문단이 이미 말하지 않았는지 세 관문을 지나야 그린다.
**그림이 필요한지, 필요하면 무엇을 그릴지는 `references/choosing-a-diagram.md`.** 담을 곳
(Case·Concept·Setup 만) · 표가 아닌지 · 옆 문단이 이미 말하지 않았는지 세 관문을 지나야 그린다.
순서·경계·상태 전이처럼 문장만으로 따라가기 어려운 관계가 있으면 **`final/assets/` 에 그
그림이 이미 있는지부터 본다.** SSOT 를 만들 때 그려 둔 것이 있고 계약의 `ssot-assets` 가 이
글감에 배정해 두었으면 기록의 `assets`**그 파일을 그대로** 가리킨다. 사본을 따로 만들지
@@ -76,18 +86,20 @@ Concept 에 담고 `관계`로 가리킨다. `references/record-kinds.md`
## 어느 스킬이 무엇을 하나
이 스킬이 첫 초안을 만든다. 나머지는 초안이 나온 뒤에 각각 다른 것을 고친다.
이 스킬이 첫 초안을 만든다. `writing-practitioner-guides` 는 Setup 본문을 쓰는 동안 함께 열고,
나머지는 초안이 나온 뒤에 각각 다른 것을 고친다.
| 스킬 | 하는 일 | 하지 않는 일 |
|---|---|---|
| `deriving-tech-log-root-tree` | 후보에 처분을 매기고 `PROMOTE` 를 글감으로 올린다 | 글을 쓰지 않는다 |
| `writing-tech-log-records` | 종류를 고르고 칸과 본문을 쓴다. `explaining.md`·`ai-tells.md` 를 처음부터 적용한다 | — |
| `writing-practitioner-guides` | 사람이 직접 치는 명령의 형태를 정한다 — 한 줄에 담는 계층, 도구 우선순위, 출력을 읽는 형태와 값 하나만 뽑는 형태, 넓은 명령에서 좁은 명령으로 | Tech Log 의 종류와 칸을 모른다. SSOT 대조도 색인 갱신도 하지 않는다 |
| `technical-visualizer` | 문장으로 따라가기 어려운 관계를 그림으로 만든다 | 무엇을 그릴지 정하지 않는다 — `choosing-a-diagram.md` 가 정한다 |
| `rewriting-technical-prose-naturally` | 사실과 구조가 이미 맞는 초안의 번역투·반복 문형·과한 대구를 고친다 | 분류가 틀렸거나 근거가 모자란 것은 못 고친다 |
| `writing-as-the-person-who-did-it` | 자료에 남아 있는 선택·비교·어긋남·확인하지 못한 범위를 제자리에 놓는다 | 자료에 없는 「처음에는」·「고민 끝에」를 만들지 않는다 |
Reference·Question·Decision 은 그림을 렌더링할 자리가 없다. 그림이 필요한 내용은 짝이 되는
CaseConcept 에 담고 `관계`로 가리킨다.
Reference·Question·Decision 은 그림을 렌더링할 곳이 없다. 그림이 필요한 내용은 짝이 되는
Case·Concept·Setup 에 담고 `관계`로 가리킨다.
## 보호 구간
@@ -113,7 +125,7 @@ SSOT 에 없는데 필요한 인용이라면 순서가 반대다 — `final/docu
| 실패 | 대응 |
|---|---|
| Reference 규칙에 코드블록 | Case로 옮겨 관계 연결 |
| 본문 밖 칸에 백틱 | 평문이다. 백틱을 뺀다 |
| 본문 밖 칸에 백틱을 뺌 | 이 칸도 백틱은 `<code>` 로 산다. 빼는 것은 별표·파이프·`#` 다 |
| 평문 칸에 쉼표 나열 | `이름 : 값`으로 줄 나눔. 있음·없음은 `o`·`x` |
| 표 머리글이 명사뿐 | 질문으로. 비교 축은 `전`·`후` 시점으로 |
| 그림 안에 문장·숫자 | `<text>`는 이름만. 문장은 `<desc>`·옆 문단에 |
@@ -130,6 +142,11 @@ SSOT 에 없는데 필요한 인용이라면 순서가 반대다 — `final/docu
| SSOT 에 있는 그림을 두고 새로 그림 | `final/assets/` 를 먼저 본다. `ssot-assets` 가 배정한 것을 쓴다 |
| SSOT 에 있는 측정을 두고 다시 돌림 | `final/evidence/` 의 원문을 가리킨다 |
| 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` | 목록을 중첩했다 |
| `unsafe link URL: …` | 허용되지 않는 스킴이나 `//` 주소 |
| `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
```
**이것이 조용한 결함이 되는 경로가 있다.** 막히면 시각을 빼고 넘어가게 되고, 그러면
검사기는 통과하는데 **기록에서 수치가 사라진다.** 실제로 한 회차에서 두 편이 각각
시각 둘과 타이머 설정값을 빼고 통과시켰다. 수치·날짜·시각은 보호 구간이다 —
**빼지 말고 감싼다.**
표와 코드블록 안은 걸리지 않는다. 그래서 같은 프로젝트의 다른 기록이 통과하는 것이
「이 문법이 괜찮다」는 뜻이 아니다 — 그쪽은 시각이 전부 표 안에 있었을 뿐이다.
@@ -9,11 +9,11 @@
### 1. 자리가 있는가
`assets` 는 본문이 있는 종류만 갖는다 — **CaseConcept**. Reference·Question·Decision 의
칸은 평문으로 렌더링돼 그림이 들어갈 자리가 없다.
`assets` 는 본문이 있는 종류만 갖는다 — **Case·Concept·Setup**. Reference·Question·Decision 의
칸은 평문으로 렌더링돼 그림이 들어갈 곳이 없다.
파생 기록에 그림이 필요해 보이면 그 그림은 **짝이 되는 CaseConcept 의 것**이다. 거기 담고
`관계`로 가리킨다. 담을 Case 나 Concept 이 없으면 그 그림은 아직 집이 없다 — 계약의
파생 기록에 그림이 필요해 보이면 그 그림은 **짝이 되는 Case·Concept·Setup 의 것**이다. 거기 담고
`관계`로 가리킨다. 담을 기록이 없으면 그 그림은 아직 집이 없다 — 계약의
`assetLedger.unassigned` 에 그렇게 적고, 새 글감을 세울지는 따로 판단한다.
### 2. 표가 아닌가
@@ -40,11 +40,17 @@
| **Case** | 이 요청 한 번이 어떤 순서로 무엇을 지나갔나 | `sequence` · `component-flow` |
| **Case** (경계가 논지일 때) | 무엇이 어느 경계 안에 있고 무엇이 밖에 있나 | `two-zone-pipeline` |
| **Concept** | 남의 것이 어떤 순서·구조로 동작하나 | `sequence` · `component-flow` · `ports-adapters` |
| **Setup** | (아직 못 적는다 — 아래) | — |
Case 는 **내가 돌려서 본 것**이라 대개 순서가 논지다. Concept 은 **남의 것이 어떻게 동작하는지**라
구조나 변환 사슬이 논지다. 어느 쪽이든 「무엇이 무엇으로 바뀌는가」를 못 적으면 아직 그릴 것이
없다는 뜻이다.
**Setup 은 담을 곳만 있고 본보기가 없다.** 본문 파서가 Case 와 같아서 그림이 렌더링되기는 한다.
다만 2026-09-12 에 Studio 의 환경 구성 문서가 0건이라, 위 두 줄처럼 「반복된 물음」을 셀 자료가
없다. 이 줄은 그림을 그리지 말라는 뜻이 아니라 **아직 아무도 안 그려 봤다**는 뜻이다. 그릴 때는
세 관문만 그대로 지나고, 무엇을 그렸는지 이 표에 적어 둔다.
**한 절에 그림 하나.** 같은 절에 구조 그림과 흐름 그림을 둘 다 넣으면 독자가 어느 쪽을 먼저
읽어야 하는지 알 수 없다. 둘 다 필요하면 절을 나눈다.
@@ -1,6 +1,6 @@
# 코드·표·다이어그램·이미지
본문(`bodyMarkdown`)에만 해당한다. 본문이 있는 종류는 CaseConcept 이다.
본문(`bodyMarkdown`)에만 해당한다. 본문이 있는 종류는 Case·Concept·Setup 셋이다.
## 코드블록
@@ -99,6 +99,7 @@ Reference의 칸도 반쯤만 맞는 기록이 나온다. 종류를 먼저 정
|---|---|---|
| 재현한 관측 하나 | 조건을 갖추고 돌려서 얻은 수치·실행계획·로그 | Case |
| 남의 것의 동작 하나 | 문서와 코드를 읽어 정리한 규칙·흐름 | Concept |
| 남이 따라 할 절차 하나 | 그 환경을 처음 세우는 명령과 구성 값 | Setup |
| 반복 적용할 기준 하나 | 다음에도 같게 하기로 한 규칙 | Reference |
| 프로젝트가 고른 방향 하나 | 대안을 두고 정한 것 | Decision |
| 닫히지 않은 판단 하나 | 아직 모르는 것과 무엇을 하면 닫히는지 | Question |
@@ -107,6 +108,8 @@ Reference의 칸도 반쯤만 맞는 기록이 나온다. 종류를 먼저 정
순서대로 묻는다. 먼저 걸리는 것이 그 글감의 종류다.
0. **남이 자기 기계에서 따라 할 절차인가** → Setup. 나머지 다섯은 끝난 일을 적고 이것만 실행할
순서를 적는다. 명령과 구성 값이 SSOT에 있어야 하고, 프로젝트를 반드시 고른다
1. **내가 돌려서 얻은 결과인가** → Case. 수치·실행계획·로그가 SSOT에 있어야 한다. 없으면 Case가 아니다
2. **남의 것이 어떻게 동작하는지인가** → Concept. 무엇을 보고 썼는지(버전)가 있어야 한다
3. **다음에도 같게 하기로 한 규칙인가** → Reference
@@ -131,7 +134,7 @@ Reference 하나로 나누고 `관계`로 잇는다.
미리 알아야 하는 구조가 있을 때만 Concept을 만든다. 메커니즘처럼 보이는 절을 훑어 채우면 어느
기록도 필요로 하지 않는 개념이 쌓인다.
**주제 하나에 독자 질문 하나.** Studio는 주제 아래에 섯 종류를 나눠 보여 준다. 그 주제의
**주제 하나에 독자 질문 하나.** Studio는 주제 아래에 섯 종류를 나눠 보여 준다. 그 주제의
기록들이 함께 답하는 물음을 한 줄로 적고, 그 물음에 답하지 않는 글감은 다른 주제로 옮긴다.
주제 slug는 Studio의 것을 그대로 쓴다.
@@ -157,7 +160,7 @@ Reference 하나로 나누고 `관계`로 잇는다.
"status": "게시 전", "studioId": "32d0be7d-…", "assets": 1, "evidence": 3 },
{ "title": "아직 쓰지 않은 글감" }
],
"concept": [], "reference": [], "question": [], "decision": []
"concept": [], "setup": [], "reference": [], "question": [], "decision": []
}
}
}
@@ -1,24 +1,41 @@
# 섯 종류의 칸과 게시 조건
# 섯 종류의 칸과 게시 조건
칸 이름은 Studio 편집 화면 그대로, 상한과 필수 여부는 `studio-api.openapi.yaml` 그대로다.
계약 필드명을 괄호에 적는다. 계약은 `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` | 그 종류만의 칸 |
|---|---|---|
| Case | `CASE` | 문제 · 결론 · 환경 · 재현 · 본문 |
| Reference | `REFERENCE` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| **개념** | `CONCEPT` | 기준 버전 · 본문 |
| Question | `QUESTION` | 상태 · 사실 · 가정 · 미지수 · 선택지 |
| Decision | `PROJECT_DECISION` | 상태 · 결정일 · 결정문 · 판단 이유 · 영향 · 근거 |
| **검증 기록** (Case) | `CASE` | 문제 · 결론 · 환경 · 재현 · 본문 |
| **적용 기준** (Reference) | `REFERENCE` | 목적 · 규칙 · 적용 조건 · 예외 · 예시 |
| **동작 원리** (Concept) | `CONCEPT` | 기준 버전 · 본문 |
| **환경 구성** (Setup) | `SETUP` | 버전 · 본문 |
| **열린 질문** (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자까지 보인다 |
| Topic | `topicId` | — | 경고 |
| 축 | `variantIds` | 8개 | 선택. 고르지 않으면 그 주제의 **공통 기록**이 된다 |
| Project | `projectId` | — | `PROJECT_DECISION` 게시 시 필수 |
| Project | `projectId` | — | `PROJECT_DECISION``SETUP`은 필수 |
| 관계 | `relations` | 20개 | `PROJECT_DECISION`**1개 이상 필수**. 그 종류에서는 절 이름이 「근거」다 |
편집 화면에서 축은 체크박스 묶음으로 나오고 묶음 이름이 「축 — 고르지 않으면 이 주제의 공통
@@ -67,8 +84,8 @@ evidence:
- ../../../final/evidence/explain/highlights-child-plan-A.txt
```
**`assets` 는 본문이 있는 CaseConcept 에만 둔다.** 나머지 세 종류는 칸이 평문으로
렌더링돼 그림을 표시할 자리가 없다. 그림이 필요한 내용은 Case 나 Concept 에 담고 `관계`
**`assets` 는 본문이 있는 Case·Concept·Setup 에만 둔다.** 나머지 세 종류는 칸이 평문으로
렌더링돼 그림을 표시할 곳이 없다. 그림이 필요한 내용은 본문이 있는 종류에 담고 `관계`
가리킨다.
`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·도구 |
| 재현 조건 | `reproduction` | 다른 사람이 같은 결과를 얻는 방법 |
| 마지막 검증일 | `lastVerifiedOn` | 실제로 확인한 날. 30일 지나면 경고 |
| 본문 Markdown | `bodyMarkdown` | 10만 자. 본문을 갖는 종류 중 하나 |
| 본문 Markdown | `bodyMarkdown` | 10만 자. 본문을 갖는 종류 중 하나 |
공개 화면에서 `검증 환경``재현 조건``environmentSummary` 배열에 그 순서로 실린다.
## Concept — 남의 것이 어떻게 동작하는지
`새 문서` 화면에서 이 종류만 이름이 한글이다. **개념」을 고른다.** 나머지 넷은 Case·Reference·
Question·Decision 으로 적혀 있다. 화면 설명은 「개념의 내부 동작과 아키텍처를 처음부터 풀어 씁니다」다.
`새 문서` 화면에서 **동작 원리」를 고른다.** 화면 설명은 「개념의 내부 동작과 아키텍처를 처음부터
풀어 씁니다.」다. 전에 이 절은 「이 종류만 이름이 한글이다」라고 적었는데 2026-09-12 에는 여섯 다
한글이었다.
| 칸 | 필드 | 비고 |
|---|---|---|
| 본문 Markdown | `bodyMarkdown` | 10만 자. **Case 함께 본문을 갖는 종류 중 하나** |
| 본문 Markdown | `bodyMarkdown` | 10만 자. **Case·Setup 과 함께 본문을 갖는 종류 중 하나** |
| 기준 버전 | `basisVersion` | 120자. 무엇을 보고 쓴 글인지 한 줄 |
칸이 둘뿐이다. 문제도 결론도 재현 조건도 없다. 내가 재현한 결과가 아니라 **이미 그렇게 동작하는
것**을 적기 때문이다. `Authorization Code Flow 가 code 를 한 번 더 교환하는 이유`, `Forward-Auth
subrequest 가 실어 보내는 것` 같은 것이 여기 온다.
**`lastVerifiedOn` 이 없고 `basisVersion`그 자리를 대신한다.** 개념은 날짜로 낡지 않고 버전으로
**`lastVerifiedOn` 이 없고 `basisVersion`낡음을 말한다.** 개념은 날짜로 낡지 않고 버전으로
낡기 때문이다. `Keycloak 26.7.0 identity brokering`, `Kubernetes 1.31` 처럼 적는다. 비워도 게시된다.
공개 주소는 `/concepts/{slug}` 다.
@@ -143,8 +183,7 @@ subrequest 가 실어 보내는 것` 같은 것이 여기 온다.
검증도 막지 않는다(2026-09-04 에 임시 개념 기록을 만들어 비운 채로 게시해 확인했다). 그래도
채우는 편이 낫다 — 개념이 언제 낡았는지 읽는 사람이 알 방법이 이 칸뿐이다.
편집 화면 오른쪽 `작업 상태` 이 종류를 「동작 원리」라고 부른다. `새 문서` 의 「개념」과 같은
것이다.
편집 화면 오른쪽 `작업 상태` 이 종류를 「동작 원리」라고 부른다.
Case 와 헷갈리면 **내가 무엇을 했는지**를 묻는다. 내가 돌려 보고 수치를 얻었으면 Case, 남의 문서와
코드를 읽고 동작을 정리했으면 Concept 이다.
@@ -169,6 +208,87 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
시점`, `커밋 증거 상태 전이`, `fenced lease 와 CAS`, `gRPC flow control 과 backpressure`.
여러 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 — 반복 적용할 기준
| 칸 | 필드 | 비고 |
@@ -217,6 +337,10 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
## 종류 고르기
```text
남이 그대로 따라 할 절차를 적나 ── 예 ──→ Setup
아니오 (끝난 일을 적는다)
내가 직접 돌려 보고 결과를 얻었나 ── 예 ──→ Case
아니오
@@ -236,6 +360,11 @@ Concept 이 되는 것은 이런 것들이다 — `Spring 조립의 세 경로`,
└──→ Reference
```
첫 갈래가 「끝난 일을 적나, 남이 따라 할 절차를 적나」다. 나머지 다섯은 이미 끝난 일을 적고,
Setup 만 읽는 사람이 자기 기계에서 실행할 순서를 적는다. 편집 화면 주석이 그 경계를 「Case 의
「검증 환경」 같은 평문 한 칸으로는 담기지 않는다」로 적는다 — 명령이 여러 줄이고 그대로
복사돼야 하면 Case 의 평문 칸이 아니라 Setup 의 본문에 들어간다.
Case 와 Concept 이 가장 자주 헷갈린다. **수치와 재현 조건이 있으면 Case**, 읽고 정리한 동작이면
Concept 이다. Concept 과 Reference 는 「무엇이 그런가」와 「우리가 어떻게 할 것인가」로 갈린다 —
`PKCE 가 code 가로채기를 막는 방식`은 Concept, `우리는 public client 에 PKCE 를 필수로 쓴다`
@@ -27,6 +27,25 @@
- [ ] Decision에 근거 기록이 1개 이상 연결됐다
- [ ] Decision의 영향에 감수한 비용이 있다
- [ ] 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·각주·체크박스·중첩 목록·취소선이 없다
- [ ] 표를 파이프로 썼다. 감쌌다면 세 속성이 다 있다
@@ -99,7 +118,8 @@
## 평문 칸
- [ ] 본문 밖 칸에 백틱·파이프가 없다
- [ ] 본문 밖 칸에 별표·파이프·`#`·코드펜스·인용 표지 `>` 가 없다 (**백틱은 괜찮다** — 인라인 `<code>` 로 산다)
- [ ] 코드펜스를 뗀 자리에 언어 표시(` ```text ``text`)가 남지 않았다
- [ ] 나열을 쉼표로 잇지 않고 `이름 : 값`으로 끊었다
- [ ] 있음·없음을 `o`·`x`로 적었다
- [ ] 한 문장이 화면에서 두 줄을 넘지 않는다
@@ -107,7 +127,7 @@
## 연결
- [ ] Topic이 지정됐다
- [ ] Project가 필요하면 지정됐고, 그 Project에 slug가 있다
- [ ] Project가 필요하면 지정됐고, 그 Project에 slug가 있다 (Decision과 Setup은 필수다)
- [ ] 관계의 대상이 실제로 있는 공개 기록이다
- [ ] 코드·표·그림이 필요한 내용을 Reference나 Question에 밀어 넣지 않았다
- [ ] 본문이 없는 세 종류에 `assets`를 선언하지 않았다
@@ -73,7 +73,7 @@ section it names exists, and the diagram stage cannot translate the anchor into
## 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.
```json
@@ -81,13 +81,13 @@ record kinds.
"topic": "oauth-oidc-auth-boundary",
"title": "OAuth 자격증명과 세션의 보관 경계",
"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
question; one Topic does not need two. Empty kinds are allowed — do not manufacture nodes
to fill all five.
to fill all six.
## Candidates
@@ -178,6 +178,26 @@ be stale.
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.
### 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
`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).
「대개 이렇게 쓴다」는 말은 그 47건이 그렇게 돼 있다는 뜻이다.
**Setup 은 그 47건에 없다.** 2026-09-12 에 Studio 의 환경 구성 문서가 0건이었다. 그래서 Setup
절은 계약과 편집 화면에서 읽어 썼다. 나머지 다섯처럼 올라간 기록을 세지 않았다.
`clean-architecture-backend-template` 의 949건은 다른 절 이름으로 쓰여 있었고, 지금 이 기준으로
다시 판정하는 중이다. 아직 기준을 만족한 상태가 아니므로 그 949건을 본보기로 삼지 않는다. 올라간
적 없는 초안이 아니라 **올라간 것**이 기준이다.
## 파일 뼈대 — 섯 종류가 같다
## 파일 뼈대 — 섯 종류가 같다
```markdown
---
id · kind · slug · title · topic · topicName · project · status · studio
source · sourceRevision
(종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn
(있으면) evidence · assets — assets 는 Case Concept 만
(종류별) basisVersion · decisionStatus · questionStatus · lastVerifiedOn · pinnedVersions
(있으면) evidence · assets — assets 는 Case · Concept · Setup
---
# 제목
@@ -27,7 +30,7 @@ source · sourceRevision
## 관계 ← Decision 만 「근거」다
## <칸 이름> ← 종류마다 다르다
## 본문 ← Case · Concept 만
## 본문 ← Case · Concept · Setup
<!-- body:start -->
...
<!-- body:end -->
@@ -110,6 +113,159 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
「확인했다」는 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건
칸은 `관계` · `목적` · `규칙` · `적용 조건` · `예외` · `예시`. 14건 모두 여섯 칸을 채웠다. 본문이 없다.
@@ -124,7 +280,8 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
규칙 제목만 읽어도 무엇을 금지하는지 알아야 한다. 「지표를 정확히 읽는다」보다
「지표 이름이 뜻하는 것을 그대로 읽는다」가 낫다. 코드가 필요하면 그 코드가 있는 Case 를 만들고
`관계`로 가리킨다 — Reference 칸은 평문이라 백틱이 글자로 보인다.
`관계`로 가리킨다 — Reference 칸은 마크다운 블록 파서를 안 거쳐서 코드펜스가 글자로 보인다.
낱말 하나짜리 식별자는 백틱으로 감싸면 인라인 `<code>` 로 살아난다. 여러 줄짜리 코드가 문제다.
## Question — 9건
@@ -151,7 +308,7 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
## Decision — 9건
칸은 **`근거`** · `결정문` · `판단 이유` · `영향`. **다른 과 달리 관계 절 이름이 「근거」다.**
칸은 **`근거`** · `결정문` · `판단 이유` · `영향`. **다른 다섯과 달리 관계 절 이름이 「근거」다.**
`decisionStatus` 는 frontmatter 에 있다. 근거가 1개 이상 없으면 게시가 거절된다.
| 칸 | 무엇을 |
@@ -163,14 +320,14 @@ basisVersion: oauth2-proxy 7.15.2 · Nginx auth_request
대안을 적고 왜 고르지 않았는지 적는다. 대안이 없으면 결정이 아니라 사실이다. 자료가 뒷받침하지
않는 이유를 만들지 않는다.
## 본문이 있는 종류의 공통 규칙
## 본문이 있는 종류의 공통 규칙
- **그림이 필요한지와 무엇을 그릴지는 `choosing-a-diagram.md` 가 정한다** — 종류마다 그림이
답해야 할 물음이 다르다. Case 는 순서, Concept 은 구조나 변환 사슬이 대개 논지다
- 그림은 `technical-visualizer` 로 만든다. 손으로 SVG 를 그리지 않는다
- 그림 안에는 이름만 넣는다. 문장은 `<desc>` 와 옆 문단에 둔다
- 그림과 증거는 frontmatter 의 `assets` · `evidence` 로 잇는다. 같은 파일을 기록 옆에 복사하지 않는다
- `assets` 는 본문이 있는 종류만 갖는다. Reference·Question·Decision 은 그림을 렌더링할 자리가
- `assets` 는 본문이 있는 종류만 갖는다. Reference·Question·Decision 은 그림을 렌더링할 곳이
없어서 선언해도 화면에 나오지 않는다
- 본문은 `<!-- body:start -->` 와 `<!-- body:end -->` 사이다. 그 밖은 Studio 로 가지 않는다
@@ -60,6 +60,27 @@ const PROSE_FENCE = new Set(["text", "", "txt", "console", "diff"]);
// 경로·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;
// 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 studio = join(base, "tech-log-studio");
for (const topicDir of readdirSync(studio)) {
@@ -89,8 +110,10 @@ for (const topicDir of readdirSync(studio)) {
const t = raw.trim();
if (t.length < 20) 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)]);
}
}
@@ -18,7 +18,7 @@ evidence:
<!--
본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은
표시되지 않는다. 그런 자료는 근거로 건 CaseConcept 에 담는다.
표시되지 않는다. 그런 자료는 근거로 건 Case·Concept·Setup 에 담는다.
-->
# <title>
@@ -18,7 +18,7 @@ evidence:
<!--
본문이 없는 종류라 `assets` 를 두지 않는다. 칸이 평문으로 렌더링되므로 그림과 코드블록은
표시되지 않는다. 그런 자료는 CaseConcept 에 담고 `관계`로 가리킨다.
표시되지 않는다. 그런 자료는 본문이 있는 종류(Case·Concept·Setup)에 담고 `관계`로 가리킨다.
-->
# <title>
@@ -17,7 +17,8 @@ evidence:
<!--
본문이 없는 종류라 `assets` 를 두지 않는다. 이 기록의 칸은 평문으로 렌더링되므로 그림도
코드블록도 표시되지 않는다. 그림이 필요한 내용은 CaseConcept 에 담고 `관계`로 가리킨다.
코드블록도 표시되지 않는다. 그림이 필요한 내용은 본문이 있는 종류(Case·Concept·Setup)에 담고
`관계`로 가리킨다.
`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 -->