docs(document-haness): 관문의 종료 코드 — CASE 한 편을 근거까지 잇고 런 원장을 남긴다

이 저장소 자신의 검사 층을 대상으로 삼았다. 파이프 뒤의 $? 를 읽고 검사기 열넷이
전부 --help 를 받는다고 적었다가, 파이프를 걷고 다시 재니 둘이 exit 1 이었던 일.
관찰·원인·조치가 한 사건으로 닫히고 조치가 capture-evidence.py 다.

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

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

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

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

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

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q4vKjQo9KKBBokzxqXLCfk
This commit is contained in:
DongHyeonka
2026-09-10 11:06:50 +09:00
co-authored by Claude Opus 5
parent cf3996711f
commit 230e1b20cb
31 changed files with 2262 additions and 0 deletions
@@ -0,0 +1,2 @@
그림의 정본 — <이름>/{context.json, spec.json, prompt.md}.
technical-visualizer 스킬이 만든다. 손으로 SVG 를 그리지 않는다.
@@ -0,0 +1,3 @@
그림. 그림 하나가 폴더 하나다 — <이름>/<이름>.svg 와 편집 형식들.
기록의 assets: file: 도 이 폴더를 가리킨다. Studio 에 올릴 사본을 따로 두지 않는다 —
사본을 두면 정본이 둘이 되고, 사본 쪽에는 ../../.techviz/<이름>/ 이 없어 다시 만들 수 없다.
+345
View File
@@ -0,0 +1,345 @@
# document-haness 의 검사 층 — 관문이 무엇을 보고 무엇을 안 보는가
이 문서는 `document-haness` 저장소 자신의 검사 층을 대상으로 삼는다. 이 저장소는 기술 글을
만드는 파이프라인이고, 그 파이프라인이 글을 통과시킬지 말지는 `scripts/` 의 검사기와
스킬이 들고 있는 `.mjs` 검사기가 정한다. **그 검사기들이 실제로 무엇을 보는지**를 실행으로
확인한 기록이다.
| | |
|---|---|
| 대상 저장소 | `document-haness` |
| 리비전 | `43e1aadef077ad93c30495df428ee3a71dd73f4a` (`main`) |
| 확인한 worktree | `dh-B` (`harness/B-implementation`, 같은 커밋에서 갈라짐) |
| 확인한 날 | 2026-09-10 |
| 실행 환경 | python 3.12.3 · node v24.14.0 · bash 5.2.21(1)-release |
---
## §1 검사기 열넷이 있는 자리
`scripts/` 아래에 `.py` 파일 열넷이 있다. 리비전 `43e1aad` 에서 센 값이다.
```
audit-records.py
build-tech-log-tree.py
check-figure-overlap.py
check-figure-text.py
fold-analysis-into-final.py
fold-studio-contract-into-index.py
preview-figure.py
studio-body.py
techlog.py
verify-pipeline-run.py
verify-pipeline.py
verify-project-layout.py
verify-refactor-work-item.py
verify-tech-log-tree.py
```
이 중 `techlog.py` 는 CLI 가 아니라 공유 라이브러리다. `fold-*.py` 둘은 검사기가 아니라
작업 재료를 옮기는 도구다. 나머지 열하나가 관문으로 불린다.
관문은 **종료 코드로 말한다.** `running-tech-log-pipeline` 의 단계 계약이
「관문은 종료 코드가 0 이어야 지난 것이다. 0 이 아니면 그 단계는 `FAILED` 이고 다음 단계로
넘어가지 않는다」 라고 적었고, 런 원장 `runs/<프로젝트>/<runId>/run.json`
`stages[].gates[].exit` 에 그 값이 남는다.
## §2 관찰한 것
검사기 목록을 만들려고 열넷에 `--help` 를 돌렸다. 한 줄짜리 셸 반복문이었고, 출력이 길어서
`head` 로 잘랐다.
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
열넷이 전부 `exit=0` 이었다.
```
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
```
이 저장소를 함께 조사한 다른 세션도 같은 값을 얻어 「`scripts/*.py` 14개 전부 `--help`
종료 코드 0 으로 돌아온다」 를 확인 등급 **확인함**으로 적었다. 두 사람이 같은 값을 얻었으니
맞는 값처럼 보였다.
## §3 파이프 뒤의 종료 코드
틀린 것은 검사기가 아니라 재는 방법이었다. 파이프라인의 `$?`**마지막** 명령의 종료
코드다. `head` 는 언제나 성공하므로 앞의 `python3` 가 무엇을 반환하든 `$?` 는 0 이 된다.
```bash
set +o pipefail
false | head -1; echo "false | head -1 -> exit=$?"
false; echo "false -> exit=$?"
```
```
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
```
POSIX 셸의 정의된 동작이고 이 셸의 특이점이 아니다. `pipefail` 을 켜거나
`${PIPESTATUS[0]}` 를 읽으면 앞 명령의 값을 얻는다.
같은 착각이 한 번 더 났다. 파이프를 걷어 내고 다시 잴 때 이렇게 썼다.
```bash
out=$(timeout 60 python3 $s --help 2>&1)
echo "$(basename $s) exit=$?"
```
명령 치환 `$(basename $s)` 가 먼저 실행되면서 `$?` 를 덮어썼다. 그래서 두 번째 측정도
열넷 전부 0 이었다. `code=$?` 를 명령 바로 다음 줄에 두고서야 값이 갈렸다.
## §4 다시 잰 값
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
out=$(timeout 60 python3 $s --help 2>&1)
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
```
audit-records.py exit=0
build-tech-log-tree.py exit=1
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=1
verify-tech-log-tree.py exit=0
```
두 측정의 차이는 두 줄뿐이다.
```
2c2
< build-tech-log-tree.py exit=0
---
> build-tech-log-tree.py exit=1
13c13
< verify-refactor-work-item.py exit=0
---
> verify-refactor-work-item.py exit=1
```
## §5 argparse 를 쓰지 않는 넷
열넷 중 넷이 `argparse` 를 쓰지 않는다.
```
ARGPARSE 없음 audit-records.py
ARGPARSE 없음 build-tech-log-tree.py
argparse check-figure-overlap.py
argparse check-figure-text.py
argparse fold-analysis-into-final.py
argparse fold-studio-contract-into-index.py
argparse preview-figure.py
argparse studio-body.py
ARGPARSE 없음 techlog.py
argparse verify-pipeline-run.py
argparse verify-pipeline.py
argparse verify-project-layout.py
ARGPARSE 없음 verify-refactor-work-item.py
argparse verify-tech-log-tree.py
```
`techlog.py` 는 CLI 가 아니라서 인자를 안 읽는다. 남은 셋은 `--help` 를 **옵션이 아니라
위치 인자로 먹는다.** `audit-records.py` 는 그것을 프로젝트 이름으로 받아 「문제 없음」을
찍고, `build-tech-log-tree.py``verify-refactor-work-item.py` 는 그 이름의 파일을 못 찾아
실패한다.
```
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
```
**`--help` 가 exit 0 이라는 것이 usage 가 나왔다는 뜻은 아니다.** `audit-records.py`
`--help` 라는 이름의 프로젝트를 검사해서 통과시킨 것이다.
## §6 실재하지 않는 프로젝트 이름
`audit-records.py --help` 가 「문제 없음」을 찍은 것은 `--help` 가 특별해서가 아니다.
**없는 프로젝트를 검사하면 검사할 것이 없어 통과가 된다.** 같은 이름
`nonexistent-project` 를 여섯 관문에 주면 이렇게 갈린다.
```
$ python3 scripts/audit-records.py nonexistent-project
nonexistent-project — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/verify-tech-log-tree.py nonexistent-project
[nonexistent-project] records=0
! warn 1 분해 계약 없음
· nonexistent-project: tech-log-tree.json 이 없다. 디렉터리가 정본 노릇을 하고 있다
TECH LOG TREE: PASS — 프로젝트 1 · error 0 · warn 1
exit=0
$ python3 scripts/check-figure-text.py nonexistent-project
FIGURE TEXT: PASS — 그림 0장 · 문장 0건
exit=0
$ python3 scripts/check-figure-overlap.py nonexistent-project
볼 그림이 없다
exit=0
$ python3 scripts/verify-project-layout.py nonexistent-project
PROJECT LAYOUT: FAIL — 프로젝트 1 · error 1 · warn 0
[nonexistent-project] —
✗ error 1 final/document.md 가 없다
· nonexistent-project
exit=1
$ node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs nonexistent-project --repo
nonexistent-project: tech-log-tree.json 이 없다
exit=2
```
여섯 중 넷이 아무것도 검사하지 않고 통과를 보고한다. 잡아내는 것은
`verify-project-layout.py`(`final/document.md` 를 먼저 찾는다)와
`check_evidence.mjs`(`tech-log-tree.json` 을 먼저 찾는다) 둘뿐이다.
## §7 원장은 종료 코드를 다시 재지 않는다
런 원장 검사기 `scripts/verify-pipeline-run.py` 가 관문에 대해 보는 것은 두 가지다.
```python
cmds = " ; ".join(str(g.get("cmd") or "") for g in gates)
for token in spec["gates"]:
if token not in cmds:
...
for g in gates:
if g.get("exit") not in (0, "0"):
...
```
관문 명령을 **다시 실행하지 않는다.** `exit` 는 그 런이 스스로 적어 넣은 숫자다. 그리고
`cmd` 안에 토큰 문자열(`"check-figure-text.py"`)이 있는지만 보고 **어느 프로젝트에 돌렸는지는
보지 않는다.**
§6 과 이어 붙이면 이렇게 된다. 원장에
`{"cmd": "python3 scripts/check-figure-text.py <오타난 이름>", "exit": 0}` 을 적으면 그 명령은
실제로도 exit 0 이고, 원장 검사기도 통과시킨다. 그림을 한 장도 검사하지 않은 런이 「절차를
지켰다」로 판정된다.
실제 원장에도 명령이 자리표시자가 든 요약형으로 적혀 있어 그대로 재실행할 수 없다.
```
"cmd": "python3 scripts/studio-body.py <기록.md> -o /tmp/sb-<slug>.md (44건)"
```
## §8 종료 코드를 손으로 적을 수 없게 만든다
관문 결과를 사람이 옮겨 적는 한 §2 와 §7 은 같은 뿌리에서 계속 난다. 그래서 명령을 돌리는
자리와 종료 코드를 적는 자리를 하나로 붙였다. `scripts/capture-evidence.py` 는 명령을
`subprocess` 로 직접 실행하고 **그 프로세스의 반환값을 그대로** 메타에 적는다.
```python
proc = subprocess.run(command, cwd=cwd, capture_output=True,
text=True, timeout=timeout)
exit_code, out = proc.returncode, proc.stdout + proc.stderr
```
셸을 거치지 않으므로 파이프가 낄 자리가 없고, 종료 코드를 인자로 받지 않으므로 손으로 적을
경로도 없다. 원문은 `final/evidence/raw/` 에, 실행 메타는 `final/evidence/meta/` 에 같은
이름으로 함께 떨어진다.
실제로 떨어진 메타 하나다. `§4` 의 측정을 담은 것이다.
```json
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
```
여기서 `exitCode: 0` 은 **측정 반복문 자체가 성공했다**는 뜻이다. 열넷 각각의 종료 코드는
원문 `raw/` 안에 있다. 반복문의 종료 코드와 그 안에서 잰 값은 다른 것이고, 메타는 앞의 것을
적는다.
`sourceDirty` 는 그 실행 시점에 작업 트리에 커밋 안 된 변경이 있었는지다. 있으면
`sourceRevision` 이 그 출력을 설명하지 못한다.
처음 판에서는 이 도구도 인자를 잘못 먹었다. `argparse.REMAINDER` 로 명령을 받았더니
`--proves` 부터가 실행할 명령으로 딸려 가 `No such file or directory: '--proves'` 로 죽었다.
`--` 앞뒤를 직접 가르도록 고쳤고 그 이유를 코드에 한 줄로 남겼다.
```python
# `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다
argv = sys.argv[1:]
```
§5 에서 `--help` 를 위치 인자로 먹은 세 스크립트와 같은 종류의 실수다.
셸 한 줄을 감싸는 래퍼로 만들 수도 있었다. 그러면 파이프를 다시 쓸 수 있게 되고, 값을 두 번
틀리게 만든 것이 그 셸이라 아예 거치지 않기로 했다. 감수한 것은 셸 문법이 필요한 명령을
`bash -c` 로 넘겨야 한다는 점이다 — 그 안에서 다시 파이프를 쓰면 같은 실수가 난다.
기존 경로를 지우지 않았다. 손으로 만든 `raw/`·`meta/` 도 그대로 유효하고 이 도구는
선택적으로 부른다.
## §9 확인하지 못한 것
- **셸을 쓰는 다른 자리.** `--help` 하나만 쟀다. 다른 인자에서 같은 모양인지는 안 봤다.
- **`argparse` 를 쓰는 열이 모든 인자를 옳게 다루는지.** `--help` 만 통과한 것을 확인했다.
- **`nonexistent-project` 넷이 실재하는 프로젝트에서도 통과만 내는지.** 아니다 —
`check-figure-text.py ca-tmpl` 은 exit 1 을 낸다. §6 은 「대상이 없을 때」의 동작이다.
- **`capture-evidence.py` 가 이 저장소의 모든 증거 수집을 대체할 수 있는지.** 브라우저 캡처와
대화형 명령은 이 경로로 못 담는다.
- **원장 검사기 수정의 영향.** §7 을 고치는 것은 이 저장소의 다른 담당 몫이고 여기서는
현상만 적는다.
@@ -0,0 +1 @@
Playwright MCP 로 찍은 브라우저 캡처. 무엇을 찍었는지 한 줄을 적는다.
@@ -0,0 +1 @@
그 실행의 command·cwd·executedAt·exitCode·revision. 형식은 evidence.json.
@@ -0,0 +1,18 @@
{
"id": "argparse-absent-scripts",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:57+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\\.py$'); do if grep -q '^import argparse' $s; then echo \"argparse $(basename $s)\"; else echo \"ARGPARSE 없음 $(basename $s)\"; fi; done",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/argparse-absent-scripts.txt",
"presentationPath": null,
"proves": "14개 중 넷이 argparse 를 쓰지 않는다",
"doesNotProve": "argparse 를 쓰는 열이 모든 인자를 옳게 다룬다는 것",
"sha256": "2412e5266e3889012f6a0c9732234dbfcc1a611c3368c1851d61b6c40307ff67",
"bytes": 496
}
@@ -0,0 +1,18 @@
{
"id": "exit-code-through-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:19+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c set +o pipefail; false | head -1; echo \"false | head -1 -> exit=$?\"; false; echo \"false -> exit=$?\"; echo \"bash $BASH_VERSION\"",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/exit-code-through-a-pipe.txt",
"presentationPath": null,
"proves": "파이프의 마지막 명령이 성공하면 $? 가 0 이다. 앞 명령의 종료 코드는 사라진다",
"doesNotProve": "이 셸이 특별하다는 것. POSIX 셸의 정의된 동작이다",
"sha256": "b17529b23ee69c887fbf28a474a7c566c9d9d3ef2e5b0d1c115e90649d60c36f",
"bytes": 77
}
@@ -0,0 +1,18 @@
{
"id": "gates-on-a-nonexistent-project",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:57:21+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; for cmd in 'python3 scripts/audit-records.py nonexistent-project' 'python3 scripts/verify-tech-log-tree.py nonexistent-project' 'python3 scripts/check-figure-text.py nonexistent-project' 'python3 scripts/check-figure-overlap.py nonexistent-project' 'python3 scripts/verify-project-layout.py nonexistent-project' 'node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs nonexistent-project --repo'; do out=$(timeout 120 sh -c \"$cmd\" 2>&1); code=$?; echo \"\\$ $cmd\"; echo \"$out\" | grep -v '^$' | head -4; echo \"exit=$code\"; echo; done",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/gates-on-a-nonexistent-project.txt",
"presentationPath": null,
"proves": "실재하지 않는 프로젝트 이름에 검사기 여섯 중 넷이 exit 0 을 낸다",
"doesNotProve": "그 넷이 실재하는 프로젝트에서도 통과만 낸다는 것. check-figure-text.py ca-tmpl 은 exit 1 이다",
"sha256": "3afad37da2e49290c5cf75a6970562b649fd5ddab6e54870ba9d45dd4c80ab8f",
"bytes": 1080
}
@@ -0,0 +1,18 @@
{
"id": "help-exit-codes-measured-through-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\\.py$'); do timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null; code=$?; n=$(basename $s); echo \"$n exit=$code\"; done",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-through-a-pipe.txt",
"presentationPath": null,
"proves": "파이프를 끼고 재면 43e1aad 의 scripts/*.py 14개가 전부 --help exit 0 으로 보인다",
"doesNotProve": "그 14개가 실제로 --help 를 지원한다는 것",
"sha256": "0827ec5c575d24c9998fa79f3cb26f0d4e98b1c647e5121d9e651b13784ff611",
"bytes": 410
}
@@ -0,0 +1,18 @@
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\\.py$'); do out=$(timeout 60 python3 $s --help 2>&1); code=$?; n=$(basename $s); echo \"$n exit=$code\"; done",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"presentationPath": null,
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
@@ -0,0 +1,18 @@
{
"id": "redaction-covers-command-lines",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T10:09:26+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; python3 scripts/terminal-evidence/render_terminal.py /tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/raw.txt /tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/out.svg --command 'cat evidence-sample.txt' --cwd /tmp --exit-code 0 --executed-at 2026-09-10T10:20:00+09:00; echo '--- 렌더한 SVG 의 text 노드 ---'; python3 -c \"\nimport re\ns=open('/tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/out.svg').read()\nfor t in re.findall(r'<text[^>]*>(.*?)</text>', s, re.S):\n if any(k in t for k in ('curl','psql','Authorization','GET')): print(t)\nprint('TESTONLY 남은 횟수:', s.count('TESTONLY'))\n\"",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/guards/redaction-covers-command-lines.txt",
"presentationPath": null,
"proves": "렌더러가 명령 인자 안의 Bearer·Cookie 와 접속 문자열 암호를 [REDACTED] 로 덮고, 렌더한 SVG 에 합성 자격증명이 한 건도 남지 않는다",
"doesNotProve": "raw 원문에 secret 이 들어가도 된다는 것. 렌더러 마스킹은 그물이지 방벽이 아니다",
"sha256": "2fa64df31ba1dea4194ea744a33c2e4fb921e0a45293f46cdf70e89e8ce7c2bb",
"bytes": 320
}
@@ -0,0 +1,18 @@
{
"id": "redaction-keeps-the-command-intact",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T10:33:11+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; python3 -c \"\nimport importlib.util, html, re, subprocess\nspec=importlib.util.spec_from_file_location('rt','scripts/terminal-evidence/render_terminal.py')\nm=importlib.util.module_from_spec(spec); spec.loader.exec_module(m)\nraw=open('/tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/raw2.txt').read().splitlines()\nsubprocess.run(['python3','scripts/terminal-evidence/render_terminal.py','/tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/raw2.txt','/tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/out2.svg','--command','cat evidence-sample.txt','--cwd','/tmp','--exit-code','0','--executed-at','2026-09-10T10:45:00+09:00'],capture_output=True)\nsvg=open('/tmp/claude-1000/-home-donghyeon-workspace-chat-gpt-container-dh-B/0106b08a-c31d-459b-8469-4e9326cfe523/scratchpad/r8/out2.svg').read()\nprint('원문 줄 -> 렌더 줄 (따옴표 수)')\nfor line in raw:\n out=m.redact_line(line)\n print(f' {line.count(chr(34))} -> {out.count(chr(34))} {out}')\nprint()\nprint('렌더한 SVG 에 TESTONLY 가 남은 횟수:', svg.count('TESTONLY'))\n\"",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/guards/redaction-keeps-the-command-intact.txt",
"presentationPath": null,
"proves": "마스킹이 자격증명을 덮으면서 줄마다 따옴표 수를 그대로 둔다. 합성값(TESTONLY-*)이 렌더 결과에 하나도 안 남는다",
"doesNotProve": "모든 자격증명 모양을 덮는다는 것. 확인한 것은 여기 여섯 줄이다",
"sha256": "40ed59329c1b4d1a66e49c941216c8953c1f63f0a459f2f583c48e060d0ce4b2",
"bytes": 464
}
@@ -0,0 +1,18 @@
{
"id": "what-the-three-print-for-help",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:57:21+09:00",
"executedAtSource": "이 도구가 명령을 실행한 시각",
"command": "bash -c cd /home/donghyeon/workspace/chat-gpt-container/dh-B; for s in audit-records build-tech-log-tree verify-refactor-work-item; do echo \"\\$ python3 scripts/$s.py --help\"; out=$(timeout 60 python3 scripts/$s.py --help 2>&1); code=$?; echo \"$out\"; echo \"exit=$code\"; echo; done",
"cwd": ".",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/what-the-three-print-for-help.txt",
"presentationPath": null,
"proves": "argparse 없는 넷 가운데 CLI 인 셋이 --help 를 프로젝트 이름·경로로 먹는다",
"doesNotProve": "다른 잘못된 인자에서도 같은 모양이라는 것",
"sha256": "91101c72e788bce9c81d090fe605ba92433f67b94476cfcb641f6e55e75d0035",
"bytes": 409
}
@@ -0,0 +1,2 @@
명령 출력·csv·덤프 원문. 여기가 정본이다.
하위 폴더를 자유롭게 둔다(explain/, guards/). 폴더마다 무엇을 담았는지 한 줄을 README.txt 에 적는다.
@@ -0,0 +1,14 @@
ARGPARSE 없음 audit-records.py
ARGPARSE 없음 build-tech-log-tree.py
argparse check-figure-overlap.py
argparse check-figure-text.py
argparse fold-analysis-into-final.py
argparse fold-studio-contract-into-index.py
argparse preview-figure.py
argparse studio-body.py
ARGPARSE 없음 techlog.py
argparse verify-pipeline-run.py
argparse verify-pipeline.py
argparse verify-project-layout.py
ARGPARSE 없음 verify-refactor-work-item.py
argparse verify-tech-log-tree.py
@@ -0,0 +1,3 @@
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
@@ -0,0 +1,32 @@
$ python3 scripts/audit-records.py nonexistent-project
nonexistent-project — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/verify-tech-log-tree.py nonexistent-project
[nonexistent-project] records=0
! warn 1 분해 계약 없음
· nonexistent-project: tech-log-tree.json 이 없다. 디렉터리가 정본 노릇을 하고 있다
TECH LOG TREE: PASS — 프로젝트 1 · error 0 · warn 1
exit=0
$ python3 scripts/check-figure-text.py nonexistent-project
FIGURE TEXT: PASS — 그림 0장 · 문장 0건
exit=0
$ python3 scripts/check-figure-overlap.py nonexistent-project
볼 그림이 없다
exit=0
$ python3 scripts/verify-project-layout.py nonexistent-project
PROJECT LAYOUT: FAIL — 프로젝트 1 · error 1 · warn 0
[nonexistent-project] —
✗ error 1 final/document.md 가 없다
· nonexistent-project
exit=1
$ node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs nonexistent-project --repo
nonexistent-project: tech-log-tree.json 이 없다
exit=2
@@ -0,0 +1,2 @@
터미널 증거 렌더러의 마스킹이 실제로 덮는지 확인한 출력.
쓰인 자격증명은 전부 합성(TESTONLY-*)이고 실제 비밀값이 아니다.
@@ -0,0 +1,7 @@
--- 렌더한 SVG 의 text 노드 ---
$ curl -H &quot;Authorization: Bearer [REDACTED]&quot; https://example.test/api
$ curl -H &quot;Cookie: [REDACTED]&quot; https://example.test/api
$ psql postgresql://app:[REDACTED]@db:5432/app
Authorization: Bearer [REDACTED]
GET /api/me -&gt; 200 in 14ms
TESTONLY 남은 횟수: 0
@@ -0,0 +1,9 @@
원문 줄 -> 렌더 줄 (따옴표 수)
2 -> 2 $ curl -H "X-Api-Key: [REDACTED]" https://example.invalid/d
2 -> 2 $ curl -H "Authorization: Basic [REDACTED]" https://example.invalid/d
2 -> 2 $ curl -H "Proxy-Authorization: Basic [REDACTED]" https://example.invalid/d
0 -> 0 $ curl -u admin:[REDACTED] https://example.invalid/d
2 -> 2 $ export TOKEN="[REDACTED]"
0 -> 0 GET /api/me -> 200 in 14ms
렌더한 SVG 에 TESTONLY 가 남은 횟수: 0
@@ -0,0 +1,14 @@
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
@@ -0,0 +1,14 @@
audit-records.py exit=0
build-tech-log-tree.py exit=1
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=1
verify-tech-log-tree.py exit=0
@@ -0,0 +1,17 @@
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
@@ -0,0 +1,2 @@
raw 에서 만든 터미널 SVG. 표현물이지 정본이 아니다.
scripts/terminal-evidence/render_terminal.py 로 만든다.
@@ -0,0 +1,283 @@
---
id:
kind: CASE
slug: exit-code-read-behind-a-pipe
title: 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
topic: pipeline-gate-exit-codes
topicName: 관문의 종료 코드
project: document-haness
status: 게시 전
studio: ""
lastVerifiedOn: 2026-09-10
source:
- final/document.md#§2-관찰한-것
- final/document.md#§3-파이프-뒤의-종료-코드
- final/document.md#§4-다시-잰-값
- final/document.md#§5-argparse-를-쓰지-않는-넷
- final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다
sourceRevision: 43e1aadef077ad93c30495df428ee3a71dd73f4a
evidence:
- ../../../final/evidence/raw/exit-code-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-without-a-pipe.txt
- ../../../final/evidence/raw/argparse-absent-scripts.txt
- ../../../final/evidence/raw/what-the-three-print-for-help.txt
---
# 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
scripts/ 의 검사기 열넷에 --help 를 돌려 전부 종료 코드 0 을 받았고, 이 저장소를 함께
조사한 다른 세션도 같은 값을 얻어 확인 등급 확인함으로 적었다. 값은 검사기가 아니라 재는
방법이 만든 것이었다. 파이프를 걷어 내고 다시 재니 둘이 exit 1 이다.
## 관계
- **검사할 것이 없을 때 관문은 무엇을 내야 하는가**
이 사건에서 `audit-records.py --help` 가 「문제 없음」을 찍으면서 그 물음이 열렸다.
`--help` 라는 이름의 프로젝트에는 검사할 기록이 하나도 없어 그대로 통과했다.
## 문제
관문은 종료 코드로 말한다. 단계 계약이 「관문은 종료 코드가 0 이어야 지난 것이다」 라고
적었고, 런 원장 run.json 의 stages[].gates[].exit 에 그 값이 남는데, 그 값을 사람이 셸에서
읽어 옮겨 적는다.
--help 하나를 잰 것뿐인데 값이 두 번 틀렸다. 관문 결과 전부가 같은 방법으로 적히고 있다.
## 결론
리비전 43e1aad 의 scripts/*.py 열넷 중 열둘이 --help 에 exit 0,
build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다.
exit 0 이 usage 가 나왔다는 뜻은 아니다 — audit-records.py 는 --help 를 프로젝트
이름으로 받아 검사하고 통과시킨다.
값을 틀리게 만든 것은 셸의 동작 둘이다. 파이프의 종료 코드 변수는 마지막 명령을 가리키고,
명령 치환은 그 변수를 덮어쓴다. 둘 다 정의된 동작이라 셸이 경고하지 않는다.
조치로 만든 scripts/capture-evidence.py 는 명령을 subprocess 로 직접 돌리고 그 프로세스의
반환값을 그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을 경로가 없다.
## 검증 환경
python 3.12.3 · node v24.14.0 · bash 5.2.21(1)-release · Linux.
대상 저장소 document-haness 리비전 43e1aadef077ad93c30495df428ee3a71dd73f4a.
측정은 그 커밋에서 갈라진 worktree dh-B 에서 했고, 그 시점 작업 트리에는
capture-evidence.py 가 더해져 있어 증거 메타의 sourceDirty 가 true 다.
측정 대상 열넷은 git ls-tree 로 그 커밋의 목록만 골라 냈다.
## 재현 조건
1. document-haness 를 43e1aad 로 체크아웃한다.
2. 파이프를 끼고 잰다 — 반복문 안에서 python3 출력을 head 로 넘기고 그다음 줄에서 $? 를 읽는다.
3. 열넷이 전부 exit=0 으로 나오는 것을 본다.
4. 파이프를 걷고 out=$(...) 다음 줄에서 code=$? 로 받아 다시 잰다.
5. build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit=1 로 갈리는 것을 본다.
## 본문
<!-- body:start -->
## 두 번 같은 값이 나왔다
검사기 목록을 만들려고 열넷에 `--help` 를 돌렸는데, 출력이 길어서 `head` 로 잘랐다.
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
열넷이 전부 `exit=0` 이었다.
```
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
```
이 저장소를 함께 조사한 다른 세션도 같은 값을 얻어 「`scripts/*.py` 14개 전부 `--help`
종료 코드 0 으로 돌아온다」 를 확인 등급 **확인함**으로 적었다. 두 사람이 같은 값을 얻었으니
맞는 값처럼 보였다.
## 값을 만든 것은 셸이다
파이프라인의 `$?`**마지막** 명령의 종료 코드다. `head` 는 언제나 성공하므로 앞의
`python3` 가 무엇을 반환하든 `$?` 는 0 이 된다.
```bash
set +o pipefail
false | head -1; echo "false | head -1 -> exit=$?"
false; echo "false -> exit=$?"
```
```
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
```
POSIX 셸의 정의된 동작이고 이 셸의 특이점이 아니다. `pipefail` 을 켜거나
`${PIPESTATUS[0]}` 를 읽으면 앞 명령의 값을 얻는다.
같은 착각이 한 번 더 났다. 파이프를 걷어 내고 다시 잴 때 이렇게 썼다.
```bash
out=$(timeout 60 python3 $s --help 2>&1)
echo "$(basename $s) exit=$?"
```
명령 치환 `$(basename $s)` 가 먼저 실행되면서 `$?` 를 덮어써서 두 번째 측정도 열넷 전부
0 이었다. `code=$?` 를 명령 바로 다음 줄에 두고서야 값이 갈렸다.
## 다시 잰 값
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
out=$(timeout 60 python3 $s --help 2>&1)
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
두 측정의 차이는 두 줄뿐이다.
```
2c2
< build-tech-log-tree.py exit=0
---
> build-tech-log-tree.py exit=1
13c13
< verify-refactor-work-item.py exit=0
---
> verify-refactor-work-item.py exit=1
```
잰 것은 `--help` 하나뿐이라 나머지 열둘이 다른 인자에서 어떻게 도는지는 이 값이 말해 주지
않는다.
## 왜 그 둘만인가
열넷 중 넷이 `argparse` 를 쓰지 않는다.
| 스크립트 | `argparse` | `--help` |
|---|---|---|
| `audit-records.py` | 없음 | `exit 0` — 「문제 없음」 |
| `build-tech-log-tree.py` | 없음 | `exit 1` |
| `techlog.py` | 없음 | `exit 0` — CLI 가 아니라 인자를 안 읽는다 |
| `verify-refactor-work-item.py` | 없음 | `exit 1` |
| 나머지 열 | 있음 | `exit 0` — usage |
남은 셋은 `--help` 를 옵션이 아니라 위치 인자로 먹는다. `audit-records.py` 는 그것을
프로젝트 이름으로 받아 「문제 없음」을 찍고, `build-tech-log-tree.py`
`verify-refactor-work-item.py` 는 그 이름의 파일을 못 찾아 실패한다.
이 두 숫자를 한 번 섞었다. 넷을 셋으로 적었고, 그 셋이 분석 문서 네 곳과 증거 메타 둘에
그대로 실렸다. 증거 원문에서 줄을 다시 센 뒤에 넷으로 고쳤다.
```
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
```
`exit 0` 이 usage 가 나왔다는 뜻은 아니다. `audit-records.py``--help` 라는 이름의
프로젝트를 찾아 검사하고 통과시켰다.
## 손으로 적지 못하게 했다
관문 결과를 사람이 옮겨 적는 한 같은 뿌리에서 같은 실수가 계속 난다. 그래서 명령을 돌리는
쪽과 종료 코드를 적는 쪽을 하나로 붙였다.
```python
proc = subprocess.run(command, cwd=cwd, capture_output=True,
text=True, timeout=timeout)
exit_code, out = proc.returncode, proc.stdout + proc.stderr
```
종료 코드를 인자로 받지 않으므로 손으로 적어 넣을 수는 없다. 도구의 첫머리 주석이 목표를
적는다 — 돌리지 않은 검증을 완료로 적는 경로가 없어야 한다. 원문은
`final/evidence/raw/` 에, 실행 메타는 `final/evidence/meta/` 에 같은 이름으로 함께 떨어진다.
둘을 사람이 따로 적으면 갈라지기 때문이다.
셸 한 줄을 감싸는 래퍼도 됐지만 그러면 파이프를 다시 쓸 수 있게 된다. 값을 두 번 틀리게
만든 것이 바로 그 셸이라 아예 거치지 않기로 했다. 대신 셸 문법이 필요한 명령은 `bash -c`
인자로 넘겨야 하고, 그 안에서 다시 파이프를 쓰면 같은 실수가 난다 — 이 기록의 증거
다섯 개도 그렇게 수집했다.
처음 판에서는 이 도구도 인자를 잘못 먹었다. `argparse.REMAINDER` 로 명령을 받았더니
`--proves` 부터가 실행할 명령으로 딸려 가 `No such file or directory: '--proves'` 로 죽었다.
지금은 `--` 앞뒤를 직접 가르고, 왜 그렇게 했는지를 코드에 한 줄로 남겨 두었다.
```python
# `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다
argv = sys.argv[1:]
```
`--help` 를 위치 인자로 먹은 세 스크립트와 같은 종류의 실수다. 이 저장소에서 인자를 손으로
가르는 코드는 대개 여기서 걸린다.
이 기록이 인용한 원문 다섯 개를 그 도구가 수집했다. 메타 하나는 이렇게 생겼다.
```json
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
```
여기서 `exitCode: 0` 은 **측정 반복문 자체가 끝까지 돌았다**고 말한다. 열넷 각각의 종료
코드는 원문 `raw/` 안에 있다. 반복문이 반환한 값과 그 안에서 잰 값을 섞지 않는다 — 메타에는
반복문 쪽이 들어간다.
`sourceDirty` 는 그 실행 시점에 작업 트리에 커밋 안 된 변경이 있었는지 적는다. 있으면
`sourceRevision` 이 그 출력을 설명하지 못한다. 이 다섯은 전부 `true` 다 — 수집기 자신을
더한 상태에서 쟀다.
기존 경로를 지우지 않았다. 손으로 만든 `raw/`·`meta/` 도 그대로 쓰이고, 이 도구는 필요할
때만 부른다.
## 이 사건이 닫지 못한 것
수집기는 브라우저 캡처와 대화형 명령을 담지 못한다. 이 저장소의 증거 가운데
`final/evidence/browser/` 쪽은 여전히 Playwright 로 찍고 메타를 손으로 적는다 — 종료 코드를
손으로 적을 수 없게 만든 것이 아직 절반이라는 뜻이다.
<!-- body:end -->
@@ -0,0 +1,76 @@
---
id:
kind: QUESTION
slug: what-a-gate-returns-with-nothing-to-check
title: 검사할 것이 없을 때 관문은 무엇을 내야 하는가
topic: pipeline-gate-exit-codes
topicName: 관문의 종료 코드
project: document-haness
status: 게시 전
studio: ""
questionStatus: OPEN
source:
- final/document.md#§6-실재하지-않는-프로젝트-이름
sourceRevision: 43e1aadef077ad93c30495df428ee3a71dd73f4a
evidence:
- ../../../final/evidence/raw/gates-on-a-nonexistent-project.txt
---
# 검사할 것이 없을 때 관문은 무엇을 내야 하는가
실재하지 않는 프로젝트 이름을 관문 여섯에 주면 넷이 통과를 보고한다. 검사할 것이 없으니
찾은 문제도 없다는 뜻이라 틀린 값은 아니다. 다만 관문 결과를 읽는 쪽은 그것을 검사를
지났다는 뜻으로 읽는다. 어느 쪽이 맞는지 이 저장소가 아직 정하지 않았다.
분해 계약에서는 이 후보를 Case 로 올리려다 Question 으로 돌렸다. 현상은 재현했는데 조치가
없어서다 — 조치 없이 쓰면 관찰만 있고 결과가 없는 글이 된다. 검사기 수정은 이 저장소의
다른 담당이 맡고 있어 여기서 고칠 수도 없었다.
## 관계
- **파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다**
그 사건에서 audit-records.py 가 --help 를 프로젝트 이름으로 받아 「문제 없음」을 찍은
것이 이 물음의 입구다. 같은 동작을 오타난 프로젝트 이름에서 다시 보게 된다.
## 사실
- 관문 여섯에 nonexistent-project 를 주면 넷이 exit 0 을 낸다. audit-records.py 는 「문제 없음」, verify-tech-log-tree.py 는 「PASS — 프로젝트 1 · error 0 · warn 1」, check-figure-text.py 는 「PASS — 그림 0장 · 문장 0건」, check-figure-overlap.py 는 「볼 그림이 없다」를 찍는다.
- 잡아내는 둘은 대상 파일을 먼저 찾는다. verify-project-layout.py 는 final/document.md 가 없다며 exit 1, check_evidence.mjs 는 tech-log-tree.json 이 없다며 exit 2 다.
- 검사기가 고장난 것이 아니다. 실재하는 프로젝트에서는 판정을 낸다 — check-figure-text.py ca-tmpl 은 그림 29장에서 문장 13건을 찾아 exit 1 이다.
- 런 원장 검사기 verify-pipeline-run.py 는 관문의 대상 프로젝트를 보지 않는다. 명령 문자열에 토큰이 들어 있는지와 원장이 적어 낸 exit 값만 본다.
## 가정
- 관문 결과를 읽는 쪽이 exit 0 을 「그 프로젝트를 검사했고 문제가 없었다」로 읽는다고 보고 있다. 실제로 그렇게 읽는지는 사람에게 물어보지 않았다.
## 미지수
- 「대상 없음」을 통과로 볼지 실패로 볼지 정해지지 않았다. 아직 만들지 않은 프로젝트에 관문을 미리 걸어 보는 쓰임이 있다면 exit 0 이 맞는 값이다.
- 넷 각각이 「대상 없음」과 「대상이 있는데 검사할 것이 0 건」을 구분할 수 있는지 모른다. 그 코드를 읽지 않았다.
- 오타로 관문이 무효가 된 런이 실제로 있었는지 모른다. 지금 있는 원장 세 건은 프로젝트 이름이 전부 맞다.
## 제약
- 검사기 자체의 수정은 이 저장소의 다른 담당 몫이다. 여기서는 현상만 적는다.
- 관문을 실패로 바꾸면 기존 원장과 전체 훑기가 함께 움직인다. 한쪽만 보고 정할 수 없다.
## 선택지
### 1. 넷이 「대상 없음」에서 실패를 내게 한다
대상 프로젝트 폴더가 없으면 exit 1 을 낸다. verify-project-layout.py 가 이미 그렇게 한다.
오타 한 번에 관문이 무효가 되는 경로가 막힌다. 대신 아직 만들지 않은 프로젝트에 미리 돌려
보는 쓰임이 사라지고, 기존 원장 세 건과 전체 훑기가 그대로 통과하는지 확인해야 한다.
### 2. 관문은 그대로 두고 원장 쪽에서 대상 이름을 대조한다
verify-pipeline-run.py 가 gates[].cmd 에서 프로젝트 이름을 뽑아 원장의 project 칸과
견준다. 검사기 넷을 건드리지 않아 다른 쓰임이 안 깨진다. 대신 명령을 「<기록.md>」 같은 꼴로
줄여 적은 기존 원장에서는 이름을 못 뽑는다 — 원장 세 건에 이미 그런 줄이 있다.
## 다음 검증
1. 넷에 실재하는 프로젝트를 주고 「검사 대상 0 건」이 나오는 경우와 「프로젝트 없음」이 나오는 경우의 출력을 나란히 잰다. 둘이 지금 구분되는지를 먼저 본다.
2. 원장의 gates[].cmd 에서 프로젝트 이름을 뽑아 project 칸과 대조하는 검사를 넣어 보고, 기존 원장 세 건이 통과하는지 본다.
닫는 조건 : 넷이 「대상 없음」에서 exit 0 이 아닌 값을 내도 기존 원장 세 건과 전체 훑기가 그대로 통과하면 선택지 1 로 정하고 Decision 으로 넘긴다. 하나라도 깨지면 선택지 2 로 넘긴다.
@@ -0,0 +1,230 @@
{
"schemaVersion": 4,
"project": "document-haness",
"ssot": "final/document.md",
"ssotSha256": "6d2c41d2d02eecd1476b95c83caa91e262151236b38aedf5dda775114fed561f",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"generatedAt": "2026-09-10",
"sourceRepository": {
"path": "/home/donghyeon/workspace/chat-gpt-container/document-haness",
"revision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"verified": "worktree dh-B 에서 git rev-parse HEAD 로 확인했다. 측정 대상 열넷은 git ls-tree --name-only 43e1aad scripts/ 로 그 커밋의 목록만 골라 냈다. code[] 의 세 파일도 git cat-file -e 43e1aad:<경로> 로 그 커밋에 실재하는 것을 대조했고, 인용한 줄 155·148·193 이 셋 다 raise SystemExit(...) 인 것을 git show 로 확인했다. 이 런이 더한 scripts/capture-evidence.py 와 scripts/skill-versions.py 는 43e1aad 에 없으므로 code[] 에 넣지 않았다 — 기록 본문에서는 「이 배치에서 만든 것」으로 밝힌다. 증거 메타의 sourceDirty 가 전부 true 인 것이 그 상태를 적는다."
},
"candidateScope": {
"document": "final/document.md",
"sections": [
"§2 관찰한 것",
"§3 파이프 뒤의 종료 코드",
"§4 다시 잰 값",
"§5 argparse 를 쓰지 않는 넷",
"§6 실재하지 않는 프로젝트 이름",
"§7 원장은 종료 코드를 다시 재지 않는다",
"§8 종료 코드를 손으로 적을 수 없게 만든다"
],
"excluded": [
"§1 검사기 열넷이 있는 자리",
"§9 확인하지 못한 것"
],
"note": "§1 은 배경이고 §9 는 한계 목록이라 후보 자리가 아니다. 후보는 §2~§8 에서만 나온다"
},
"note": "이 프로젝트의 글감 전부다. 분해 계약이자 색인이고, 이 파일이 정본이다. 노드의 칸(readiness·source·classification·relations…)은 사람이 적고, file·publication·status 는 기록 파일에서 읽어 채운다 — python3 scripts/build-tech-log-tree.py document-haness",
"contract": {
"decomposition": [
"글감을 찾는 입력은 final/document.md 하나다. 거기에 없는 근거는 먼저 SSOT 에 넣는다.",
"후보 전부는 candidates 에 처분과 함께 남고 PROMOTE 만 topics 로 올라간다.",
"없애고 관련 Case 나 Concept 의 한 절로 넣어도 이해·결정·재사용성이 그대로라면 독립 기록으로 만들지 않는다.",
"Topic 은 독자 질문 하나다. 그 물음에 답하지 않는 글감은 다른 Topic 으로 옮긴다.",
"Concept 은 Case·Decision·Question 을 먼저 고른 뒤 그것을 이해하는 데 필요한 것만 거꾸로 더한다."
],
"readinessValues": [
"READY",
"OPEN",
"NEEDS_EVIDENCE",
"NEEDS_DECISION",
"BLOCKED"
],
"dispositionValues": {
"PROMOTE": "독립 Tech Log 로 쓴다",
"MERGE_INTO": "다른 기록의 한 절로 흡수한다",
"KEEP_IN_SSOT": "분석에는 남기고 독립 기록으로 만들지 않는다 — 정상적인 성공 결과다",
"NEEDS_EVIDENCE": "주장에 아직 검증이 없다",
"NEEDS_DECISION": "방향이 그럴듯하지만 프로젝트가 정하지 않았다",
"BLOCKED": "원본이 불완전하거나 서로 어긋난다"
}
},
"counts": {
"topics": 1,
"nodes": 2,
"written": 2,
"unwritten": 0,
"unlisted": 0,
"candidates": 5
},
"topics": {
"pipeline-gate-exit-codes": {
"topic": "pipeline-gate-exit-codes",
"title": "관문의 종료 코드",
"readerQuestion": "관문이 exit 0 을 냈다는 것은 무엇을 확인했다는 뜻인가?",
"kinds": {
"case": [
{
"title": "파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다",
"kind": "case",
"slug": "exit-code-read-behind-a-pipe",
"readiness": "READY",
"source": [
"final/document.md#§2-관찰한-것",
"final/document.md#§3-파이프-뒤의-종료-코드",
"final/document.md#§4-다시-잰-값",
"final/document.md#§5-argparse-를-쓰지-않는-넷",
"final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다"
],
"code": [
"scripts/audit-records.py:155",
"scripts/build-tech-log-tree.py:148",
"scripts/verify-refactor-work-item.py:193"
],
"evidence": [
"evidence/raw/exit-code-through-a-pipe.txt",
"evidence/raw/help-exit-codes-measured-through-a-pipe.txt",
"evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"evidence/raw/argparse-absent-scripts.txt",
"evidence/raw/what-the-three-print-for-help.txt"
],
"classification": "재현·진단·조치가 닫히는 하나의 사건이다. 두 세션이 같은 오측정을 하고, 원인이 셸의 정의된 동작으로 좁혀지고, 종료 코드를 손으로 적을 수 없게 만드는 수집기로 닫았다",
"missing-verification": "--help 하나만 쟀다. 다른 인자에서 argparse 를 쓰는 열이 어떻게 도는지는 확인하지 않았다",
"relations": [
"question:what-a-gate-returns-with-nothing-to-check"
],
"publication": "초안",
"file": "pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/exit-code-through-a-pipe.txt",
"../../../final/evidence/raw/help-exit-codes-measured-through-a-pipe.txt",
"../../../final/evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"../../../final/evidence/raw/argparse-absent-scripts.txt",
"../../../final/evidence/raw/what-the-three-print-for-help.txt"
]
}
],
"concept": [],
"reference": [],
"question": [
{
"title": "검사할 것이 없을 때 관문은 무엇을 내야 하는가",
"kind": "question",
"slug": "what-a-gate-returns-with-nothing-to-check",
"readiness": "OPEN",
"source": [
"final/document.md#§6-실재하지-않는-프로젝트-이름"
],
"evidence": [
"evidence/raw/gates-on-a-nonexistent-project.txt"
],
"classification": "현상은 재현했고 조치는 정해지지 않았다. 답이 나오려면 이 저장소가 「대상 없음」을 통과로 볼지 실패로 볼지 정해야 한다",
"missing-verification": "실재하는 프로젝트 전부에서 같은 넷이 어떻게 도는지는 안 쟀다. ca-tmpl 하나에서 check-figure-text.py 가 exit 1 을 내는 것만 봤다",
"relations": [
"case:exit-code-read-behind-a-pipe"
],
"known": [
"관문 여섯에 실재하지 않는 프로젝트 이름 nonexistent-project 를 주면 넷이 exit 0 을 낸다 — audit-records.py 는 「문제 없음」, verify-tech-log-tree.py 는 「PASS — error 0 · warn 1」, check-figure-text.py 는 「PASS — 그림 0장」, check-figure-overlap.py 는 「볼 그림이 없다」",
"잡아내는 둘은 대상 파일을 먼저 찾는다 — verify-project-layout.py 는 final/document.md 가 없다고 exit 1, check_evidence.mjs 는 tech-log-tree.json 이 없다고 exit 2",
"이것은 「검사기가 고장났다」가 아니다. 실재하는 프로젝트에서는 판정을 낸다 — check-figure-text.py ca-tmpl 은 그림 29장에서 문장 13건을 찾아 exit 1 이다",
"런 원장 검사기 verify-pipeline-run.py 는 관문의 대상 프로젝트를 보지 않고 명령 문자열에 토큰이 있는지와 원장이 적어 낸 exit 만 본다"
],
"unknown": [
"「대상 없음」을 통과로 볼지 실패로 볼지 이 저장소가 정하지 않았다. 아직 안 만든 프로젝트에 관문을 미리 걸어 보는 쓰임이 있다면 exit 0 이 맞다",
"넷 각각이 「대상 없음」과 「대상이 있는데 검사할 것이 0 건」을 구분할 수 있는지 — 코드를 읽지 않았다",
"오타로 관문이 무효가 된 런이 실제로 있었는지. 원장 세 건은 전부 프로젝트 이름이 맞다"
],
"next-verification": [
"넷에 실재하는 프로젝트를 주고 「검사 대상 0 건」이 나오는 경우와 「프로젝트 없음」이 나오는 경우의 출력을 나란히 잰다",
"런 원장의 gates[].cmd 에서 프로젝트 이름을 뽑아 원장의 project 칸과 대조하는 검사를 verify-pipeline-run.py 에 넣어 보고, 기존 원장 세 건이 통과하는지 본다"
],
"decision-criterion": "넷이 「대상 없음」에서 exit 0 이 아닌 값을 내도 기존 원장 세 건과 verify-pipeline.py 전체 훑기가 그대로 통과하면 그 방향으로 정한다. 하나라도 깨지면 「대상 없음」을 통과로 두고 대신 원장 쪽에서 대상 이름을 대조한다",
"publication": "초안",
"file": "pipeline-gate-exit-codes/question/question-what-a-gate-returns-with-nothing-to-check.md",
"status": "게시 전",
"studioId": "",
"assets": [],
"assetFiles": [],
"evidenceFiles": [
"../../../final/evidence/raw/gates-on-a-nonexistent-project.txt"
]
}
],
"decision": []
}
}
},
"candidates": [
{
"id": "DH-C01",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§2-관찰한-것",
"final/document.md#§3-파이프-뒤의-종료-코드",
"final/document.md#§4-다시-잰-값"
],
"summary": "파이프를 끼고 재서 검사기 열넷이 전부 --help 를 받는 것으로 보였고, 다시 재니 둘이 exit 1 이었다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "case:exit-code-read-behind-a-pipe",
"reason": "관찰·원인·조치가 한 사건으로 닫히고 두 세션이 같은 값을 얻어 계약 문서에까지 들어간 자리다. 독립 기록으로 남길 값이 있다"
},
{
"id": "DH-C02",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§5-argparse-를-쓰지-않는-넷"
],
"summary": "열넷 중 넷이 argparse 를 쓰지 않고, 그중 셋이 --help 를 위치 인자로 먹는다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:exit-code-read-behind-a-pipe",
"reason": "왜 그 둘만 exit 1 이었는지를 설명하는 절이다. 떼어 내면 CASE 의 원인 절이 비고, 따로 읽을 값도 없다"
},
{
"id": "DH-C03",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다"
],
"summary": "명령 실행과 종료 코드 기록을 한 자리로 붙인 증거 수집기를 만들었다",
"disposition": "MERGE_INTO",
"dispositionReview": "CONFIRMED",
"target": "case:exit-code-read-behind-a-pipe",
"reason": "CASE 의 조치 절이다. 도구 자체의 사용법은 Reference 감이지만 아직 이 저장소 하나에서만 써서 「주의할 조건」을 적을 근거가 없다 — 실행 여덟 건이 전부 이 프로젝트의 증거 수집이고, 다른 프로젝트나 브라우저 캡처에 걸어 본 적이 없다"
},
{
"id": "DH-C04",
"kindCandidate": "CASE",
"sourceRefs": [
"final/document.md#§6-실재하지-않는-프로젝트-이름"
],
"summary": "실재하지 않는 프로젝트 이름에 관문 여섯 중 넷이 exit 0 을 낸다",
"disposition": "PROMOTE",
"dispositionReview": "CONFIRMED",
"target": "question:what-a-gate-returns-with-nothing-to-check",
"reason": "현상은 재현했지만 조치가 없다. 조치 없는 CASE 는 「관찰만 있고 결과가 없는 글」이 되므로 Question 으로 올린다 — 확인된 사실과 답을 구할 방법은 있고 답만 없다. 검사기 수정은 이 저장소의 다른 담당 몫이다"
},
{
"id": "DH-C05",
"kindCandidate": "QUESTION",
"sourceRefs": [
"final/document.md#§7-원장은-종료-코드를-다시-재지-않는다"
],
"summary": "런 원장이 관문을 재실행하지 않고 자기 신고한 exit 만 본다. 재실행이 옳은 답인지는 안 정했다",
"disposition": "NEEDS_DECISION",
"dispositionReview": "CONFIRMED",
"target": null,
"reason": "재실행하면 원장 검사가 느려지고 부작용 있는 관문(파일을 고치는 build-tech-log-tree.py)도 다시 돌게 된다. 무엇을 재실행하고 무엇을 신고로 받을지 이 저장소가 정하지 않았다. 정해지기 전에는 Question 으로도 못 쓴다 — 「답을 구할 방법」이 결정에 달려 있다"
}
],
"unlisted": [],
"history": {}
}
@@ -0,0 +1,233 @@
{
"schemaVersion": 1,
"runId": "2026-09-10-1033",
"project": "document-haness",
"record": "docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"startedAt": "2026-09-10T10:33:45+09:00",
"finishedAt": "2026-09-10T11:00:39+09:00",
"stages": [
{
"id": "S1",
"name": "코드베이스 → SSOT",
"skill": "analyzing-codebase-for-tech-log",
"runBy": "subagent",
"status": "DONE",
"skipReason": "",
"skillEcho": "Do not turn inference into observation in the final document.",
"inputs": [
"docs/document-haness/final/document.md",
"docs/document-haness/final/evidence/raw/**",
"docs/document-haness/final/evidence/meta/**"
],
"outputs": [
"docs/document-haness/final/document.md",
"docs/document-haness/final/evidence/meta/argparse-absent-scripts.json",
"docs/document-haness/final/evidence/meta/what-the-three-print-for-help.json",
"docs/document-haness/tech-log-studio/tech-log-tree.json"
],
"gates": [
{
"cmd": "python3 scripts/verify-project-layout.py document-haness",
"exit": 0
},
{
"cmd": "python3 scripts/build-tech-log-tree.py document-haness",
"exit": 0
},
{
"cmd": "python3 scripts/verify-tech-log-tree.py document-haness",
"exit": 0
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs document-haness --repo",
"exit": 0
}
],
"notes": "대조 모드다. 분석을 새로 하지 않고 SSOT 가 증거 원문과 어긋나는지만 봤다. S3 이 올린 어긋남 하나가 실물이었다 — 증거 정본 argparse-absent-scripts.txt 의 「ARGPARSE 없음」이 4줄인데 SSOT 는 셋이라고 적었다. 「argparse 가 없는 것(넷)」과 「--help 를 위치 인자로 먹는 것(셋)」을 한 숫자로 섞은 것이다. SSOT 네 곳(:155 절 제목 · :157 · :176 · §9)을 고치고, 절 제목이 바뀌어 앵커를 가리키는 세 곳(계약 2 · 기록 frontmatter 1)을 함께 고쳤다. 증거 메타 둘의 proves·doesNotProve 도 같은 오산을 담고 있어 고쳤다 — 그것은 사람이 적은 주장이다. exitCode·sha256·bytes·command 는 실행이 적은 값이라 안 건드렸고, raw 원문은 한 글자도 안 건드렸다(메타 8건의 sha256 을 다시 계산해 원문과 일치 확인). runs/.../stage/S3-before.md 는 과거 사본이라 옛 앵커를 그대로 두었다. 그 밖에 §1~§9 의 인용 블록 다섯을 raw 와 diff 로 대조했고 §7 의 원장 인용이 runs/virtualization/2026-09-09-1052/run.json:78 에 실재하는 것까지 확인했다. 오케스트레이터가 증거 원문과 코드로 다시 세어 넷인 것을 독립으로 확인했다. 판단이 필요해 안 고친 것 둘 — guards/ 증거 둘을 인용하는 기록이 없고(warn 2건), sourceRepository.path 가 worktree 가 아니라 원본 경로를 가리킨다(verified 칸이 사유를 적는다)."
},
{
"id": "S2",
"name": "SSOT → 분해 계약",
"skill": "deriving-tech-log-root-tree",
"runBy": "orchestrator",
"status": "SKIPPED",
"skipReason": "이 글감이 tech-log-tree.json 에 이미 PROMOTE · dispositionReview CONFIRMED 로 있다 (candidates[DH-C01], target=case:exit-code-read-behind-a-pipe). 분해를 다시 하지 않았다.",
"skillEcho": "",
"inputs": [
"docs/document-haness/final/document.md",
"docs/document-haness/tech-log-studio/tech-log-tree.json"
],
"outputs": [
"docs/document-haness/tech-log-studio/tech-log-tree.json"
],
"gates": [
{
"cmd": "python3 scripts/build-tech-log-tree.py document-haness",
"exit": 0
},
{
"cmd": "python3 scripts/verify-tech-log-tree.py document-haness",
"exit": 0
}
],
"notes": "build 는 파생 칸(file·publication·status·ssotSha256)만 다시 채운다. 사람이 적은 칸은 그대로다."
},
{
"id": "S3",
"name": "글감 → 기록",
"skill": "writing-tech-log-records",
"runBy": "subagent",
"status": "DONE",
"skipReason": "",
"skillEcho": "**옮겨 적었다고 확인한 것이 아니다.** 앞선 기록에서 코드를 가져오거나 기억으로 경로를 쓰면",
"inputs": [
"docs/document-haness/tech-log-studio/tech-log-tree.json",
"docs/document-haness/final/document.md",
"runs/document-haness/2026-09-10-1033/stage/S3-before.md"
],
"outputs": [
"docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md"
],
"gates": [
{
"cmd": "python3 scripts/studio-body.py docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md -o /tmp/s3-body.md",
"exit": 0
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/s3-body.md",
"exit": 0
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs document-haness --repo",
"exit": 0
},
{
"cmd": "python3 scripts/verify-tech-log-tree.py document-haness",
"exit": 0
}
],
"notes": "기록은 이미 있었고 서브에이전트가 계약·SSOT 와 대조해 세 곳을 고쳤다. (1) 요약 칸의 백틱 — 평문으로 렌더링되는 칸이다. (2) 요약이 「계약 문서에까지 들어갔다」고 적었는데 SSOT 는 「확인 등급 확인함으로 적었다」까지만 말한다. SSOT 가 뒷받침하는 문장으로 바꿨다. (3) 본문이 「열넷 중 셋이 argparse 를 쓰지 않는다」로 시작하면서 바로 아래 표에 「없음」을 넷 적어 두었다. 증거 정본 argparse-absent-scripts.txt 의 「ARGPARSE 없음」이 4줄이므로 표가 맞고 문장이 틀렸다 — 셋→넷, 남은 둘→남은 셋으로 고쳤다. 오케스트레이터가 증거 원문과 코드로 다시 세어 넷인 것을 확인했다. 같은 오산이 SSOT·계약·증거 메타에 그대로 있다는 것을 서브에이전트가 크게 적어 올렸고, SSOT 를 고치는 것은 자기 단계 밖이라 손대지 않았다 — S1 로 돌렸다. check_prose 경고 2건(CASE·POSIX 약어)은 남겼다. CASE 는 frontmatter 의 kind 값이고 POSIX 는 SSOT 가 쓰는 표준 명칭이라 풀어 쓰면 보호 구간을 건드린다."
},
{
"id": "S4",
"name": "기록 → 그림",
"skill": "technical-visualizer",
"runBy": "orchestrator",
"status": "SKIPPED",
"skipReason": "그림이 필요 없다. 세 관문 중 셋째에 걸린다 — 본문의 「왜 그 둘만인가」 절이 argparse 유무와 --help 결과를 표 하나로 답하고 있어, 같은 것을 그림으로 다시 그리면 옆 문단이 이미 말한 것을 되풀이한다. 계약의 이 노드에도 assets 가 없다.",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "이 프로젝트의 final/assets 는 비어 있다. check-figure-text.py 와 check-figure-overlap.py 는 볼 그림이 없어 돌리지 않았다."
},
{
"id": "S5",
"name": "AI 티 제거",
"skill": "rewriting-technical-prose-naturally",
"runBy": "subagent",
"status": "DONE",
"skipReason": "",
"skillEcho": "| Sentences are all short and choppy | Rejoin with `~기 때문에`, `~다 보니`, `~어서`; break only where the subject changes |",
"inputs": [
"docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"runs/document-haness/2026-09-10-1033/stage/S5-before.md"
],
"outputs": [
"docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md"
],
"gates": [
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/style_profile.mjs docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 1
},
{
"cmd": "python3 scripts/studio-body.py docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md -o /tmp/s5-body.md",
"exit": 0
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/s5-body.md",
"exit": 0
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs document-haness --repo",
"exit": 0
},
{
"cmd": "python3 scripts/check-preservation.py runs/document-haness/2026-09-10-1033/stage/S5-before.md docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
}
],
"notes": "서브에이전트가 아홉 곳을 고쳤다. 전부 문장을 잇거나 문단 순서를 옮긴 것이고 삭제한 문장도 새로 쓴 문장도 없다(68문장 → 63문장). check-preservation.py 가 「보호 구간 변화 0건 · 유보 감소 0종」으로 확인했다 — 윤문이 수치·코드·인용·URL 을 건드리지 않았고 유보 표현도 안 지웠다는 뜻이다. style_profile.mjs 는 exit 1 로 그대로 적는다. 관문이 아니라 측정이고 문서 계약이 이것에만 「error 0」을 안 붙였다. 이유 연결어미는 5.9 → 9.5 로 기준 안에 들어왔고 문장 평균 길이는 41.6 → 45 로 기준(48~75) 밖에 남았다. 서브에이전트가 남은 짧은 문장 22개를 전부 열어 보고 재현 조건 단계·수치 한 줄·코드로 넘기는 도입·정의 한 줄·방향 전환이라 잇지 않았다고 적었다. 수치를 맞추려고 문장을 넣지 말라는 것이 스킬의 규칙이다. 서브에이전트가 사실 어긋남 하나를 올렸다 — 본문이 「이 기록의 증거 여섯 개」라고 적었는데 frontmatter 의 evidence 는 다섯이다. 수치는 보호 구간이라 S5 가 못 고친다고 판단해 넘겼고, 오케스트레이터가 frontmatter 를 세어 확인한 뒤 다섯으로 고쳤다. check-preservation 은 이 고침을 못 잡는다 — 한글 수사는 아라비아 숫자가 아니라서 보호 구간 비교에 안 걸린다. 이 검사기의 알려진 한계다."
},
{
"id": "S6",
"name": "일한 사람의 목소리",
"skill": "writing-as-the-person-who-did-it",
"runBy": "subagent",
"status": "DONE",
"skipReason": "",
"skillEcho": "고치는 방법은 하나뿐이다. **자료에 남아 있는 사람의 흔적을 찾아서 제자리에 놓는다.**",
"inputs": [
"docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"docs/document-haness/final/document.md",
"docs/document-haness/tech-log-studio/tech-log-tree.json",
"docs/document-haness/final/evidence/meta/*.json",
"scripts/capture-evidence.py",
"runs/document-haness/2026-09-10-1033/run.json",
"runs/document-haness/2026-09-10-1033/stage/S6-before.md"
],
"outputs": [
"docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md"
],
"gates": [
{
"cmd": "node .agents/skills/writing-as-the-person-who-did-it/scripts/check_voice.mjs docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
},
{
"cmd": "node .agents/skills/rewriting-technical-prose-naturally/scripts/check_prose.mjs --warn docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
},
{
"cmd": "python3 scripts/studio-body.py docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md -o /tmp/s6-body.md",
"exit": 0
},
{
"cmd": "node --experimental-transform-types .agents/skills/writing-tech-log-records/scripts/check_body.mjs /tmp/s6-body.md",
"exit": 0
},
{
"cmd": "node .agents/skills/writing-tech-log-records/scripts/check_evidence.mjs document-haness --repo",
"exit": 0
},
{
"cmd": "python3 scripts/check-preservation.py runs/document-haness/2026-09-10-1033/stage/S6-before.md docs/document-haness/tech-log-studio/pipeline-gate-exit-codes/case/case-exit-code-read-behind-a-pipe.md",
"exit": 0
}
],
"notes": "흔적 있음. 두 곳을 넣었고 둘 다 근거를 파일과 칸으로 댔다. (A) 「왜 그 둘만인가」 절에 이 런이 겪은 어긋남을 그 대목에 놓았다 — 두 숫자를 섞어 넷을 셋으로 적었고 그것이 분석 문서 네 곳과 증거 메타 둘에 실렸으며 증거 원문에서 줄을 다시 세어 고쳤다는 것. 근거는 이 원장의 S1·S3 notes 다. (B) 「손으로 적지 못하게 했다」 절에 도구를 쓴 사람의 말을 옮겼다 — scripts/capture-evidence.py 의 docstring :5 와 :9 다. 기록이 기능만 적고 목표와 이유는 안 적던 자리다. 인용부호나 백틱으로 감싸지 않고 평문으로 녹였다 — check-preservation 이 새로 생긴 인라인코드와 「」 직접인용을 「새로생김」으로 세기 때문이다. 낱말은 docstring 그대로다. 안 넣은 것도 적었다. 「처음에는」·「고민 끝에」·「놀랍게도」는 자료에 없어 한 건도 안 썼고, 커밋 메시지는 capture-evidence.py 가 아직 untracked 라 흔적이 없다 — 찾아봤고 없었다. 이미 있던 사람의 흔적 셋(같은 착각이 한 번 더 났다 · 셸 래퍼도 됐지만 · 처음 판에서는 이 도구도 인자를 잘못 먹었다)에는 손대지 않았다. 서브에이전트가 계약의 어긋남 하나를 올렸다 — candidates[DH-C03].reason 이 「한 번밖에 안 써서」라고 적는데 이 도구가 적은 메타가 여덟 건이다. 자료와 어긋나 그 문장을 근거로 못 쓴다고 판단하고 안 넣었다. 오케스트레이터가 메타를 세어 확인한 뒤 계약을 「이 저장소 하나에서만 써서 … 실행 여덟 건이 전부 이 프로젝트의 증거 수집이고 다른 프로젝트나 브라우저 캡처에 걸어 본 적이 없다」로 고쳤다."
},
{
"id": "S7",
"name": "Studio 저장",
"skill": "publishing-tech-log-to-studio",
"runBy": "orchestrator",
"status": "SKIPPED",
"skipReason": "Studio 반입을 요청받지 않았다. 이 배치에서 Studio 브라우저 세션은 통합 세션 A 가 소유하고, 게시 권한이 저장 권한과 분리돼 있지 않아 무인 저장이 막혀 있다 (A-studio-change-requests.md 의 CR-001). 화면을 열지 않았다.",
"skillEcho": "",
"inputs": [],
"outputs": [],
"gates": [],
"notes": "기록 frontmatter 의 id 와 studio 는 비어 있다. 저장한 적이 없다는 뜻이고 그대로 둔다."
}
]
}
@@ -0,0 +1,275 @@
---
id:
kind: CASE
slug: exit-code-read-behind-a-pipe
title: 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
topic: pipeline-gate-exit-codes
topicName: 관문의 종료 코드
project: document-haness
status: 게시 전
studio: ""
lastVerifiedOn: 2026-09-10
source:
- final/document.md#§2-관찰한-것
- final/document.md#§3-파이프-뒤의-종료-코드
- final/document.md#§4-다시-잰-값
- final/document.md#§5-argparse-를-쓰지-않는-셋
- final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다
sourceRevision: 43e1aadef077ad93c30495df428ee3a71dd73f4a
evidence:
- ../../../final/evidence/raw/exit-code-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-without-a-pipe.txt
- ../../../final/evidence/raw/argparse-absent-scripts.txt
- ../../../final/evidence/raw/what-the-three-print-for-help.txt
---
# 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
`scripts/` 의 검사기 열넷에 `--help` 를 돌려 전부 종료 코드 0 을 받았다. 두 세션이 각자
같은 값을 얻어 계약 문서에까지 「14개 전부 `--help` 가 종료 코드 0」으로 들어갔다. 값은
검사기가 아니라 재는 방법이 만든 것이었다. 파이프를 걷어 내고 다시 재니 둘이 `exit 1` 이다.
## 관계
- **검사할 것이 없을 때 관문은 무엇을 내야 하는가**
이 사건에서 `audit-records.py --help` 가 「문제 없음」을 찍으면서 그 물음이 열렸다.
`--help` 라는 이름의 프로젝트에는 검사할 기록이 하나도 없어 그대로 통과했다.
## 문제
관문은 종료 코드로 말한다. 단계 계약이 「관문은 종료 코드가 0 이어야 지난 것이다」 라고
적었고, 런 원장 run.json 의 stages[].gates[].exit 에 그 값이 남는다. 사람이 셸에서 그 값을
읽어 옮겨 적는다.
--help 하나를 잰 것뿐인데 값이 두 번 틀렸다. 관문 결과 전부가 같은 방법으로 적히고 있다.
## 결론
리비전 43e1aad 의 scripts/*.py 열넷 중 열둘이 --help 에 exit 0,
build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다.
exit 0 이 usage 가 나왔다는 뜻은 아니다 — audit-records.py 는 --help 를 프로젝트
이름으로 받아 검사하고 통과시킨다.
값을 틀리게 만든 것은 셸의 동작 둘이다. 파이프의 종료 코드 변수는 마지막 명령을 가리키고,
명령 치환은 그 변수를 덮어쓴다. 둘 다 정의된 동작이라 셸이 경고하지 않는다.
조치로 scripts/capture-evidence.py 를 만들었다. 명령을 subprocess 로 직접 돌리고 그
프로세스의 반환값을 그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을
경로가 없다.
## 검증 환경
python 3.12.3 · node v24.14.0 · bash 5.2.21(1)-release · Linux.
대상 저장소 document-haness 리비전 43e1aadef077ad93c30495df428ee3a71dd73f4a.
측정은 그 커밋에서 갈라진 worktree dh-B 에서 했고, 그 시점 작업 트리에는
capture-evidence.py 가 더해져 있어 증거 메타의 sourceDirty 가 true 다.
측정 대상 열넷은 git ls-tree 로 그 커밋의 목록만 골라 냈다.
## 재현 조건
1. document-haness 를 43e1aad 로 체크아웃한다.
2. 파이프를 끼고 잰다 — 반복문 안에서 python3 출력을 head 로 넘기고 그다음 줄에서 $? 를 읽는다.
3. 열넷이 전부 exit=0 으로 나오는 것을 본다.
4. 파이프를 걷고 out=$(...) 다음 줄에서 code=$? 로 받아 다시 잰다.
5. build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit=1 로 갈리는 것을 본다.
## 본문
<!-- body:start -->
## 두 번 같은 값이 나왔다
검사기 목록을 만들려고 열넷에 `--help` 를 돌렸다. 출력이 길어서 `head` 로 잘랐다.
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
열넷이 전부 `exit=0` 이었다.
```
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
```
이 저장소를 함께 조사한 다른 세션도 같은 값을 얻어 「`scripts/*.py` 14개 전부 `--help`
종료 코드 0 으로 돌아온다」 를 확인 등급 **확인함**으로 적었다. 두 사람이 같은 값을 얻었으니
맞는 값처럼 보였다.
## 값을 만든 것은 셸이다
파이프라인의 `$?`**마지막** 명령의 종료 코드다. `head` 는 언제나 성공하므로 앞의
`python3` 가 무엇을 반환하든 `$?` 는 0 이 된다.
```bash
set +o pipefail
false | head -1; echo "false | head -1 -> exit=$?"
false; echo "false -> exit=$?"
```
```
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
```
POSIX 셸의 정의된 동작이고 이 셸의 특이점이 아니다. `pipefail` 을 켜거나
`${PIPESTATUS[0]}` 를 읽으면 앞 명령의 값을 얻는다.
같은 착각이 한 번 더 났다. 파이프를 걷어 내고 다시 잴 때 이렇게 썼다.
```bash
out=$(timeout 60 python3 $s --help 2>&1)
echo "$(basename $s) exit=$?"
```
명령 치환 `$(basename $s)` 가 먼저 실행되면서 `$?` 를 덮어썼다. 그래서 두 번째 측정도
열넷 전부 0 이었다. `code=$?` 를 명령 바로 다음 줄에 두고서야 값이 갈렸다.
## 다시 잰 값
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
out=$(timeout 60 python3 $s --help 2>&1)
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
두 측정의 차이는 두 줄뿐이다. 잰 것은 `--help` 하나뿐이라 나머지 열둘이 다른 인자에서
어떻게 도는지는 이 값이 말해 주지 않는다.
```
2c2
< build-tech-log-tree.py exit=0
---
> build-tech-log-tree.py exit=1
13c13
< verify-refactor-work-item.py exit=0
---
> verify-refactor-work-item.py exit=1
```
## 왜 그 둘만인가
열넷 중 셋이 `argparse` 를 쓰지 않는다.
| 스크립트 | `argparse` | `--help` |
|---|---|---|
| `audit-records.py` | 없음 | `exit 0` — 「문제 없음」 |
| `build-tech-log-tree.py` | 없음 | `exit 1` |
| `techlog.py` | 없음 | `exit 0` — CLI 가 아니라 인자를 안 읽는다 |
| `verify-refactor-work-item.py` | 없음 | `exit 1` |
| 나머지 열 | 있음 | `exit 0` — usage |
남은 둘은 `--help` 를 옵션이 아니라 위치 인자로 먹는다. `audit-records.py` 는 그것을
프로젝트 이름으로 받아 「문제 없음」을 찍고, `build-tech-log-tree.py`
`verify-refactor-work-item.py` 는 그 이름의 파일을 못 찾아 실패한다.
```
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
```
`exit 0` 이 usage 가 나왔다는 뜻은 아니다. `audit-records.py``--help` 라는 이름의
프로젝트를 찾아 검사하고 통과시켰다.
## 손으로 적지 못하게 했다
관문 결과를 사람이 옮겨 적는 한 같은 뿌리에서 계속 난다. 그래서 명령을 돌리는 쪽과
종료 코드를 적는 쪽을 하나로 붙였다.
```python
proc = subprocess.run(command, cwd=cwd, capture_output=True,
text=True, timeout=timeout)
exit_code, out = proc.returncode, proc.stdout + proc.stderr
```
셸 한 줄을 감싸는 래퍼도 됐지만 그러면 파이프를 다시 쓸 수 있게 된다. 값을 두 번 틀리게
만든 것이 바로 그 셸이라 아예 거치지 않기로 했다. 대신 셸 문법이 필요한 명령은
`bash -c` 를 인자로 넘겨야 하고, 그 안에서 다시 파이프를 쓰면 같은 실수가 난다 — 이 기록의
증거 여섯 개도 그렇게 수집했다. 종료 코드를 인자로 받지 않으므로 손으로 적어 넣을 수는 없다. 원문은 `final/evidence/raw/` 에, 실행 메타는 `final/evidence/meta/`
같은 이름으로 함께 떨어진다.
처음 판에서는 이 도구도 인자를 잘못 먹었다. `argparse.REMAINDER` 로 명령을 받았더니
`--proves` 부터가 실행할 명령으로 딸려 가 `No such file or directory: '--proves'` 로 죽었다.
지금은 `--` 앞뒤를 직접 가르고, 왜 그렇게 했는지를 코드에 한 줄로 남겨 두었다.
```python
# `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다
argv = sys.argv[1:]
```
`--help` 를 위치 인자로 먹은 세 스크립트와 같은 종류의 실수다. 이 저장소에서 인자를 손으로
가르는 코드는 대개 여기서 걸린다.
이 기록이 인용한 원문 다섯 개를 그 도구가 수집했다. 메타 하나는 이렇게 생겼다.
```json
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
```
여기서 `exitCode: 0` 은 **측정 반복문 자체가 끝까지 돌았다**고 말한다. 열넷 각각의 종료
코드는 원문 `raw/` 안에 있다. 반복문이 반환한 값과 그 안에서 잰 값을 섞지 않는다 — 메타에는
반복문 쪽이 들어간다.
`sourceDirty` 는 그 실행 시점에 작업 트리에 커밋 안 된 변경이 있었는지 적는다. 있으면
`sourceRevision` 이 그 출력을 설명하지 못한다. 이 다섯은 전부 `true` 다 — 수집기 자신을
더한 상태에서 쟀다.
기존 경로를 지우지 않았다. 손으로 만든 `raw/`·`meta/` 도 그대로 쓰이고, 이 도구는 필요할
때만 부른다.
## 이 사건이 닫지 못한 것
수집기는 브라우저 캡처와 대화형 명령을 담지 못한다. 이 저장소의 증거 가운데
`final/evidence/browser/` 쪽은 여전히 Playwright 로 찍고 메타를 손으로 적는다 — 종료 코드를
손으로 적을 수 없게 만든 것이 아직 절반이라는 뜻이다.
<!-- body:end -->
@@ -0,0 +1,275 @@
---
id:
kind: CASE
slug: exit-code-read-behind-a-pipe
title: 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
topic: pipeline-gate-exit-codes
topicName: 관문의 종료 코드
project: document-haness
status: 게시 전
studio: ""
lastVerifiedOn: 2026-09-10
source:
- final/document.md#§2-관찰한-것
- final/document.md#§3-파이프-뒤의-종료-코드
- final/document.md#§4-다시-잰-값
- final/document.md#§5-argparse-를-쓰지-않는-넷
- final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다
sourceRevision: 43e1aadef077ad93c30495df428ee3a71dd73f4a
evidence:
- ../../../final/evidence/raw/exit-code-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-without-a-pipe.txt
- ../../../final/evidence/raw/argparse-absent-scripts.txt
- ../../../final/evidence/raw/what-the-three-print-for-help.txt
---
# 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
scripts/ 의 검사기 열넷에 --help 를 돌려 전부 종료 코드 0 을 받았다. 이 저장소를 함께
조사한 다른 세션도 같은 값을 얻어 확인 등급 확인함으로 적었다. 값은 검사기가 아니라 재는
방법이 만든 것이었다. 파이프를 걷어 내고 다시 재니 둘이 exit 1 이다.
## 관계
- **검사할 것이 없을 때 관문은 무엇을 내야 하는가**
이 사건에서 `audit-records.py --help` 가 「문제 없음」을 찍으면서 그 물음이 열렸다.
`--help` 라는 이름의 프로젝트에는 검사할 기록이 하나도 없어 그대로 통과했다.
## 문제
관문은 종료 코드로 말한다. 단계 계약이 「관문은 종료 코드가 0 이어야 지난 것이다」 라고
적었고, 런 원장 run.json 의 stages[].gates[].exit 에 그 값이 남는다. 사람이 셸에서 그 값을
읽어 옮겨 적는다.
--help 하나를 잰 것뿐인데 값이 두 번 틀렸다. 관문 결과 전부가 같은 방법으로 적히고 있다.
## 결론
리비전 43e1aad 의 scripts/*.py 열넷 중 열둘이 --help 에 exit 0,
build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다.
exit 0 이 usage 가 나왔다는 뜻은 아니다 — audit-records.py 는 --help 를 프로젝트
이름으로 받아 검사하고 통과시킨다.
값을 틀리게 만든 것은 셸의 동작 둘이다. 파이프의 종료 코드 변수는 마지막 명령을 가리키고,
명령 치환은 그 변수를 덮어쓴다. 둘 다 정의된 동작이라 셸이 경고하지 않는다.
조치로 scripts/capture-evidence.py 를 만들었다. 명령을 subprocess 로 직접 돌리고 그
프로세스의 반환값을 그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을
경로가 없다.
## 검증 환경
python 3.12.3 · node v24.14.0 · bash 5.2.21(1)-release · Linux.
대상 저장소 document-haness 리비전 43e1aadef077ad93c30495df428ee3a71dd73f4a.
측정은 그 커밋에서 갈라진 worktree dh-B 에서 했고, 그 시점 작업 트리에는
capture-evidence.py 가 더해져 있어 증거 메타의 sourceDirty 가 true 다.
측정 대상 열넷은 git ls-tree 로 그 커밋의 목록만 골라 냈다.
## 재현 조건
1. document-haness 를 43e1aad 로 체크아웃한다.
2. 파이프를 끼고 잰다 — 반복문 안에서 python3 출력을 head 로 넘기고 그다음 줄에서 $? 를 읽는다.
3. 열넷이 전부 exit=0 으로 나오는 것을 본다.
4. 파이프를 걷고 out=$(...) 다음 줄에서 code=$? 로 받아 다시 잰다.
5. build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit=1 로 갈리는 것을 본다.
## 본문
<!-- body:start -->
## 두 번 같은 값이 나왔다
검사기 목록을 만들려고 열넷에 `--help` 를 돌렸다. 출력이 길어서 `head` 로 잘랐다.
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
열넷이 전부 `exit=0` 이었다.
```
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
```
이 저장소를 함께 조사한 다른 세션도 같은 값을 얻어 「`scripts/*.py` 14개 전부 `--help`
종료 코드 0 으로 돌아온다」 를 확인 등급 **확인함**으로 적었다. 두 사람이 같은 값을 얻었으니
맞는 값처럼 보였다.
## 값을 만든 것은 셸이다
파이프라인의 `$?`**마지막** 명령의 종료 코드다. `head` 는 언제나 성공하므로 앞의
`python3` 가 무엇을 반환하든 `$?` 는 0 이 된다.
```bash
set +o pipefail
false | head -1; echo "false | head -1 -> exit=$?"
false; echo "false -> exit=$?"
```
```
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
```
POSIX 셸의 정의된 동작이고 이 셸의 특이점이 아니다. `pipefail` 을 켜거나
`${PIPESTATUS[0]}` 를 읽으면 앞 명령의 값을 얻는다.
같은 착각이 한 번 더 났다. 파이프를 걷어 내고 다시 잴 때 이렇게 썼다.
```bash
out=$(timeout 60 python3 $s --help 2>&1)
echo "$(basename $s) exit=$?"
```
명령 치환 `$(basename $s)` 가 먼저 실행되면서 `$?` 를 덮어썼다. 그래서 두 번째 측정도
열넷 전부 0 이었다. `code=$?` 를 명령 바로 다음 줄에 두고서야 값이 갈렸다.
## 다시 잰 값
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
out=$(timeout 60 python3 $s --help 2>&1)
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
두 측정의 차이는 두 줄뿐이다. 잰 것은 `--help` 하나뿐이라 나머지 열둘이 다른 인자에서
어떻게 도는지는 이 값이 말해 주지 않는다.
```
2c2
< build-tech-log-tree.py exit=0
---
> build-tech-log-tree.py exit=1
13c13
< verify-refactor-work-item.py exit=0
---
> verify-refactor-work-item.py exit=1
```
## 왜 그 둘만인가
열넷 중 넷이 `argparse` 를 쓰지 않는다.
| 스크립트 | `argparse` | `--help` |
|---|---|---|
| `audit-records.py` | 없음 | `exit 0` — 「문제 없음」 |
| `build-tech-log-tree.py` | 없음 | `exit 1` |
| `techlog.py` | 없음 | `exit 0` — CLI 가 아니라 인자를 안 읽는다 |
| `verify-refactor-work-item.py` | 없음 | `exit 1` |
| 나머지 열 | 있음 | `exit 0` — usage |
남은 셋은 `--help` 를 옵션이 아니라 위치 인자로 먹는다. `audit-records.py` 는 그것을
프로젝트 이름으로 받아 「문제 없음」을 찍고, `build-tech-log-tree.py`
`verify-refactor-work-item.py` 는 그 이름의 파일을 못 찾아 실패한다.
```
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
```
`exit 0` 이 usage 가 나왔다는 뜻은 아니다. `audit-records.py``--help` 라는 이름의
프로젝트를 찾아 검사하고 통과시켰다.
## 손으로 적지 못하게 했다
관문 결과를 사람이 옮겨 적는 한 같은 뿌리에서 계속 난다. 그래서 명령을 돌리는 쪽과
종료 코드를 적는 쪽을 하나로 붙였다.
```python
proc = subprocess.run(command, cwd=cwd, capture_output=True,
text=True, timeout=timeout)
exit_code, out = proc.returncode, proc.stdout + proc.stderr
```
셸 한 줄을 감싸는 래퍼도 됐지만 그러면 파이프를 다시 쓸 수 있게 된다. 값을 두 번 틀리게
만든 것이 바로 그 셸이라 아예 거치지 않기로 했다. 대신 셸 문법이 필요한 명령은
`bash -c` 를 인자로 넘겨야 하고, 그 안에서 다시 파이프를 쓰면 같은 실수가 난다 — 이 기록의
증거 여섯 개도 그렇게 수집했다. 종료 코드를 인자로 받지 않으므로 손으로 적어 넣을 수는 없다. 원문은 `final/evidence/raw/` 에, 실행 메타는 `final/evidence/meta/`
같은 이름으로 함께 떨어진다.
처음 판에서는 이 도구도 인자를 잘못 먹었다. `argparse.REMAINDER` 로 명령을 받았더니
`--proves` 부터가 실행할 명령으로 딸려 가 `No such file or directory: '--proves'` 로 죽었다.
지금은 `--` 앞뒤를 직접 가르고, 왜 그렇게 했는지를 코드에 한 줄로 남겨 두었다.
```python
# `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다
argv = sys.argv[1:]
```
`--help` 를 위치 인자로 먹은 세 스크립트와 같은 종류의 실수다. 이 저장소에서 인자를 손으로
가르는 코드는 대개 여기서 걸린다.
이 기록이 인용한 원문 다섯 개를 그 도구가 수집했다. 메타 하나는 이렇게 생겼다.
```json
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
```
여기서 `exitCode: 0` 은 **측정 반복문 자체가 끝까지 돌았다**고 말한다. 열넷 각각의 종료
코드는 원문 `raw/` 안에 있다. 반복문이 반환한 값과 그 안에서 잰 값을 섞지 않는다 — 메타에는
반복문 쪽이 들어간다.
`sourceDirty` 는 그 실행 시점에 작업 트리에 커밋 안 된 변경이 있었는지 적는다. 있으면
`sourceRevision` 이 그 출력을 설명하지 못한다. 이 다섯은 전부 `true` 다 — 수집기 자신을
더한 상태에서 쟀다.
기존 경로를 지우지 않았다. 손으로 만든 `raw/`·`meta/` 도 그대로 쓰이고, 이 도구는 필요할
때만 부른다.
## 이 사건이 닫지 못한 것
수집기는 브라우저 캡처와 대화형 명령을 담지 못한다. 이 저장소의 증거 가운데
`final/evidence/browser/` 쪽은 여전히 Playwright 로 찍고 메타를 손으로 적는다 — 종료 코드를
손으로 적을 수 없게 만든 것이 아직 절반이라는 뜻이다.
<!-- body:end -->
@@ -0,0 +1,278 @@
---
id:
kind: CASE
slug: exit-code-read-behind-a-pipe
title: 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
topic: pipeline-gate-exit-codes
topicName: 관문의 종료 코드
project: document-haness
status: 게시 전
studio: ""
lastVerifiedOn: 2026-09-10
source:
- final/document.md#§2-관찰한-것
- final/document.md#§3-파이프-뒤의-종료-코드
- final/document.md#§4-다시-잰-값
- final/document.md#§5-argparse-를-쓰지-않는-넷
- final/document.md#§8-종료-코드를-손으로-적을-수-없게-만든다
sourceRevision: 43e1aadef077ad93c30495df428ee3a71dd73f4a
evidence:
- ../../../final/evidence/raw/exit-code-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-through-a-pipe.txt
- ../../../final/evidence/raw/help-exit-codes-measured-without-a-pipe.txt
- ../../../final/evidence/raw/argparse-absent-scripts.txt
- ../../../final/evidence/raw/what-the-three-print-for-help.txt
---
# 파이프 뒤의 종료 코드를 읽고 검사기 열넷이 다 통과한다고 적었다
scripts/ 의 검사기 열넷에 --help 를 돌려 전부 종료 코드 0 을 받았고, 이 저장소를 함께
조사한 다른 세션도 같은 값을 얻어 확인 등급 확인함으로 적었다. 값은 검사기가 아니라 재는
방법이 만든 것이었다. 파이프를 걷어 내고 다시 재니 둘이 exit 1 이다.
## 관계
- **검사할 것이 없을 때 관문은 무엇을 내야 하는가**
이 사건에서 `audit-records.py --help` 가 「문제 없음」을 찍으면서 그 물음이 열렸다.
`--help` 라는 이름의 프로젝트에는 검사할 기록이 하나도 없어 그대로 통과했다.
## 문제
관문은 종료 코드로 말한다. 단계 계약이 「관문은 종료 코드가 0 이어야 지난 것이다」 라고
적었고, 런 원장 run.json 의 stages[].gates[].exit 에 그 값이 남는데, 그 값을 사람이 셸에서
읽어 옮겨 적는다.
--help 하나를 잰 것뿐인데 값이 두 번 틀렸다. 관문 결과 전부가 같은 방법으로 적히고 있다.
## 결론
리비전 43e1aad 의 scripts/*.py 열넷 중 열둘이 --help 에 exit 0,
build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다.
exit 0 이 usage 가 나왔다는 뜻은 아니다 — audit-records.py 는 --help 를 프로젝트
이름으로 받아 검사하고 통과시킨다.
값을 틀리게 만든 것은 셸의 동작 둘이다. 파이프의 종료 코드 변수는 마지막 명령을 가리키고,
명령 치환은 그 변수를 덮어쓴다. 둘 다 정의된 동작이라 셸이 경고하지 않는다.
조치로 만든 scripts/capture-evidence.py 는 명령을 subprocess 로 직접 돌리고 그 프로세스의
반환값을 그대로 메타에 적는다. 종료 코드를 인자로 받지 않으므로 손으로 적을 경로가 없다.
## 검증 환경
python 3.12.3 · node v24.14.0 · bash 5.2.21(1)-release · Linux.
대상 저장소 document-haness 리비전 43e1aadef077ad93c30495df428ee3a71dd73f4a.
측정은 그 커밋에서 갈라진 worktree dh-B 에서 했고, 그 시점 작업 트리에는
capture-evidence.py 가 더해져 있어 증거 메타의 sourceDirty 가 true 다.
측정 대상 열넷은 git ls-tree 로 그 커밋의 목록만 골라 냈다.
## 재현 조건
1. document-haness 를 43e1aad 로 체크아웃한다.
2. 파이프를 끼고 잰다 — 반복문 안에서 python3 출력을 head 로 넘기고 그다음 줄에서 $? 를 읽는다.
3. 열넷이 전부 exit=0 으로 나오는 것을 본다.
4. 파이프를 걷고 out=$(...) 다음 줄에서 code=$? 로 받아 다시 잰다.
5. build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit=1 로 갈리는 것을 본다.
## 본문
<!-- body:start -->
## 두 번 같은 값이 나왔다
검사기 목록을 만들려고 열넷에 `--help` 를 돌렸는데, 출력이 길어서 `head` 로 잘랐다.
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
timeout 60 python3 $s --help 2>&1 | head -30 >/dev/null
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
열넷이 전부 `exit=0` 이었다.
```
audit-records.py exit=0
build-tech-log-tree.py exit=0
check-figure-overlap.py exit=0
check-figure-text.py exit=0
fold-analysis-into-final.py exit=0
fold-studio-contract-into-index.py exit=0
preview-figure.py exit=0
studio-body.py exit=0
techlog.py exit=0
verify-pipeline-run.py exit=0
verify-pipeline.py exit=0
verify-project-layout.py exit=0
verify-refactor-work-item.py exit=0
verify-tech-log-tree.py exit=0
```
이 저장소를 함께 조사한 다른 세션도 같은 값을 얻어 「`scripts/*.py` 14개 전부 `--help`
종료 코드 0 으로 돌아온다」 를 확인 등급 **확인함**으로 적었다. 두 사람이 같은 값을 얻었으니
맞는 값처럼 보였다.
## 값을 만든 것은 셸이다
파이프라인의 `$?`**마지막** 명령의 종료 코드다. `head` 는 언제나 성공하므로 앞의
`python3` 가 무엇을 반환하든 `$?` 는 0 이 된다.
```bash
set +o pipefail
false | head -1; echo "false | head -1 -> exit=$?"
false; echo "false -> exit=$?"
```
```
false | head -1 -> exit=0
false -> exit=1
bash 5.2.21(1)-release
```
POSIX 셸의 정의된 동작이고 이 셸의 특이점이 아니다. `pipefail` 을 켜거나
`${PIPESTATUS[0]}` 를 읽으면 앞 명령의 값을 얻는다.
같은 착각이 한 번 더 났다. 파이프를 걷어 내고 다시 잴 때 이렇게 썼다.
```bash
out=$(timeout 60 python3 $s --help 2>&1)
echo "$(basename $s) exit=$?"
```
명령 치환 `$(basename $s)` 가 먼저 실행되면서 `$?` 를 덮어써서 두 번째 측정도 열넷 전부
0 이었다. `code=$?` 를 명령 바로 다음 줄에 두고서야 값이 갈렸다.
## 다시 잰 값
```bash
for s in $(git ls-tree --name-only 43e1aad scripts/ | grep '\.py$'); do
out=$(timeout 60 python3 $s --help 2>&1)
code=$?
n=$(basename $s)
echo "$n exit=$code"
done
```
두 측정의 차이는 두 줄뿐이다.
```
2c2
< build-tech-log-tree.py exit=0
---
> build-tech-log-tree.py exit=1
13c13
< verify-refactor-work-item.py exit=0
---
> verify-refactor-work-item.py exit=1
```
잰 것은 `--help` 하나뿐이라 나머지 열둘이 다른 인자에서 어떻게 도는지는 이 값이 말해 주지
않는다.
## 왜 그 둘만인가
열넷 중 넷이 `argparse` 를 쓰지 않는다.
| 스크립트 | `argparse` | `--help` |
|---|---|---|
| `audit-records.py` | 없음 | `exit 0` — 「문제 없음」 |
| `build-tech-log-tree.py` | 없음 | `exit 1` |
| `techlog.py` | 없음 | `exit 0` — CLI 가 아니라 인자를 안 읽는다 |
| `verify-refactor-work-item.py` | 없음 | `exit 1` |
| 나머지 열 | 있음 | `exit 0` — usage |
남은 셋은 `--help` 를 옵션이 아니라 위치 인자로 먹는다. `audit-records.py` 는 그것을
프로젝트 이름으로 받아 「문제 없음」을 찍고, `build-tech-log-tree.py`
`verify-refactor-work-item.py` 는 그 이름의 파일을 못 찾아 실패한다.
```
$ python3 scripts/audit-records.py --help
--help — 기록 0건 · 원문 0 · 메타 0 · 렌더 0
문제 없음
합계 0건
exit=0
$ python3 scripts/build-tech-log-tree.py --help
--help: tech-log-tree.json 이 없다. 글감을 먼저 적는다
exit=1
$ python3 scripts/verify-refactor-work-item.py --help
REFACTOR WORK ITEM VERIFICATION: FAIL
- invalid work-item.json: --help/work-item.json
exit=1
```
`exit 0` 이 usage 가 나왔다는 뜻은 아니다. `audit-records.py``--help` 라는 이름의
프로젝트를 찾아 검사하고 통과시켰다.
## 손으로 적지 못하게 했다
관문 결과를 사람이 옮겨 적는 한 같은 뿌리에서 같은 실수가 계속 난다. 그래서 명령을 돌리는
쪽과 종료 코드를 적는 쪽을 하나로 붙였다.
```python
proc = subprocess.run(command, cwd=cwd, capture_output=True,
text=True, timeout=timeout)
exit_code, out = proc.returncode, proc.stdout + proc.stderr
```
종료 코드를 인자로 받지 않으므로 손으로 적어 넣을 수는 없다. 원문은
`final/evidence/raw/` 에, 실행 메타는 `final/evidence/meta/` 에 같은 이름으로 함께 떨어진다.
셸 한 줄을 감싸는 래퍼도 됐지만 그러면 파이프를 다시 쓸 수 있게 된다. 값을 두 번 틀리게
만든 것이 바로 그 셸이라 아예 거치지 않기로 했다. 대신 셸 문법이 필요한 명령은 `bash -c`
인자로 넘겨야 하고, 그 안에서 다시 파이프를 쓰면 같은 실수가 난다 — 이 기록의 증거
다섯 개도 그렇게 수집했다.
처음 판에서는 이 도구도 인자를 잘못 먹었다. `argparse.REMAINDER` 로 명령을 받았더니
`--proves` 부터가 실행할 명령으로 딸려 가 `No such file or directory: '--proves'` 로 죽었다.
지금은 `--` 앞뒤를 직접 가르고, 왜 그렇게 했는지를 코드에 한 줄로 남겨 두었다.
```python
# `--` 앞뒤를 먼저 가른다. argparse.REMAINDER 에 맡기면 옵션이 명령으로 딸려 간다
argv = sys.argv[1:]
```
`--help` 를 위치 인자로 먹은 세 스크립트와 같은 종류의 실수다. 이 저장소에서 인자를 손으로
가르는 코드는 대개 여기서 걸린다.
이 기록이 인용한 원문 다섯 개를 그 도구가 수집했다. 메타 하나는 이렇게 생겼다.
```json
{
"id": "help-exit-codes-measured-without-a-pipe",
"kind": "terminal",
"sourceRevision": "43e1aadef077ad93c30495df428ee3a71dd73f4a",
"sourceDirty": true,
"executedAt": "2026-09-10T09:54:41+09:00",
"exitCode": 0,
"exitCodeSource": "실행한 프로세스의 반환값. 손으로 적지 않는다",
"rawPath": "evidence/raw/help-exit-codes-measured-without-a-pipe.txt",
"proves": "파이프 없이 재면 같은 14개 중 build-tech-log-tree.py 와 verify-refactor-work-item.py 가 exit 1 이다",
"doesNotProve": "다른 인자에서의 동작. --help 하나만 잰 값이다",
"sha256": "e8c134d07592bb1b051ea6ad3abf952a9fa502fc85eb26cdb56194447ea03a9b",
"bytes": 410
}
```
여기서 `exitCode: 0` 은 **측정 반복문 자체가 끝까지 돌았다**고 말한다. 열넷 각각의 종료
코드는 원문 `raw/` 안에 있다. 반복문이 반환한 값과 그 안에서 잰 값을 섞지 않는다 — 메타에는
반복문 쪽이 들어간다.
`sourceDirty` 는 그 실행 시점에 작업 트리에 커밋 안 된 변경이 있었는지 적는다. 있으면
`sourceRevision` 이 그 출력을 설명하지 못한다. 이 다섯은 전부 `true` 다 — 수집기 자신을
더한 상태에서 쟀다.
기존 경로를 지우지 않았다. 손으로 만든 `raw/`·`meta/` 도 그대로 쓰이고, 이 도구는 필요할
때만 부른다.
## 이 사건이 닫지 못한 것
수집기는 브라우저 캡처와 대화형 명령을 담지 못한다. 이 저장소의 증거 가운데
`final/evidence/browser/` 쪽은 여전히 Playwright 로 찍고 메타를 손으로 적는다 — 종료 코드를
손으로 적을 수 없게 만든 것이 아직 절반이라는 뜻이다.
<!-- body:end -->