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": {}
}