fix(setup): 실험대를 새로 세워 setup 35편을 밟고 어긋난 명령과 결과를 고친다
기반 가이드 7단계로 실험대를 철거하고 다시 세운 뒤 virtualization setup 9편과 keycloak-session-store 26편을 순서대로 밟았다. 24편은 끝까지, 11편은 되는 데까지 밟았고 밟은 범위를 편마다 적었다. 명령이 못 도는 것을 고쳤다. - kubectl 을 `kc-lab-1` 에서 치라고 적었는데 그 기계에 kubeconfig 가 없다. 라벨 639개와 각 편의 「어디서 치는가」를 `[lab host]` 로 옮겼다 - `-o custom-columns=…[0]…` 이 zsh 에서 글로브로 읽혀 안 돈다. 28곳에 따옴표 - busybox `sed` 가 끝 개행을 안 붙여 A-3 의 측정이 언제나 0 이었다 - `--token-file ~/node-token` 뒤에 그 파일을 지우면 k3s agent 가 재부팅을 못 견딘다. `/etc/rancher/node-token` 으로 옮기는 처방을 재서 넣었다 - 게스트에 없는 도구를 전제로 한 명령 넷 — `conntrack`·`dig`·`strings`·`nginx -v` - `echo` 와 JWT 헤더가 `"이름" : [ 값 ]` 으로 찍는데 문서는 공백 없이 옮겨 적어 그 실측으로 만든 grep·sed 가 한 줄도 못 잡는다 - B-0 이 `directAccessGrantsEnabled` 와 계정 완성을 빠뜨려 B-3 이 못 돈다 - D-4·D-4a 가 `test-server` 와 `certbot-renew.*` 를 가리키는데 실제로는 `kc-lab-edge` 의 `certbot.service` 다 - `virsh setmaxmem --config` 를 `dominfo` 로 판정하면 틀린다. `--inactive` 로 - `LIBVIRT_DEFAULT_URI` 를 rc 에만 넣으면 `ssh host '명령'` 에서 안 먹는다 결과가 조건부인 것을 갈랐다. - readiness 는 즉시 안 뒤집힌다. A-1·A-2 의 60초 창을 적었다 - 03 의 층 ②③ `301` 은 04 이후의 값이고 그 단계에서는 `404` 다 - A-0 의 로그 필터를 요청 직후에 치면 정반대 결론이 나온다 - A-5 의 한 방향 차단은 잠깐 `1` 이었다 `2` 로 돌아온다 증거는 두 프로젝트의 `evidence/raw/` 에 99벌을 README 와 함께 남겼다. 비밀은 길이만 적었고 화면에 찍힌 토큰은 가렸다. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
ab59130196
commit
024362d096
@@ -61,11 +61,18 @@ aside[class*="studio-document-status"] ← 이것이 안 보이면 인증 실
|
||||
### 1. 기록을 읽고 무엇을 넣을지 정한다
|
||||
|
||||
`kind` 로 칸 목록이 정해진다. 어떤 `##` 제목이 Studio 의 어느 칸인지는
|
||||
[references/studio-form-map.md](references/studio-form-map.md).
|
||||
[references/studio-form-map.md](references/studio-form-map.md), 그 칸이 서버에서 어떤 이름과
|
||||
자료형인지는 [references/studio-api.md](references/studio-api.md).
|
||||
|
||||
**frontmatter 는 메타데이터고, 본문의 `##` 가 Studio 의 칸이며, 제목 아래 첫 문단이 `요약`
|
||||
이다.** 계약에 없는 `##` 는 화면에 자리가 없어 통째로 사라진다.
|
||||
|
||||
**본문은 이제 화면으로 넣지 않는다.** 편집 화면의 본문이 블록 편집기로 바뀌어 블록 하나가
|
||||
`<textarea>` 하나다 — 기록 한 편이 블록 171개인 것을 실제로 봤다. 그런데 저장이 서버로 보내는
|
||||
것은 여전히 `bodyMarkdown` 이라는 마크다운 문자열 하나다. 그래서 **옮기는 일은 화면이 아니라
|
||||
`PUT /api/v1/studio/documents/{id}` 로 한다** — 로그인된 그 페이지 안에서 `fetch` 로 부르므로
|
||||
세션과 CSRF 가 그대로 실린다. 화면은 읽고 대조하는 데 쓴다.
|
||||
|
||||
### 2. 그림이 있으면 Asset 을 먼저 올린다
|
||||
|
||||
frontmatter `assets:` 의 `file:` 이 올릴 파일이고 `key:` 는 저장소 쪽 이름이다. 올리면 서버가
|
||||
@@ -101,6 +108,12 @@ python3 scripts/studio-body.py <기록.md> --key <저장소 key>=<서버가 준
|
||||
|
||||
### 4. 저장한다
|
||||
|
||||
API 로 넣었으면 `PUT` 이 200 을 내는 것이 저장이다. **`expectedVersion` 은 저장 직전에 `GET`
|
||||
으로 읽은 `document.version` 이다** — 지어내지 않는다. 보내지 않은 칸은 비워지므로 그 종류의
|
||||
칸을 전부 담는다.
|
||||
|
||||
화면으로 고쳤으면 버튼을 누른다.
|
||||
|
||||
```
|
||||
aside[class*="studio-document-status"] 안의 `저장` 버튼
|
||||
→ 같은 aside 의 글자가 `저장됨` 으로 바뀔 때까지 기다린다 (최대 30초)
|
||||
@@ -141,12 +154,12 @@ python3 scripts/verify-tech-log-tree.py <프로젝트>
|
||||
|
||||
| 종류 | 경로 | 본문 |
|
||||
|---|---|---|
|
||||
| Case | `DELETE /v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Reference | `DELETE /v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Concept | `DELETE /v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Case | `DELETE /api/v1/studio/cases/{id}` (`:72`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Reference | `DELETE /api/v1/studio/references/{id}` (`:81`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Concept | `DELETE /api/v1/studio/concepts/{id}` (`:95`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Setup | `DELETE /api/v1/studio/setups/{id}` | `{"expectedVersion": <저장 버전>}` |
|
||||
| Question | `DELETE /v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Decision | `DELETE /v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Question | `DELETE /api/v1/studio/questions/{id}` (`:114`) | `{"expectedVersion": <저장 버전>}` |
|
||||
| Decision | `DELETE /api/v1/studio/projects/{id}/decisions/{decisionId}` (`:123`) | `{"expectedVersion": <저장 버전>}` |
|
||||
|
||||
**본문 없이 부르면 204 가 아니라 422 다.** `ExpectedVersionRequest` 가 없으면
|
||||
`REQUEST_VALIDATION_FAILED` / `Request body is malformed` 로 거절된다. 헤더에는
|
||||
|
||||
@@ -114,31 +114,49 @@ while (cur < want) { // 모자라면 더한다
|
||||
const labels = await fs.locator('label span').allInnerTexts();
|
||||
```
|
||||
|
||||
## 칸 DOM 이 두 가지다
|
||||
## 칸은 `aria-label` 로 찾는다 (2026-09-17 에 다시 잼)
|
||||
|
||||
위 xpath 는 절반만 통한다. 실제 편집 화면에는 모양이 둘이다.
|
||||
|
||||
```html
|
||||
<label class="studio-field"><span>문제</span><textarea></textarea></label> <!-- A -->
|
||||
<div class="studio-field"><label for="studio-field-summary">요약</label>
|
||||
<textarea id="studio-field-summary"></textarea></div> <!-- B -->
|
||||
```
|
||||
|
||||
`요약` · `본문 Markdown` · `관계 N 이유` 가 B형이다. A형만 찾으면 이 칸들이 조용히 안 채워진다.
|
||||
**둘 다 본다.**
|
||||
**위의 `label` 두 모양 이야기는 낡았다.** 지금 `label.studio-field` 가 감싸는 것은 제목과 요약
|
||||
둘뿐이고, 나머지 칸은 전부 `aria-label` 을 갖는다. xpath 로 `span` 을 뒤질 일이 없다.
|
||||
|
||||
```js
|
||||
async function field(page, name) {
|
||||
const a = page.locator(`xpath=//label[./span[normalize-space(.)="${name}"]]`)
|
||||
.locator('textarea, input').first();
|
||||
if (await a.count()) return a;
|
||||
const id = await page.locator(`xpath=//label[normalize-space(.)="${name}"]`)
|
||||
.first().getAttribute('for');
|
||||
return id ? page.locator('#' + id) : null;
|
||||
}
|
||||
document.querySelector('input[aria-label="제목"]')
|
||||
document.querySelector('textarea[aria-label="요약"]')
|
||||
document.querySelector('input[aria-label="slug"]')
|
||||
document.querySelector('input[aria-label="판단 기준 1 제목"]')
|
||||
document.querySelector('textarea[aria-label="적용할 때 3"]')
|
||||
```
|
||||
|
||||
**본문 칸의 이름은 `본문` 이 아니라 `본문 Markdown` 이다.**
|
||||
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
|
||||
`+ 버전 추가` 다. 줄 수를 먼저 맞추고 값을 넣는 규칙은 그대로다.
|
||||
|
||||
## 본문 칸이 사라졌다 — 블록 편집기다
|
||||
|
||||
**`본문 Markdown` 이라는 textarea 는 더 없다.** 본문은 `.studio-block-editor` 이고 블록 하나가
|
||||
`<textarea>` 하나다. 기록 한 편이 블록 171개인 것을 실제로 봤다.
|
||||
|
||||
```
|
||||
.studio-block-editor
|
||||
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
|
||||
├ .studio-block-editor__blocks
|
||||
│ └ .studio-authoring-block[data-kind] heading · paragraph · code · bullet · raw
|
||||
└ button + 블록 추가
|
||||
```
|
||||
|
||||
**코드 블록의 `input.studio-code-language` 에 울타리 뒤 정보 문자열 전체가 들어간다** —
|
||||
`bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"` 가 그 입력의 값이었다. 라벨을
|
||||
넣을 별도 칸은 없다.
|
||||
|
||||
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 그래서 쓰인 기록을 화면으로 옮기지
|
||||
않는다 — [studio-api.md](studio-api.md) 의 `PUT` 으로 `bodyMarkdown` 을 통째로 보낸다.
|
||||
`.studio-markdown-source` 는 `readonly` 라 입력 자리가 아니지만, 화면이 지금 무엇을 직렬화할지를
|
||||
그대로 보여 주므로 **저장 전에 눈으로 대조하기에 좋다.**
|
||||
|
||||
## 저장 다음은 게시가 아니라 검증이다
|
||||
|
||||
편집 화면의 `aside` 에는 `저장` 과 `게시` 둘뿐인데, 저장만 된 기록은 서버가 `nextAction` 을
|
||||
**`VALIDATE`** 로 준다. 검증 화면은 `/studio/documents/{id}/validation` 이다. 이 스킬은 저장까지라
|
||||
거기까지 가지 않는다 — 다만 「저장했는데 왜 게시가 안 되나」의 답이 이것이다.
|
||||
|
||||
## Asset 을 올릴 때
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Studio 가 실제로 주고받는 것
|
||||
|
||||
여기 적은 것은 **2026-09-17 에 로그인한 브라우저에서 직접 불러 확인한 것**이다. 문서를 보고
|
||||
적은 것이 아니라 요청을 보내고 돌아온 것을 옮겼다. 확인한 판이 바뀌면 이 파일도 낡는다.
|
||||
|
||||
## 왜 이 파일이 필요해졌나
|
||||
|
||||
**편집 화면의 본문이 블록 편집기로 바뀌었다.** 전에는 본문이 `본문 Markdown` 이라는 textarea
|
||||
하나였고 거기에 통째로 붙여넣으면 됐다. 지금은 블록 하나가 `<textarea>` 하나다 — 기록 한 편이
|
||||
블록 171개인 것을 실제로 봤다. 화면으로 그것을 채우는 것은 사람이 한 편을 쓸 때의 방식이지
|
||||
쓰인 기록을 옮기는 방식이 아니다.
|
||||
|
||||
그런데 **저장이 서버로 보내는 것은 블록이 아니라 마크다운 문자열 하나**다. 화면은 그 문자열
|
||||
위의 편집기이고, 저장 요청은 `bodyMarkdown` 한 칸을 보낸다. 그래서 옮기는 일은 화면이 아니라
|
||||
이 API 로 한다.
|
||||
|
||||
## 인증
|
||||
|
||||
**자격증명을 이 저장소에 두지 않는다.** 이미 로그인된 브라우저 세션을 쓰고, 요청은 그 페이지
|
||||
안에서(`fetch`) 보낸다. 쿠키가 그대로 실린다.
|
||||
|
||||
CSRF 토큰은 `XSRF-TOKEN` 쿠키에 있고 `x-csrf-token` 헤더로 되돌려 준다.
|
||||
|
||||
```js
|
||||
document.cookie.match(/(?:^|;\s*)XSRF-TOKEN=([^;]+)/)?.[1]
|
||||
```
|
||||
|
||||
로그인이 안 돼 있으면 `/studio` 가 「기록을 쓰려면 로그인이 필요합니다」를 낸다.
|
||||
**로그인 화면을 자동으로 통과하려 들지 않는다** — 사람에게 로그인해 달라고 말하고 멈춘다.
|
||||
|
||||
## 응답 껍데기
|
||||
|
||||
성공이든 실패든 같은 모양이다.
|
||||
|
||||
```json
|
||||
{ "success": true, "data": { … }, "error": null,
|
||||
"meta": { "requestId": "…", "traceId": "…", "correlationId": "…", "page": null } }
|
||||
```
|
||||
|
||||
실패하면 `data` 가 `null` 이고 `error` 에 `{code, category, message, retryable, details}` 가 온다.
|
||||
`details.supportedMethods` 처럼 고칠 방법을 담아 주는 경우가 있다.
|
||||
|
||||
## 경로
|
||||
|
||||
| 무엇 | 요청 |
|
||||
|---|---|
|
||||
| 목록 | `GET /api/v1/studio/documents?limit=100` |
|
||||
| 한 편 | `GET /api/v1/studio/documents/{id}` |
|
||||
| 새로 만들기 | `POST /api/v1/studio/documents` → 201 |
|
||||
| **저장** | `PUT /api/v1/studio/documents/{id}` → 200 |
|
||||
| 지우기 | `DELETE /api/v1/studio/{종류}/{id}` → 204 |
|
||||
|
||||
**`limit` 은 100 이 상한이다.** 200 을 주면 422 `REQUEST_VALIDATION_FAILED` 다. 더 받으려면
|
||||
`data.nextCursor` 를 `cursor` 로 넘긴다.
|
||||
|
||||
**`/documents/{id}` 는 `PUT` 과 `GET` 만 받는다.** 거기에 `DELETE` 를 보내면 405 이고
|
||||
`details.supportedMethods` 가 `["PUT","GET"]` 을 준다. 삭제는 종류별 경로다 —
|
||||
`cases` · `references` · `concepts` · `setups` · `questions`, 그리고 Decision 은
|
||||
`projects/{projectId}/decisions/{decisionId}`.
|
||||
|
||||
## 한 편 읽기
|
||||
|
||||
```
|
||||
GET /api/v1/studio/documents/{id}
|
||||
→ data: { document, currentValidation, latestPreview, currentPublication,
|
||||
dependencyRevision, nextAction }
|
||||
```
|
||||
|
||||
`nextAction` 이 다음에 할 일을 말한다 — 저장만 된 기록은 `VALIDATE` 다. 검증 화면은
|
||||
`/studio/documents/{id}/validation` 이고 편집 화면의 `aside` 에는 **`저장` 과 `게시` 둘뿐**이라
|
||||
검증은 그 화면에서 따로 한다.
|
||||
|
||||
## 저장
|
||||
|
||||
```
|
||||
PUT /api/v1/studio/documents/{id}
|
||||
headers: content-type: application/json
|
||||
accept: application/json
|
||||
x-csrf-token: <XSRF-TOKEN 쿠키>
|
||||
idempotency-key: <요청마다 새로>
|
||||
body: { "expectedVersion": <읽어 온 document.version>, "document": { … } }
|
||||
```
|
||||
|
||||
**`expectedVersion` 은 방금 읽은 `document.version` 이다.** 저장이 끝나면 1 올라간다. 지어내지
|
||||
말고 저장 직전에 `GET` 으로 읽는다.
|
||||
|
||||
`document` 는 **그 종류의 칸을 전부** 담는다. 빠뜨린 칸은 비는 것으로 저장된다 — 부분 갱신이
|
||||
아니다.
|
||||
|
||||
## 종류마다의 칸
|
||||
|
||||
여섯이 함께 갖는 것: `kind` · `title` · `slug` · `summary` · `relations` · `projectId` ·
|
||||
`topicId` · `variantIds`. 서버가 주지만 보내지 않는 것: `id` · `version` · `updatedAt`.
|
||||
|
||||
| kind | 그 종류만의 칸 |
|
||||
|---|---|
|
||||
| `CASE` | `problem` · `conclusion` · `environment` · `reproduction` · `bodyMarkdown` · `lastVerifiedOn` |
|
||||
| `CONCEPT` | `bodyMarkdown` · `basisVersion` |
|
||||
| `SETUP` | `bodyMarkdown` · `pinnedVersions` |
|
||||
| `REFERENCE` | `purpose` · `rules` · `applyWhen` · `exceptions` · `examples` · `verifiedOn` |
|
||||
| `QUESTION` | `facts` · `assumptions` · `unknowns` · `constraints` · `options` · `nextValidation` · `resolution` · `questionStatus` |
|
||||
|
||||
되풀이되는 칸의 모양이다. `id` 는 서버가 붙이므로 새로 넣을 때는 빼고 `order` 만 0 부터 센다.
|
||||
|
||||
| 칸 | 항목 |
|
||||
|---|---|
|
||||
| `relations` | `{ targetId, reason, order }` |
|
||||
| `facts` · `assumptions` · `unknowns` · `constraints` · `applyWhen` · `exceptions` · `examples` | `{ text, order }` |
|
||||
| `options` | `{ title, description, order }` |
|
||||
| `rules` | `{ title, body, order }` — **제목과 본문이 따로다** |
|
||||
| `pinnedVersions` | `{ name, version }` — `order` 가 없다 |
|
||||
|
||||
## `bodyMarkdown` 은 바이트 그대로 돌아온다
|
||||
|
||||
넣은 것과 읽어 온 것이 같은지 확인했다. **코드 울타리의 정보 문자열이 그대로 살아남는다.**
|
||||
|
||||
```
|
||||
```bash label="[host] ① 친다"
|
||||
```
|
||||
|
||||
넣고 다시 읽었을 때 `label="…"` 까지 한 글자도 안 바뀌었다. 그래서 `studio-body.py` 가 만든
|
||||
본문을 그대로 실어 보내면 된다.
|
||||
|
||||
## 화면에서 블록이 어떻게 생겼나
|
||||
|
||||
옮기는 일은 API 로 하지만, 사람이 화면을 읽을 때와 무엇이 어긋났는지 볼 때 필요하다.
|
||||
|
||||
```
|
||||
.studio-block-editor
|
||||
├ textarea.studio-markdown-source 본문 전체의 거울. display:none · aria-hidden · readonly
|
||||
├ .studio-block-editor__blocks
|
||||
│ └ .studio-authoring-block[data-kind] 블록 하나
|
||||
└ button + 블록 추가
|
||||
```
|
||||
|
||||
`data-kind` 는 `heading` · `paragraph` · `code` · `bullet` · `raw` 다. `raw`(화면 이름
|
||||
**고급 블록**)가 표처럼 블록으로 안 갈리는 것을 그대로 담는다.
|
||||
|
||||
**코드 블록의 `input.studio-code-language` 는 언어 이름만 담지 않는다.** 울타리 뒤의 정보
|
||||
문자열 **전체**가 거기 들어간다 — `bash label="[kc-lab-1] ① 기동 로그에서 쿠키 설정을 찾는다"`
|
||||
가 그 입력의 값이었다. 라벨을 넣을 별도 칸은 없다.
|
||||
|
||||
**`.studio-markdown-source` 는 읽는 데만 쓴다.** `readonly` 이고 `display:none` 이라 입력
|
||||
자리가 아니다. 다만 화면이 지금 무엇을 직렬화할지를 그대로 보여 주므로, 저장 전에 눈으로
|
||||
대조하기에 좋다.
|
||||
|
||||
합성한 `ClipboardEvent` 로 붙여넣는 것은 **안 먹는다.** 화면으로 본문을 채우려면 블록을 하나씩
|
||||
만들어야 한다.
|
||||
|
||||
## 화면의 칸 셀렉터
|
||||
|
||||
칸은 이제 `aria-label` 로 찾는다. `label.studio-field` 는 제목과 요약 둘만 감싼다.
|
||||
|
||||
```js
|
||||
document.querySelector('input[aria-label="제목"]')
|
||||
document.querySelector('textarea[aria-label="요약"]')
|
||||
document.querySelector('input[aria-label="slug"]')
|
||||
document.querySelector('input[aria-label="판단 기준 1 제목"]')
|
||||
document.querySelector('textarea[aria-label="적용할 때 3"]')
|
||||
```
|
||||
|
||||
되풀이 칸을 늘리는 버튼은 `+ 판단 기준` · `+ 적용할 때` · `+ 예외와 주의` · `+ 예시` ·
|
||||
`+ 버전 추가` 다.
|
||||
|
||||
상태 레일은 그대로다 — `aside[class*="studio-document-status"]`(실제 class 는
|
||||
`studio-document-status-bar`). 그 안에 **`저장` 과 `게시` 둘뿐**이고, 저장된 상태에서는
|
||||
`저장` 이 `disabled` 이며 글자가 `저장됨` 이다.
|
||||
|
||||
## 하지 않는 것
|
||||
|
||||
- **게시하지 않는다.** 이 스킬은 저장까지다. 한 번이라도 게시한 문서는 게시를 취소해도 삭제가
|
||||
409 로 거절된다
|
||||
- **`expectedVersion` 을 지어내지 않는다.** 저장 직전에 읽는다
|
||||
- **부분 갱신이라고 생각하지 않는다.** 보내지 않은 칸은 비워진다
|
||||
- **로그인 화면을 자동으로 넘기지 않는다**
|
||||
@@ -100,8 +100,20 @@ Decision 의 도메인 `ACCEPTED` 가 화면에서는 `ADOPTED` 로 보인다.
|
||||
|
||||
`Reference`·`Question`·`Decision` 의 칸은 **평문으로 렌더링된다.**
|
||||
|
||||
- 백틱과 파이프가 글자 그대로 보인다. 코드·표를 넣지 않는다
|
||||
- 줄바꿈은 `<br>` 로만 살아난다
|
||||
**「평문」이 「전부 글자 그대로」라는 뜻은 아니다.** 렌더러
|
||||
(`tech-log-frontend` 의 `presentation/shared/public-render/prose-text.tsx`)가 셋을 해석한다.
|
||||
|
||||
| 무엇 | 평문 칸에서 |
|
||||
|---|---|
|
||||
| 백틱 쌍 | **인라인 `<code>` 로 산다.** 빼지 않는다 — 빼면 식별자가 민무늬로 나온다 |
|
||||
| 빈 줄 | 문단이 갈린다 |
|
||||
| 한 줄 바꿈 | `<br>` |
|
||||
| 별표 · 파이프 · `#` · 코드펜스 · 인용 표지 `>` | **글자 그대로 나온다.** 넣지 않는다 |
|
||||
|
||||
**전에 이 자리에 「백틱과 파이프가 글자 그대로 보인다」고 적혀 있었고 백틱 쪽은 틀렸다.**
|
||||
백틱이 글자로 나오던 것은 고쳐진 옛 버그이고 그 파일 주석에 그렇게 적혀 있다. 칸이 어떻게
|
||||
보이는지는 **렌더러가 정본이다** — 이 문서가 아니다.
|
||||
|
||||
- 나열은 쉼표로 잇지 말고 `이름 : 값` 으로 줄을 나눈다
|
||||
|
||||
코드·표·그림이 필요하면 짝이 되는 Case·Concept·Setup 에 담고 `관계` 로 가리킨다.
|
||||
|
||||
@@ -367,6 +367,163 @@ Not `${NAME}` — guides use `$TOKEN`, `$SID` and friends as real shell
|
||||
variables, and a reader who reads a placeholder as one will paste it unchanged.
|
||||
`{{ }}` is not shell syntax, so pasting it fails where the reader can see it.
|
||||
|
||||
## The reader cannot see what is not on screen
|
||||
|
||||
Everything below came out of reading 35 finished guides in one pass. Each rule
|
||||
names the accident that produced it. None of them is a style preference — in
|
||||
every case the guide passed every other rule in this file and still handed the
|
||||
reader a wrong answer that looked right.
|
||||
|
||||
### A value copied into a container is a snapshot, not a reference
|
||||
|
||||
`kubectl run --env="K0=$K0"`, `docker run -e`, cloud-init user-data: each bakes
|
||||
the string as it was at that moment. From then on two spellings mean different
|
||||
things, and they differ by two characters:
|
||||
|
||||
```bash
|
||||
exec pod -- sh -c '...$K0...' # the value baked into the pod
|
||||
exec pod -- sh -c '...'"$K0"'...' # the value in the shell typing this
|
||||
```
|
||||
|
||||
**They agree until something restarts.** In one guide a `rollout restart` changed
|
||||
the pod IP, and a later step still read the baked copy; the request went to an
|
||||
address that no longer existed, and the failure it produced was a `500` — which
|
||||
was also that step's expected result. The screen looked correct.
|
||||
|
||||
So: when a step replaces the thing a baked value points at — a pod IP, a node
|
||||
address, a lease — say in that step which spelling the next commands use.
|
||||
Prefer recreating the container. If state inside it forbids that, show the
|
||||
live-read form and add one line saying why it differs from the block above.
|
||||
|
||||
**Distance is not the test.** A variable defined 700 lines up can be safe, and
|
||||
one defined nine lines up can be stale. Ask what happened in between.
|
||||
|
||||
### A step that only works inside a window says when, not just what
|
||||
|
||||
The four-part shape above — what / command / where to look / what it means —
|
||||
has no *when*. Several steps read correctly only during a restart, only while a
|
||||
rule is installed, only within sixty seconds. Read late, they do not error;
|
||||
they print a different, plausible number.
|
||||
|
||||
Name the command that opens the window and the one that closes it, and say
|
||||
**what the reader sees if they type it after it closed** — that is what most
|
||||
readers will actually get.
|
||||
|
||||
### A check must be able to fail
|
||||
|
||||
For every line that says "이렇게 나오면 통과", state what would print the same
|
||||
thing while the condition is false. Three real cases:
|
||||
|
||||
- `virsh list --all` used to prove a group membership took effect — it connects
|
||||
to the per-user URI, which works with no group at all
|
||||
- a `postmaster.pid` path checked on a fixed node, when the volume may be on the
|
||||
other one — "no such file" passes either way
|
||||
- `source ~/.bashrc` on a host whose login shell is zsh — true in that shell,
|
||||
gone at next login, and the symptom surfaces one guide later
|
||||
|
||||
If the check passes when the step was skipped — because it reads a different
|
||||
scope, host or shell than the step wrote to — it is not a check. Put the scope
|
||||
in the command, or write one line naming the failure this check cannot see.
|
||||
|
||||
### Never ship a command the guide knows is wrong
|
||||
|
||||
Three guides printed a command and told the reader, in prose underneath, to
|
||||
edit it before typing. Prose is not a guard. The worst of them was a **valid**
|
||||
assignment:
|
||||
|
||||
```bash
|
||||
OLDID=980ee9b7-... # ← copy from the output above
|
||||
```
|
||||
|
||||
Paste it and `OLDID` holds the literal string, and the `delete` two lines later
|
||||
runs. Every other hardcoded value in those guides failed loudly — `(0 rows)`,
|
||||
`(nil)`. This one succeeded at the wrong thing.
|
||||
|
||||
If prose under a block says "replace X first", the block already says it — or
|
||||
says `{{X}}`, which is not shell syntax and fails where the reader can see it.
|
||||
|
||||
### One block, one machine
|
||||
|
||||
A code block is the reader's copy unit. When the machine changes, the block
|
||||
ends, even when the commands form one logical step. Splitting the explanation
|
||||
in prose does not help: they copy the block.
|
||||
|
||||
And a machine label tells the reader *where* a block runs — it does not put
|
||||
them there. When consecutive blocks carry different labels, the transition is
|
||||
its own step with its own command (`ssh host`, `exit`, "open a second
|
||||
terminal"). Count them per guide: entries and exits must balance. A
|
||||
verification section that runs somewhere else needs both.
|
||||
|
||||
### A shell function defined mid-guide is worse than all three tiers
|
||||
|
||||
It leaves no file, so the reader cannot re-read it. It dies with the shell, so
|
||||
a new window silently breaks every later step. `unset` does not reach it.
|
||||
|
||||
One guide defined `R()` on line 188 and used it through line 617 — a one-letter
|
||||
name for `kubectl -n … exec deploy/redis -- redis-cli`. At the point of use
|
||||
the reader cannot see what command they are running.
|
||||
|
||||
If something is repeated often enough to want a name, make it a script file
|
||||
with that name. If it must be a function, define it in the same block as its
|
||||
first use, name it for what it does (`redis`, not `R`), and restate the
|
||||
definition at the top of any later section more than a screen away.
|
||||
|
||||
### Names the reader must already have
|
||||
|
||||
A host alias, a directory, a volume, a file that no earlier step created is not
|
||||
a placeholder — it is an assumption, and the placeholder rule above does not
|
||||
catch it. Four guides in one set ran entirely on `ssh kc-lab-1` with no stanza
|
||||
anywhere that creates it; the second guide hits it on its first command.
|
||||
|
||||
Show the command that creates it, or name the guide that owns it, in the prose
|
||||
above the first block that uses it.
|
||||
|
||||
### Walk the guide twice
|
||||
|
||||
`echo >>` is one case of a larger rule. Anything created under a fixed name — a
|
||||
pod, a volume, a DHCP reservation, a namespace — says what the second run
|
||||
prints and how to clear it first. `--rm` only removes on clean exit; Ctrl+C
|
||||
leaves the pod and the next run dies on `AlreadyExists`.
|
||||
|
||||
And anything that destroys and recreates a host says what identity changes with
|
||||
it — SSH host keys, MAC-bound leases, certificates — and what that breaks the
|
||||
next time the reader connects.
|
||||
|
||||
### The output block belongs to the command above it
|
||||
|
||||
If it came from a wider `grep`, an extra `uniq -c`, or a `sed` that rewrote
|
||||
names, show that command too or say so on the line before. Readers compare
|
||||
their screen to yours character by character; a silent edit makes them hunt for
|
||||
a fault that is not there.
|
||||
|
||||
### Context selectors are read as a set
|
||||
|
||||
`-n`, `--context`, `-h`, `-U`. One guide's `kubectl exec` was missing
|
||||
`-n keycloak-lab` while the eight commands around it had it. On its own the
|
||||
line looks fine; next to its neighbours it is obviously running somewhere else.
|
||||
Read them as a column, not line by line.
|
||||
|
||||
### When the same action has several forms, one of them is the step
|
||||
|
||||
One guide gave three ways to kill a backend and marked none of them. Show the
|
||||
step; put the rest under a heading that says they are alternatives. Leaving the
|
||||
reader to choose asks them to weigh a trade-off the guide has not explained yet.
|
||||
|
||||
### A fixed output path makes a block single-use
|
||||
|
||||
"Change the variable and run it again" is incomplete when the block writes to a
|
||||
fixed file: the second run overwrites the first run's baseline, silently, and
|
||||
the comparison two sections later has nothing to compare against. Name the
|
||||
output path among the things to change.
|
||||
|
||||
### Run the remedy you prescribe
|
||||
|
||||
A guide diagnosed lexical sorting and prescribed `sort -g`. Every line in that
|
||||
file began with the same `200`, so `sort -g` compared equal and fell through to
|
||||
byte order — its output was identical to plain `sort`. The fix the guide
|
||||
offered did not fix anything, and the sentence "miss this and you misread the
|
||||
maximum" stayed true after following it.
|
||||
|
||||
## Rationalization table
|
||||
|
||||
| Excuse | Reality |
|
||||
@@ -383,6 +540,11 @@ variables, and a reader who reads a placeholder as one will paste it unchanged.
|
||||
| "The file ends up the same either way" | Only on the first run. `echo >>` appends again every time someone repeats the guide |
|
||||
| "So I should use nano for everything" | No. The trigger is authoring a file the reader must understand. `grep`, `virsh`, a long `virt-install` stay as they are |
|
||||
| "`python3 -c` only checks syntax" | Then it misses the schema. If the format has a validator — `cloud-init schema`, `nginx -t`, `--dry-run` — that is the step |
|
||||
| "The prose right under it says to change that value" | The reader copies the block. Prose is not a guard — put `{{NAME}}` in the block |
|
||||
| "The variable is defined earlier in the same shell" | Ask what restarted in between. A pod IP baked at line 877 was stale by line 764 |
|
||||
| "The label says which machine it runs on" | A label says where, not how to get there. The transition is its own step |
|
||||
| "It's the same command, just shorter" | A one-letter function hides the command at the exact moment the reader needs to read it |
|
||||
| "The check passed" | Ask what it would print if the step had been skipped. Three checks in one set passed either way |
|
||||
|
||||
## Red flags — stop and rewrite
|
||||
|
||||
@@ -398,6 +560,14 @@ variables, and a reader who reads a placeholder as one will paste it unchanged.
|
||||
- one line stacking connect + redirect + file creation (`ssh host 'cat > f' < f`)
|
||||
- `python3 -c` checking syntax when the format has its own validator
|
||||
- a step that appends, so walking the guide twice appends the line twice
|
||||
- prose under a block telling the reader to edit the command before typing it
|
||||
- a block whose machine label differs from the one above, with no command between
|
||||
- a one- or two-letter shell function, or any function defined far from its use
|
||||
- a host alias, directory or volume that no step in any guide creates
|
||||
- `--rm` with no line saying what a Ctrl+C leaves behind
|
||||
- a "통과" line that would print the same thing if the step had been skipped
|
||||
- a step that reads correctly only during a window, with no word about the window
|
||||
- a remedy you have not run against the data that made you prescribe it
|
||||
|
||||
## References
|
||||
|
||||
|
||||
Reference in New Issue
Block a user