Conversation
PR #88이 발화 축을 턴 경계에서 이벤트 클래스 인스턴스로 옮기면서 본문만
개편해, 모델이 가장 먼저·가장 자주 읽는 표면이 폐기된 계약을 실행 지시로
유지하고 있었다. 본문은 "턴 종료 자동 발화 없음"을 말하는데 frontmatter는
"CANCEL까지 매 turn fire-now"를 지시한다. 이 상태로 기존 픽스처 35종이 전부
통과하므로 CI가 계약 드리프트를 한 건도 잡지 못한다.
- frontmatter 6행 교체 — 모드 선택에서 무조건적 `마다`→repeat 주장을 걷어내
재발 한정사가 명사를 수식할 때로 좁히고, 벽시계 요청 ARM 금지·인스턴스 단위
발화·self-cancel과 fire-first 순서를 상시 로드 표면에 올린다. `usage`는 예산
린트가 읽지 않지만 같은 폐기 문구를 담고 있어 함께 고친다
- `## Control-Flow Invariants` 섹션 신설 — 흩어져 있던 5개 제어 규칙을 한 구간
으로 모은다. 되돌릴 수 없는 규칙은 결과 절을 가드의 일부로 적는다. 순서만
서술하면 문체로 읽혀 재배열을 부른다
- `active-notify`를 `lint-skill-invariants.sh` 면제 목록에서 제거 — 면제 사유
("종료가 훅·도구 주도 경계이며 모델이 유지할 카운터가 아니다")가 self-cancel
도입으로 거짓이 되었다
- `_common/notify.md`을 디스패처 사실로 재정렬 — 종료가 사용자 CANCEL과 모델
self-cancel 두 갈래인데 디스패처는 둘을 같은 삭제로 처리하고 이벤트 클래스를
모른다. 따라서 마지막 fire-now를 cancel보다 앞세우는 것은 모델의 의무이지
디스패처의 보장이 아니다
- `_common` 적재 지시를 경로 앵커링 — bare 상대 경로는 세션 작업 디렉토리에서도
스킬 디렉토리에서도 해석되지 않는다
- 드리프트 린트 + 짝 테스트 + 픽스처 6종 신설, `Makefile`의 lint·test 타깃에
등록 — 인용 검증은 제목 앵커로 대조한다. 균일 +1 시프트는 항상 이웃한 실재
헤딩에 착지하므로 존재 검사로는 이번 사고를 한 건도 잡지 못한다. 수정 전
트리에서 9건 적발, 수정 후 0건
시계 기반 요청을 스케줄러에 위임하려면 예약된 턴이 배너를 낼 표면이 필요한데, 기존 표면은 전부 ARM 사이클에 묶여 있다. 예약 턴에는 사이클이 없으므로 fire-now는 플래그 부재로 무성 skip되고, 인라인 terminal-notifier 표현은 매 틱 바이트 단위로 재현해야 해 따옴표 하나만 어긋나도 자동 승인을 잃는다. - `fire-oneshot <workflow> <summary>` — 플래그를 읽지도 쓰지도 않고 락도 잡지 않는다. 살아 있는 ARM 사이클은 이 호출 전후로 바이트 동일하므로, 사이클이 없는 호출자가 남의 사이클을 건드릴 수 없다 - 전용 배너 그룹(`-tick` 접미사) — 같은 그룹 배너는 알림 센터에서 서로를 대체하므로, 사이클 그룹을 재사용하면 틱 배너가 완료 배너를 지운다. 그룹 분리가 장식이 아니라 이 회귀를 막는 장치다 - PreToolUse 훅의 `notify_re`에 토큰 추가 — 없으면 예약 턴의 배너가 사람이 없는 자리에서 권한 다이얼로그 뒤에 갇힌다 - 서브커맨드 열거를 실측으로 전수 갱신(개수 주장, 헤더 주석, 사용자 대면 오류 문자열, 공유 문서 3곳). "fire-now가 유일한 발신 표면"이라는 주장은 삭제가 아니라 **ARM 사이클로 범위를 좁힌다** — 구분 축은 디스패처 경유 여부가 아니라 사이클 소속 여부다 - 픽스처 3종 — ARM 없이도 발신되고 플래그를 만들지 않는지, 살아 있는 플래그가 바이트 동일한지, 두 그룹이 분리되는지, 훅 α·γ 양 경로가 allow인지
§2.6은 지금까지 시계 요청을 위임한다고 선언만 하고 배너에 도달할 표면이 없었다. ARM 금지·fire-now 금지·디스패처의 무성 exit 0이 서로를 닫아, 시계 요청은 실제로는 아무것도 하지 않는다. 위임처로 지목하던 인터벌 스킬도 일회성 지연의 수단이 아니었다 — 프롬프트를 즉시 1회 실행하고, 단일 wake가 클램프되며, 기본값 off인 플래그 뒤에 있다. 이제 두 분기 모두 스케줄러에 직접 예약한다. 배송 경로가 근거다 — 스케줄러는 저장된 프롬프트를 모델 왕복 없이 큐에 넣는 반면 인터벌 스킬은 매번 모델이 프롬프트를 다시 타이핑한다. - **Step 0 전제조건 점검** — 예약 전에 Darwin·terminal-notifier를 확인하고, 실패하면 예약하지 않고 말한다. 디스패처가 두 조건에서 조용히 exit 0 하므로 이 점검 없이 예약하면 30분 뒤 아무것도 하지 않는 턴이 열리고 그것을 관측할 사람이 없다. 옛 경로의 t=0 배너가 우연히 하던 역할을 의도적으로 대체한다. 점검 명령에 디스패처와 같은 PATH prepend를 복제해, 발화 가능한 호스트를 거짓 거절하는 반대 방향 실패를 막는다 - **2분 하한** — 다음-매치 탐색이 다음 분 시작에서 출발하므로 진행 중인 분은 절대 매치되지 않는다. 분·시·일·월이 고정된 일회성 표현식이 그 분에 놓이면 실패하는 게 아니라 1년 뒤에 매치된다. 검증을 통과하고 성공을 보고하고 영원히 발화하지 않는다. 1분으로는 부족하다 — 시각 계산과 호출 착지 사이에 분 경계를 한 번만 넘어도 1년 좌초에 충분하다 - **날짜 직접 계산 금지 + 인접성** — 네 필드를 한 번의 date 호출에서 얻고 그 직후 첫 행동으로 예약한다. 2분 하한이 사는 여유가 정확히 그 1분이다 - **가용성 탐침** — cron 도구는 지연 도구라 로드된 스키마를 보는 탐침은 언제나 부재로 판정한다. 그것은 거절을 뒤집어쓴 무성 실패다 - **침묵 분기 없음** — 예약에 이르지 못하는 두 경로(Step 0 실패·스케줄러 부재)는 "예약하지 않았다"로 끝나서는 안 된다. 폴백 사다리의 모든 단이 사용자에게 보이는 한 줄로 끝나며, 어떤 경우에도 arm으로 되돌아가지 않는다 - **고지** — 발화 시각은 구체적 로컬 시각으로, 중지는 예약 삭제로, 만료·첫 발화 계약은 분기별로 다르게 말한다. 반복 분기에 "첫 알림이 즉시 온다"고 말하지 않고 지연 수치를 인용하지 않는다. 작업 ID 소실에 대비해 목록 조회로 대상을 식별하는 회수 경로를 둔다 - **이중 취소 의무** — 이벤트 앵커 전환 제안이 시계 요청 사용자에게 이벤트 사이클을 열도록 유도하므로 둘이 동시에 살아 있을 수 있다. 한쪽만 끄고 취소했다고 보고하지 않는다 - §1.1 표와 §6.2 금지 목록의 범위를 ARM 사이클로 명시, 워크드 예시를 4단계로 교체 — 예시는 산문보다 모방되기 쉬워 옛 경로를 남기면 실질적으로 존치된다
§2.5의 게이트가 도달 불가능한 블록을 달고 있었고, 인스턴스 경계가 없는
클래스에 발화 정의가 없었으며, self-cancel 허용 조건이 모델이 할 수 없는
예측("더 나올 인스턴스가 없다")에 걸려 있었다.
- **Gate 0 신설** — 절을 먼저 가르고 알림 절만 게이트한다. 작업 절이 인터벌을
명명해도(`"5분마다 체크해서"`) 이 스킬의 일이 아니며 §2.6으로 보내지 않는다.
새 §2.6은 알림 전용 절차라 폴링 요구가 도착하면 맞는 절차가 없고 최악의
경우 폴링 대신 배너를 예약한다. 알림 절 자체가 시계 기반이면 반복·일회성
**양쪽 다** ARM하지 않고 위임한다
- **Gate 2를 2a/2b로 분할** — 한정사가 명사를 수식하는 경우와 수식할 명사가
없는 경우는 서로 다른 경로(후자는 §2.9 캐스케이드)라 한 게이트에 묶여 있을
이유가 없었다. 완전성 진술을 명시해 "어느 게이트에도 안 걸림" 출구가 없음을
못박는다
- **Gate 1 연결어에 `와`/`과`/`하고`/`이랑` 추가 + 닫힌 목록이 아님을 명시** —
이 항목만 상시 로드 표면이 맞고 본문이 틀렸다. 의미 기반 서술에 닫힌 목록을
이식하면 정상 동작하던 표면에 결함을 전파하게 된다
- **Hybrid 블록 해체** — 도달 불가능한 위치에 있던 두 항목을 각각 Gate 0(A)의
정상 경로와 bypass 라우팅을 소유하는 §2.0으로 옮긴다
- **인스턴스 하한** — 관측 가능한 경계가 없는 제네릭 클래스에 한해 user-task
도구 호출이 있었던 턴의 종료를 인스턴스 1회로 본다. 턴 종료 자동 발화의
부활이 아니라 인스턴스의 정의이며 정밀 클래스에는 적용되지 않는다. 여기에
**외부 스케줄러가 연 턴은 인스턴스가 아니다**를 붙여, 제네릭 클래스 ×
살아 있는 예약 × 소강 금지가 합성돼 종료 불가가 되는 경로를 함께 막는다
- **self-cancel을 행위자 기준으로 재작성** — 발화한 인스턴스가 전부 모델 자신이
dispatch한 것의 완료였고, 사용자 작업이 이번 턴에 끝나며 자기 계획에 인스턴스를
더 만들 단계가 없을 때만 허용. 사용자 본인의 커밋·CI·배포처럼 모델이 몰지 않는
계열이면 self-cancel 자체를 금지한다 — 모델은 자기 컨텍스트를 지나간 조각만
보므로 끝난 계열과 조용한 계열을 구분할 수 없다. §6.4는 정확한 여집합으로
다시 써 같은 판단을 두 번 요구하지 않는다
- **오탐 비용 서술 정정** — 잘못된 self-cancel은 "배너 하나"가 아니라 계열의
남은 배너 전부를 잃고, 자리를 비운 사용자에게는 손실 자체가 관측되지 않는다.
회복 경로는 재-ARM이 아니라 모델 응답 텍스트의 한 줄 공개다
- §2.3에 이중 취소 의무 추가, §6.1을 "요청 없음"과 "요청은 있으나 ARM이 답이
아님"으로 분할, §2.9의 키보드 전제 교체, §3 디스패처 목록 보강,
§7의 교차 파일 포인터를 계약 내용 인라인으로 대체
PR #88이 fire-now 인접 호출을 규약으로 못박으면서, 그때까지 도달 불가능하던 경합이 열렸다. cancel과 arm이 fire 분기의 락에 참여하지 않아 두 호출의 쓰기가 fire의 read-modify-write 창 안에 떨어질 수 있다. - `cancel` 직후 플래그가 사라졌다가 진행 중이던 fire가 끝나면 **되살아나고**, 이어진 fire-now가 정상 발화한다. 사용자는 껐다고 믿는 채로 배너를 계속 받는다 - 모드를 좁히는 re-ARM도 같은 방식으로 되돌려져, 사용자가 방금 교체한 넓은 모드가 조용히 복구된다 수정: - `_acquire_lock` 헬퍼로 락 획득을 단일화하되 **예산을 둘로 나눈다**. fire는 기다려도 배너만 늦지만 cancel·arm은 모델의 턴 안에서 인라인 실행되므로, 그만큼의 정지가 fire-first 규약이 막으려는 실패 그 자체다 - fire 예산의 실제 벽시계 시간은 주석이 주장하던 10초가 아니라 **40초대**다 — 반복마다 /bin/sleep을 fork하기 때문에 반복당 비용이 sleep 인자의 4배다. 주석을 실측으로 정정하고, 인라인 예산은 약 1초로 세 자릿수 낮춘다 - cancel·arm은 예산 소진 시 **fail-open**으로 진행한다. 락 없이 진행해 다시 열리는 창은 마이크로초 수준인 반면, 취소를 요청한 사용자에게 배너가 계속 가는 것은 확정적 손해다 - cancel에 사전 fast path — `mkdir "$lockdir"`의 ENOENT는 플래그 **파일**이 아니라 플래그 **디렉토리**가 없을 때 난다(lockdir의 부모가 그것이다). 즉 그 세션에서 한 번도 arm하지 않았거나 TMPDIR이 청소된 경우이며, 지울 것도 없으므로 예산을 통째로 태우는 대신 즉시 반환한다 - EXIT trap을 **락 보유 여부로 게이트** — 무조건 rmdir은 fail-open 경로에서 wedged 보유자의 락을 훔친다 - 픽스처 3종 — 앞의 둘은 sleep 타이밍이 아니라 락으로 순서를 강제해 수정 전 트리에서 red가 되고, 셋째는 fail-open이 남의 락을 훔치지 않음을 고정한다
γ 경로는 `CLAUDE_CODE_SESSION_ID=<값> <원래 명령>`을 문자열 연결로 만들어 `updatedInput.command`로 돌려주는데, 값이 인용되지 않아 세션 ID의 셸 메타문자가 그대로 명령이 된다. 이 경로는 플러그인에서 유일하게 **명령을 재작성하면서 동시에 allow를 반환**하는 자리라, `;` 하나가 notify.sh 호출을 다른 명령으로 바꿔 놓고 그 자동 승인까지 물려받는다. - `@sh`로 주입 값을 인용 — 공백이 든 세션 ID가 대입문과 떠도는 인자로 쪼개지는 것도 함께 막힌다 - 메타문자 픽스처 — 인용된 형태를 요구하고, 사용자의 notify.sh 호출이 여전히 실행 대상의 꼬리로 남아 있는지 확인한다(수정 전 트리에서 red) - 복합 명령 픽스처 — `A && B`가 여전히 매치되지만 환경 대입 접두는 **첫 명령 에만** 바인딩된다는 한계를 고정한다. 이것은 인용으로 해소되지 않으므로, 자기 순서 규약을 가진 §4.6에 "두 호출을 `&&`로 잇지 말 것"을 명시한다 — 이어 붙이면 cancel이 다른 플래그 경로를 보고 아무것도 취소하지 못한다 - 기존 γ 픽스처 3종은 기대 문자열을 하드코딩하지 않고 같은 `@sh`로 도출하도록 수정
시계 기반 경로가 처음으로 동작하게 되고 `fire-oneshot`이라는 공개 서브커맨드가 추가되므로 minor다. 산문 수정을 patch로 처리한 v1.5.1이 아니라 기능 추가를 minor로 처리한 v1.5.0이 맞는 선례다. CHANGELOG의 새 절은 이번 PR과 함께 **v2.0.0에 기재되지 않은 변경**도 담는다 — repeat 모드의 발화 축 전환과 self-cancel 신설이 그 릴리즈 직전에 머지됐으나 절에 오르지 않았다. 소급 기재 대신 전방 기재를 택한 이유는, v2.0.0 시점의 계약을 그때 문서화하면 실제로는 표면 간에 어긋나 있던 상태를 정상으로 기술하게 되기 때문이다.
훅은 매치된 부분 문자열이 아니라 명령줄 **전체**에 allow를 낸다. `notify_re`에
앵커가 없어 어디든 부분 일치하면 참이 되므로, `notify.sh` 호출 뒤에 무엇을
붙이든 함께 승인됐다. 스텁 디스패처와 센티넬을 둔 실행 수준 확인에서 주입
페이로드 5종(`;`·`&&`·`|`·`$( )`·백틱)이 **전부 센티넬을 생성**했다 — 결정
문자열만이 아니라 뒤에 붙은 명령이 실제로 실행된다. matcher가 `Bash`라
폭발 반경은 이 스킬이 아니라 세션 전체다.
**앵커만으로는 닫히지 않는다.** 세 변경은 하나의 수정이며 어느 둘만 출하해도
우회가 남는다.
- **패턴** — 명령줄 전체에 앵커하고 `$`로 인자 꼬리를 종단한다
- **매처** — `grep -qE`를 `[[ =~ ]]`로 교체. `grep`은 행 단위라 `^…$`가 행에
걸리고, 첫 행이 정상 호출인 두 줄 명령이 그대로 통과한다
- **문자 클래스** — 분리자를 `[[:blank:]]`로. `[[:space:]]`는 bash ERE에서
개행에 매치하므로 매처를 바꿔도 두 줄 명령이 뚫린다. 인자 꼬리에서
`[:cntrl:]`을 배제한다
인자 꼬리는 `( ) " ' \ ! # * ~ { }`를 허용한다 — 명령 도입 문자가 배제된 뒤에는
이들이 명령을 만들 수 없고, 배제하면 `fire-now "완료 (12초)"` 같은 정상 요약이
다이얼로그로 떨어져 부재 사용자가 답할 수 없다.
**`bypass_re`는 좁히지 않는다.** 승인해야 할 형태가 `$(uname -s)`·`||`·`{ }`를
포함하는 다행 셸 블록이라 어떤 앵커도 매치할 수 없고, `Makefile`에 같은
정규식 사본이 있어 함께 움직여야 한다. 따라서 이 커밋이 닫는 것은 훅의 두
자동 승인 분기 중 하나뿐이며, 다른 하나는 여전히 명령줄 전체를 승인한다.
폴백 분기는 추가하지 않았다 — 앵커 이후 재작성은 단일 단순 명령만 보므로
도달하지 않는 죽은 경로가 된다.
픽스처:
- `pretool-compound-command` 기대값 반전 + **미체이닝 호출이 여전히 승인되는지
확인하는 대조 단언** — 음성만 있으면 패턴이 아무것도 매치하지 않게 돼도 초록이다
- `pretool-suffix-injection-no-match` 신설 11케이스. **다행 2건이 세 번째 변경을
고정한다** — 메타문자가 없는 두 줄 케이스는 `[[:space:]]` 분리자를 쓰는
정규식에 여전히 매치하므로, 그 케이스를 빼면 틀린 분리자로의 회귀가 초록으로 출하된다
- `pretool-sid-metachar-quoting`을 문자열 비교에서 **실제 실행**으로 승격 —
센티넬을 드라이버의 픽스처별 TMPDIR에 두고 재작성된 줄을 그대로 돌린다
- arm의 플래그 쓰기를 제자리 재기록에서 tmp 스테이징 + mv 원자 교체로 변경. 락을 잡지 못하고 진행하는 경로에서 이 쓰기는 락을 쥔 fire의 판독과 겹치는데, 제자리 재기록은 파일을 수십 마이크로초 동안 비우고 그 순간을 읽은 fire가 플래그를 스테일로 간주해 삭제한다. tmp 변수를 EXIT 트랩 정리 대상에 추가 - 락 예산을 벽시계 목표에서 반복 횟수를 유도하는 형태로 변경. 상수 값은 그대로이며 주석의 수치와 실제 예산이 어긋날 수 없게 된다. fire 예산에는 env 시임을 두어 픽스처가 소진 경로를 실제 벽시계 없이 검증할 수 있게 하고, 인라인 예산에는 두지 않는다 — 그 예산의 상한을 재는 픽스처가 시임으로 무력화되지 않아야 한다 - fire-oneshot의 바이너리 부재 힌트를 ARM 사이클과 분리된 파일로 이동. 예약된 틱이 아무 출력도 남기지 않고 사라지던 경로를 닫는다 - cancel fast path 주석의 판정 조건을 정정. 플래그 디렉토리는 한 TMPDIR을 공유하는 모든 세션의 공용이라, 부재 조건은 이 세션이 아니라 그 TMPDIR 전체에 걸린다 실측: arm 300회 동안 길이 0 관측이 HEAD 1170회에서 0회로. 예산 상수는 기본값에서 바이트 동일(FIRE=10000 / INLINE=200)이고 FIRE 시임만 반응하며 INLINE 시임은 무시된다. **이 수정이 닫는 것은 찢어진 읽기 클래스뿐이다** — 락 보유자가 있는 동안 arm이 fail-open으로 새 사이클을 쓰고 대기하던 fire가 그것을 소비하는 경쟁 상태는 원자 쓰기 후에도 동일하게 재현되며, 별도로 기록해 남긴다.
설계 §H는 "찾은 인용을 어떻게 비교할지"만 규정하고 "무엇을 인용·부정으로 인식할지"를 규정하지 않았다. 설계와 구현이 함께 비워 둔 그 층 때문에 린트는 겨냥한 드리프트 클래스를 표기 한 종으로만 인식했다 — 가설이 아니라 이 레포의 실제 이력이다. 수정 전 `_common/notify.md`의 `SKILL.md` 참조 5건 중 린트가 본 것은 3건뿐이었고, 못 본 2건이 정확히 통과 목록에 해당한다. - **부정문 판정** — `[Nn]o`가 단어 경계 없이 부분 일치해 `now`·`notes`· `nothing`·`notified`·`fire-now`가 전부 예외를 만족시켰다. 하필 이 파일의 중심 용어가 `fire-now`다. 온전한 단어 경계로 좁히고 **비대칭 창**을 둔다 — 영어 부정어는 문구 앞, 한국어 부정 어미는 뒤. 리뷰가 제안한 앞쪽 전용 창은 `…존재하지 않는다`를 오탐한다(실측). 창을 48자로 묶어 무관한 절의 부정이 단언을 허가하지 못하게 한다 - **인용 정규화** — 역순 어순·연결어·`§` 없는 `section N`을 표준형으로 접는다. 추출 정규식은 건드리지 않는다. 규칙 4의 미매핑 파일 스윕도 **같은 인식기**를 쓴다 — 그러지 않으면 테이블 행이 없는 파일이 산문 어순으로 스윕을 회피한다 - **판정 단위를 줄에서 블록으로** — 문단을 다시 줄바꿈하는 것만으로 양방향 결함이 난다. 문구가 줄바꿈에 걸리면 못 보고, 부정어가 앞 줄로 넘어가면 정당한 산문을 막는다. 둘 다 최소 케이스로 재현했다 - **금지 문구를 표기 변형 계열로** — 출하된 두 리터럴은 재서술 10종 중 3종만 잡았다 - **P3 4건** — 앵커 길이 하한(한두 글자는 어떤 제목에나 부분 일치한다), 제목 중복 경고(치명 규칙은 현 트리를 빨갛게 만든다 — 실제 §4.6과 §6.4가 같은 제목이고 그것이 리뷰가 지목한 자리바꿈 쌍이다), 규칙 4 스윕을 루프 밖으로, 선언됐으나 부재한 공유 파일을 무성 skip에서 위반으로. 마지막 것은 종료 코드 우선순위 변경을 강제한다 — `scanned==0`이 이기면 그 위반이 exit 2로 숨는다 - **남는 상한을 헤더에 기록** — 이 검사는 계약을 가리키는 표기를 막지 계약 자체를 막지 않는다. 다른 문장으로 풀어 쓰면 통과한다(`턴이 끝날 때마다` 실측) 픽스처 6 → 19. 검출률 실측 — 인용 1/7 → 7/7, 부정문 2/7 → 7/7, 금지 문구 3/10 → 10/10, 수정 전 트리 적발 9 → 11(증가분 전부 인용 클래스). 단일 지점 뮤테이션 11종을 각각 19종 전부에 돌려 **11/11 사살, 그중 8종은 단독 판별자**다. 두 픽스처는 단독 사살이 없으나 각 규칙의 평탄한 기본 케이스라 유지하고 그 중복을 드라이버 헤더에 숨기지 않고 적는다.
디스패처가 받아들이는 서브커맨드와 그것을 열거하는 표면들이 어긋나도 잡는 것이 없었다. 네 파일 전부 기존 스크립트가 건드리지만 **case 블록을 읽어 열거와 대조하는 스크립트는 하나도 없다** — 특히 SKILL.md §1 코드 블록은 어떤 검사도 보지 않는다. 그 방향이 물리는 방향이다: 훅 정규식에서 이름이 빠지면 호출이 조용히 다이얼로그로 떨어지고, 부재 사용자는 답할 수 없다. **SOT는 실행되는 코드**다 — `notify.sh`의 `case "$subcommand" in` 블록. 문서에서 끌어오면 문서 대 문서 비교가 되어 이 검사가 존재하는 이유인 드리프트를 보지 못한다. case 블록이 읽히지 않게 되면 조용히 아무것도 검사하지 않는 대신 exit 2로 크게 실패한다. 세 규칙 — 커버리지(모든 SOT 이름이 스캔 표면마다 최소 1회) · 열거 정확성 (서브커맨드를 담은 alternation은 SOT 집합과 **디스패처 순서까지** 일치) · 기수(`N subcommand(s)` 주장이 SOT 개수와 일치, 숫자·영어 수사 양쪽). - **추출기는 비탐욕·문자 제한**이다. 훅의 경로 문자 클래스가 리터럴 괄호를 담고 있어 느슨한 추출기는 서브커맨드 이름을 포함한 span을 잡아 영구 오탐이 되고, 그 "해결책"은 훅 정규식을 느슨하게 만드는 것 — 이번 릴리스 보안 수정의 정반대다 - **대문자는 라이프사이클 단계, 소문자는 서브커맨드.** `ARM / FIRE-NOW / CANCEL`로 적는 네 자리는 `fire-oneshot`을 의도적으로 빼는데, 그것은 어느 ARM 사이클에도 속하지 않기 때문이다. 그 자리들이 flag되지 않게 하는 것이 이 구분이다 - **헤더가 하중을 진다** — L0 방벽(규칙 1은 본문으로 충족되며, 위반을 본 유지보수자가 가장 가까운 편집 가능한 자리인 프론트매터에서 해결하는 것을 막는다), 네 번째 규칙(산문 군집 완전성)이 현 트리에서 정상 문장 10건을 오탐해 **만들었다가 버려졌다는 음성 결과**, 그리고 폐쇄 주장 기각의 실측 근거 두 건. 근거 없는 기각 하나를 남기면 재제안하기 쉬운 쪽이 그것이 된다 - `Makefile` **두 줄** — 이 레포는 글롭 없이 한 줄씩 열거하므로 한쪽만 등록하면 그 레그에서 한 번도 실행되지 않는다 실측 — 현 트리 `8 check(s) passed` exit 0 · 이름 추가만 하고 표면 미동기화 → exit 1(7건) · 개명 → exit 1(4건) · 삭제 → exit 1(4건) · **네 표면을 모두 동기화하면 다시 exit 0**이므로 항상 빨간 검사가 아니라 충족 가능한 검사다. 라이프사이클 픽스처 4종(29 → 33) — fire 분기가 fail-**closed**임을 고정하고, 이른 락 해제와 `lock_held=0`의 짝을 고정하며, 인라인 예산이 fire 예산과 분리돼 있음을 고정하고, `arm`의 fail-open이 남의 락을 훔치지 않음을 고정한다. **예산 소진 픽스처의 벽시계 단언이 시임을 고정한다** — 단언이 없으면 시임을 되돌려도 초록으로 남고 단지 800배 느려진다. 실측: 시임 적용 시 스위트 8.9초, 시임을 되돌리면 **정확히 그 픽스처 하나만** red(32 passed / 1 failed).
한 파일 33편집 + §6.1 제자리 재편. **Gate 1 배제절 (P1).** `"각 커밋마다, 각 배포마다 알림 줘"`가 배너 2회 후 조용히 만료됐다. `:356`이 "first match wins"를 선언하고 Gate 1은 "≥2 named sub-events"만 확인하므로 `,`가 연결어 목록에 있는 이상 `single --count=2`로 확정되고 **Gate 2a는 읽히지 않는다.** 같은 파일의 §2.4 tiebreak은 정반대를 지시하는데 두 절 사이 우선순위 규칙은 문서 어디에도 없었다. 이제 Gate 1이 열거 항목을 하나씩 보아 **한 번 지나가면 끝나는 유한한 순간**인지 **반복하는 클래스**인지 묻고, 하나라도 후자면 닫힌 열거가 아니므로 Gate 2a로 내려보낸다. `각`/`각각`의 분배 해석은 모든 항목이 유한한 순간일 때만 성립한다. §2.4의 중복 판정은 삭제하고 §2.5로 위임한다 — 같은 판단을 두 절이 각자 내리던 상태가 사라진다. **다중 클래스는 합집합 라벨 단일 사이클**(`commit or deploy`)로 처리한다. `arm`이 무조건 idempotent overwrite라 클래스별 ARM은 물리적으로 표현 불가이고, 대표 클래스 하나로 좁히는 것은 §6.1 Event-class inflation의 정확한 역방향이다 — 그 안티패턴은 사용자가 준 명사 너머로 클래스를 늘리고, 이쪽은 사용자가 준 명사를 버린다. 혼합 열거의 유한 순간도 같은 라벨에 구성원으로 합류한다. **예약 회수 경로 (P1).** 본문이 조회·삭제 도구를 이름으로 부르지 않아 스키마를 적재할 이름 자체가 없었다(차단이지 저하가 아니다). 이제 `CronList`·`CronDelete`를 이름으로 부른다. 식별 키도 관측된 렌더링에 맞춘다 — 목록은 프롬프트를 잘라 보여 주고 이 절이 예약하는 것은 절대 경로로 쓴 호출이라 `fire-oneshot` 토큰이 절단면 바깥이다. 예약 시각과 일회성·반복 마커를 키로 쓰고, 남은 프롬프트 접두사는 "우리 것인가"에만 답하는 계열 필터로 강등한다. 작업 ID는 잘리지 않고 맨 앞에 오므로 **판별자가 아니라 삭제 핸들**이다. 같은 분에 예약된 일회성 둘은 보이는 범위에서 바이트 동일하므로 **모호하면 묻는다** — 방금 취소를 말한 사용자는 정의상 자리에 있다. **훅 수정이 거짓으로 만든 두 문장.** 파일 서두의 "다이얼로그가 결코 뜨지 않는다"는 절대 주장을 조건부로 좁히고, §4.6의 `&&` 금지는 근거를 다시 댄다 — 앵커 이후 체이닝된 쌍은 재작성에 도달조차 하지 않으므로 세션 ID 바인딩이 더는 그 금지의 기전이 아니다. **개행 형태를 함께 막는다**: 두 줄로 나누면 `&&` 금지는 충족되지만 매처가 명령줄 전체에 앵커돼 있어 똑같이 실패한다. **§6.1 (P1 + P2-11).** `:1143`의 `(A) No alert request was made — ARM, and say nothing.`이 굵은 명령형으로 자기 문단 본문·자기 첫 불릿·상시 적재 표면을 전부 뒤집고 있었다(merge-base에 0건 — 이 PR이 만든 것이다). 그리고 `:1157`에 앞선 빈 줄이 없어 (B) 분기 전체가 (A)의 마지막 불릿 안으로 렌더링됐다. 둘 다 고친다. 아울러 제목이 자기 내용과 반대를 말하던 것도 **제자리에서** 교정한다 — (B)의 세 항목은 ARM을 실제로 수행하며 형태만 제약하는데 "call forbidden" 아래 있었다. **절을 쪼개거나 번호를 다시 매기지 않는다**: 살아 있는 §6.x 교차 참조 30곳이 깨지고 어떤 린트도 그것을 검사하지 않는다. §6 도입부의 §6.1 서술은 제목 교정 이후에 다시 도출했다 — 원안대로 함께 적용했으면 자기 절을 잘못 서술하는 도입부가 됐다. 그 밖에 — ToolSearch를 시계 읽기 앞으로 옮겨 "직후 첫 행동" 요구를 충족 가능하게 하고 스키마 적재 실패에 사다리 단을 준다 · 2분 미만 예외가 `arm`도 `fire-now`도 아닌 `fire-oneshot`을 쓰도록 표면을 준다 · `emit`이 지금 실행하는 명령이 아니라 큐에 담기는 한 줄임을 밝히고 절대 경로 요구를 상속시킨다 · 사다리 첫 단이 묻기 전에 무엇을 못 했는지 먼저 말한다 · 종결 계약을 예외 없는 형태로 · §2.9 무응답 분기 신설 · §4.3 배제 명사 정렬 · §2.7/2.8 결번이 의도된 것임을 개수 없이 한 줄로 · 워크드 예시 넷과 철자 통일.
설계 §A는 복제가 허용될 때도 형태를 "판단을 바꾸는 최소형"으로 못박고 근거·예시·예외는 정본에만 두라고 규정한다. 신설 CFI는 그 반대로 만들어졌다 — 담아야 할 것을 빼고 빼야 할 것을 담았다. 편집 일곱 건: - **CFI-4에서 비용 논증 문단 삭제.** 정본(§4.6)이 같은 것을 온전히 서술한다. 이 삭제가 §6.4의 `stated once` 문장을 **참으로 만들므로** 그 문장은 건드리지 않는다 — 그 문장은 이 PR이 처음부터 거짓으로 쓴 것이었고, 고치는 방법이 문장을 바꾸는 것이 아니라 그것이 부정하던 두 벌 중 하나를 없애는 것이다 - **CFI-4에서 (a) 실패 예시 목록 삭제.** 예시는 §A가 정본 전용으로 지정한 것이고, (a)는 이미 행위자 검사라 워크드 케이스 없이 자기 판정 가능하다 - **CFI-1에서 세 절 삭제** — 인스턴스 정의 문장, 정밀 클래스 예외 서술, 그리고 말미의 합성 실패 절. 셋 다 정본에 존치하며, 특히 마지막은 정본이 같은 합성을 온전히 서술하고 있다 - **CFI-1에 판정 축 문장 삽입** — `who opened the turn`. 이것이 "판단을 바꾸는 최소형"에 정확히 해당하며, 삭제한 서술들이 대신 담고 있던 자리다 - **§2.3 포인터 교체** — 비용이 CFI-4에서 사라졌으므로 그쪽을 함께 가리키던 포인터가 거짓이 된다. 허용 조건과 비용 모두 §4.6 하나를 가리킨다 - **§6.4의 `exact complement` 문장 교체** — 네 불릿 중 둘만 허용 조건의 실패이고 나머지 둘은 범위 제한과 순서 규칙의 실패다. `each bullet is one condition failing`은 4개 중 2개에만 참이었다 수용 검사 — `every remaining banner` 2 → 1, `who opened the turn` 1 → 2. 편집 구간을 약 72열로 리랩했고 그 리랩은 린트 중립이다(해당 규칙은 프론트매터와 공유 문서만 스캔한다).
- **README 반복 알림 종료 서술을 예측 기준에서 행위자 기준으로.** 기존 문안은 "해당 이벤트가 더 나오지 않게 되면 모델이 스스로 종료한다"고 약속했는데, 본문 규약은 모델이 직접 일으키지 않는 이벤트(사용자 본인의 커밋·CI)에 대해서는 self-cancel을 **금지**한다. 사용자 대면 표면이 본문이 금지하는 동작을 약속하고 있었고 어떤 게이트도 그 텍스트를 보지 않는다 - **사용 예시 표의 자동 종료 약속 삭제 + 표준 규칙 명시** — 표의 어떤 행도 자동 종료를 약속하지 않는다. 예시 발화만으로는 그 이벤트를 누가 일으키는지가 정해지지 않으므로 분기 규칙은 산문에 한 번만 두고 표는 결과만 담는다. 다중 클래스 행을 함께 추가한다 - **`_common/notify.md` §4 제목 개명** — 같은 이름의 헤딩이 본문에도 있어 가리키는 대상이 모호했다. `SKILL.md`의 적재 지시 목록을 **같은 커밋에서** 갱신한다(분리하면 존재하지 않는 제목을 가리키는 포인터가 남는다) - **센티넬 3절 정정** — 세 L2 절이 힌트 파일을 단일 공유 파일로 서술하는데 디스패처가 `fire-oneshot`에 자기 센티넬을 줬다. 이것은 디스패처 사실이므로 L2가 정확히 그 자리이며 본문으로 올리지 않는다 - **CHANGELOG 동결 절의 과대 진술 두 구절만 정정.** `폐기된 계약 문구를 금지`는 실제로는 두 표기만 금지했고, `SKILL.md §N 인용을 대조`는 일곱 표기 중 하나만 인식했다 — **그 릴리스 시점에도 거짓이었으므로** 고친다. 반면 `픽스처 6종`과 `9건 적발`은 그 시점에 참이었으므로 **그대로 둔다**. 동결된 릴리스 절은 그 릴리스의 정확한 기록이고, 넓힌 뒤의 수치는 버려지는 것이 아니라 새 릴리스 절로 옮겨 간다
v2.1.0이 세운 계약 표면을 실제 동작과 맞추는 라운드이므로 patch다. 사용자에게 도달 가능한 기능을 더하지 않고, 이미 주장한 것을 실제와 맞춘다. 훅 좁히기가 이 판정의 유일한 쟁점인데, 그것은 **출하된 과잉 권한의 회수**다 — 무앵커 정규식은 master에 있고 이번에 새로 생긴 것은 픽스처뿐이다. 근거 셋: 훅 헤더가 스스로 선언한 동의 범위가 `notify.sh` 호출이었으므로 사용자에게 제공된 능력의 철회가 아니고, 좁히기가 영향을 주는 명령 형태를 규약이 이미 금지하거나 지시하지 않으며(호출은 절대 경로로 하도록 못박혀 있어 상대 경로 앞에 `cd`를 붙이는 형태는 규약이 지시한 적이 없다), 넓은 승인이 사용자가 설치하는 상태에 이미 있다. 뒤집는 조건 하나가 남아 있다 — §2.9 tier 2의 사용자 무응답 분기를 additive로 보면 2.2.0이 방어 가능하며, 그 경우 해당 항목을 `Added`로 옮겨야 한다. 전부 `Fixed` 아래 둔 채 2.2.0으로 올리지는 않는다. 릴리스 노트는 네 의무를 진다 — 훅 승인이 한정됐다고 말하지 않고 주어를 좁힌 분기로 한정할 것, 권한 테스트 경로가 대상이 아니라는 짝 문장을 함께 실을 것, 복합 형태가 이제 다이얼로그를 거친다고 고지할 것, 사용자 대면 변경 셋을 모두 담을 것. 특히 혼합 열거는 다중 클래스 서술이 문언상 배제하므로 별도로 적었다.
훅이 발행하던 세션 지속 승인 규칙은 와일드카드로만 쓸 수 있고, 그 와일드카드의 유일한 가변부가 경로 접두사 전체를 덮는다. 승인된 호출을 덮을 만큼 넓은 규칙은 같은 파일의 경로 내용 검사가 거절하는 명령어까지 함께 덮으므로, 세션의 두 번째 호출부터 그 검사를 무효로 만든다. 훅은 어차피 모든 Bash 호출에 붙어 매번 판정하므로 규칙이 더해 주던 것은 미래의 도달 범위뿐이었다. - notify 레그는 applyPermissionRules를 발행하지 않는다. terminal-notifier 우회 레그는 이미 그 호출의 명령줄 전체를 승인하고 있어 규칙이 새로 더하는 도달 범위가 없으므로 규칙을 유지한다 — 그 레그의 축소는 별건 - 경로 세그먼트를 캡처해 내용을 검사한다. 상대 경로, `..`로 밖으로 나가는 경로, 셸이 실행 시점에 확장하는 글로브는 리터럴로 끝나더라도 설치된 디스패처가 아니다. 부정형 클래스가 [:space:]여야 하는 이유(개행 흡수)를 주석으로 고정 — 분리자의 [:blank:]와 "일관되게" 바꾸면 두 줄 명령의 첫 줄이 승인된 명령어가 된다 - 세션 ID 주입 시 `tostring`을 거친다. 문자열이 아닌 세션 ID는 @sh가 원소별로 인용해 대입문 하나가 대입문 + 명령어 위치의 부유 단어로 갈라져 알림이 유실된다 - 우회 매처를 파이프라인 대신 here-string으로 넘긴다. [[ =~ ]]는 bash ERE 에서 `.`가 개행에 걸려 한 줄의 terminal-notifier와 다른 줄의 -group을 짝지어 문서화된 형태가 아닌 명령줄을 승인한다 픽스처 일곱: 주입 픽스처 헤더를 실제로 고정되는 세 축(분리자/경로 세그먼트/내용)으로 다시 쓰고 다섯 경우 추가, notify 레그 셋은 규칙 부재 단언으로 반전, 우회 레그 픽스처는 규칙 단언을 유지한 채 줄 넘김 음성 사례 추가, 세션 ID 픽스처에 비문자열 사례 추가, SKILL.md의 권한 테스트 블록을 실행 시점에 추출해 검증하는 픽스처 신규. 변이 10건 측정 — 9건이 살아 있는 변이이고 그중 8건이 단독 판별자다. 남은 한 건(here-string↔파이프라인)은 구별 가능한 입력을 만들지 못해 핀되지 않았음을 감추지 않고 기록한다. 파일 머리의 α 경로 설명은 이번 변경으로 거짓이 되므로 같은 커밋에서 정정했다.
락 예산 둘은 비용 상수에서 산술로 파생된다. 상수가 유효한 bash 정수가 아니면 두 파생 모두 `set -e`에서 스크립트를 멈추지 못한 채 실패하고 이름을 빈 값이 아니라 UNSET으로 남긴다. 첫 사용이 `set -u`에 걸리는데 macOS 시스템 bash 3.2.57은 이것을 종료 상태 0으로 보고하므로, 디스패처 전체가 성공을 반환하면서 플래그를 쓰지 않고 아무것도 출력하지 않는다. 사용자는 알림이 걸렸다고 듣고 배너는 오지 않는다. - 첫 사용 전에 두 예산을 검증한다. `:-` 확장이 필수 — 실패한 대입은 이름을 비운 게 아니라 없앤 것이라 맨 확장은 가드가 막으려던 그 `set -u`에 그대로 걸린다. 트리거는 특정 값이 아니라 **유효한 bash 정수 리터럴이 아닌 모든 값**이며(`5.`·빈 값·`4.9`·`5x` 측정, 전부 동일한 서명), `5`→`5.`는 한 글자 추가로 발동한다 - 반대 논거를 남긴다: 이 결함은 사용자 입력에서 도달할 수 없고 소스 편집을 요구하므로 유지보수자 위험이며 통상 낮은 등급이다. 다만 이 상수는 자기를 깨뜨리는 값(`4.9ms`)을 제시하는 주석 블록이 바로 위에서 끝나고, 실패가 조용하다 — 둘이 겹치는 자리라 여기서는 등급을 올렸다. 유지보수자 도달성을 더 무겁게 두는 독자는 같은 사실에서 낮은 등급에 이를 수 있다 - 예약 틱의 미설치 안내에서 센티넬을 없앤다. 틱은 아무도 보지 않는 자리에서 돌고 사전 조건 검사는 큐잉 시점에 끝났으므로 이 줄이 틱이 남길 수 있는 유일한 흔적인데, 어떤 센티넬이든 그것을 지운다 — 사이클의 것은 앞선 호출이 하나 남긴 뒤의 모든 틱을, 자기 것은 첫 틱 이후의 모든 틱을 침묵시킨다 - 취소의 잔여 창 설명과 발사 분기 락의 경합 주체 설명을 실제와 맞춘다. 잔여 창은 마이크로초가 아니라 락 보유자의 다음 쓰기까지이고, 두 세션은 이 락을 두고 경합하지 않는다(락 이름이 세션 ID를 담은 플래그 파일에서 온다). 벽시계 리터럴은 다른 리터럴로 갈아 끼우지 않고 조건부로 적는다 — 반복 횟수는 어디서나 정확하지만 초 단위 목표는 비용 상수 가정에 딸린 값 기존 픽스처의 거짓 헤더를 정정한다. 그 헤더는 원자적 쓰기를 고정한다고 적었지만 두 단언 모두 호출이 반환한 뒤에 돌아 제자리 덮어쓰기와 구별되지 않았고, `.tmp-` 잔여물이 없다는 단언은 그 파일을 애초에 만들지 않는 코드에 대해 무조건 통과했다. 잠금 자세를 도입한 커밋에는 시험 가능한 성질이 넷 있었는데 환경 변수 이음매 하나만 핀되어 있었다. 나머지 셋을 픽스처로 고정한다: - `arm-write-is-atomic` — 쓰기가 진행 중인 동안 다른 프로세스가 플래그를 표집한다. 포크 없는 in-process 표집기여야 창을 볼 수 있고, 한쪽 방향 검출기임을 헤더에 적었다(찢어진 읽기는 증명, 0은 증거) - `arm-cleans-staging-file-on-failure` — `mv`를 실패시켜 EXIT 트랩의 정리 경로를 결정적으로 도달시킨다. 행복 경로는 스스로 치우므로 정상 ARM만으로는 아무것도 측정하지 못한다 - `fire-oneshot-tick-sentinel-is-separate` — 두 센티넬 형태가 서로 다르게 실패하므로 단언도 둘이다 변이 매니페스트와 러너를 드라이버 옆에 둔다. 선언은 개수가 아니라 픽스처별 벡터여야 한다 — 트랩 한 줄을 공유하는 두 변이가 각각 하나씩, 서로 다른 픽스처를 붉히므로 개수 기준 하네스는 둘을 서로로 받아들이고 시험된 적 없는 핀을 보고한다. 적색 집합은 드라이버의 PASS 줄 여집합에서 읽고 드라이버의 실패 집계와 대조한다. `^FAIL:` 접두사 grep은 쓸 수 없다 — 이 스위트의 픽스처 여럿이 같은 접두사로 자기 진단을 찍어, 측정해 보면 집계 21에 추출 22가 나온다. 러너는 잘못된 벡터를 거부하는지 자기 점검할 수 있고(`--self-check`), 한 번도 무엇을 거부해 본 적 없는 하네스는 거부할 수 없는 하네스와 구별되지 않는다. 전체 통과가 변이마다 수명주기 스위트를 한 번씩 다시 돌리므로 `make test`의 선행 조건으로 걸지 않고 별도 타깃으로 둔다.
이 린트에는 같은 형태의 결함이 넷 있었고 전부 **인식기가 보는 것과 소비자가 쓰는 것이 어긋난** 자리다. 인식기가 입력의 일부만 읽고도 성공을 보고하거나, 인식기는 접었는데 소비자는 접지 않아 참인 진술을 위반으로 판정한다. - exit 2를 빈 집합이 아니라 **모양**으로 판정한다. 네 칸 들여쓴 새 arm, 들여쓴 `esac`, 다중 패턴 arm 셋 다 비어 있지 않은 목록을 내므로 빈 집합 검사는 통과하는데, 그중 다중 패턴 arm은 이어지는 규칙들이 그 목록으로 판정을 내려 **표면을 탓하는 메시지**를 낸다 — 그대로 따르면 훅 정규식에서 살아 있는 서브커맨드를 지우게 된다 - 규칙 1을 부분 문자열이 아니라 **온전한 낱말**로 본다. 부분 문자열 검사는 `alarm` 때문에 `arm`이, `cancellation` 때문에 `cancel`이 다뤄진다고 판정한다. 공유 문서는 열거 사이트가 하나도 없어 규칙 1이 보호의 전부이므로 여기서 가장 크게 문다. 경계는 `\b`가 아니라 `[^a-z-]` — `\b`는 BSD·GNU 사이에서 이식성이 없고, 하이픈을 경계로 쳐서 `fire-now`가 `fire-oneshot` 안에서 충족돼 버린다 - 규칙 3이 `grep`에는 대소문자를 접어 넘기면서 매핑에는 접지 않던 것을 맞춘다. 문장 첫머리의 `Four subcommands`는 참인 진술인데 위반으로 잡혔다 - 규칙 2의 추출기는 그대로 두고 **입력**을 정규화한다. 백틱 강조와 `|` 주변 공백은 이 레포 자신의 표기이며, 추출기를 느슨하게 하는 쪽은 훅 정규식을 넓히라는 결론으로 이어져 이번 릴리스의 보안 수정과 정반대가 된다 픽스처 5→12, 드라이버에 exit 2 접두사 규약 추가. 변이 7건을 픽스처별 기대 적색 집합으로 사전 등록해 측정 — 7/7 사살, 6건이 단독 판별, 하나(가드 블록 제거)는 두 프로브 픽스처를 함께 붉힌다. 각 신규 픽스처가 **광고한 이유로** 붉는지 진단 메시지로 확인했다. 드라이버 헤더의 exit 2 설명은 반만 맞았으므로 정정했다 — 파일 부재 절반은 의도적 무보장이고, 케이스 블록 절반은 파일을 지우지 않고도 픽스처로 덮을 수 있으며 조용한 통과가 있던 쪽이 그쪽이다. macOS 레그가 이 린트들을 한 번도 돌리지 않았다. 두 드라이버를 잡에 추가하고 **경로 필터도 함께 고친다** — 필터에 이 린트·드라이버·픽스처를 고르는 항목이 하나도 없어서, 이 커밋 같은 변경은 macOS 실행을 아예 유발하지 않았다. 드라이버만 추가하고 필터를 두면 자기 자신의 편집에 대해 잠든 채로 남는다. GitHub의 필터 문법 자체는 로컬에서 실행할 수 없어 확인하지 못했고, 패턴이 대상 파일명을 고르는지는 파일명 대조로 확인했다. 변이 매니페스트 디렉터리는 필터에 넣지 않았다 — 이 잡의 어느 단계도 그 파일을 읽지 않으므로 고르면 아무것도 사지 못한 채 실행만 늘어난다. 수명주기 픽스처 둘도 이 축의 항목이라 함께 싣는다. 하나는 `set -e` 아래에서 도달할 수 없는 `rc=$?` 때문에 자기 진단을 영영 찍지 못했고, 다른 하나는 예산 시임 값이 상한 밖이라 측정은 옳게 하면서 잡 제한시간 안에 끝나지 않았다. 시임에는 하한도 있어서 — 인라인 예산 아래로 내리면 픽스처는 통과하면서 아무것도 재지 않는다 — 두 경계를 헤더에 함께 적었다.
이 린트가 지키는 것은 두 표면의 계약 문구인데, 인식층이 그 문구를 실제로 쓰이는 형태로 읽지 못했다. 결함 여섯은 서로 다른 규칙에 흩어져 있지만 한 방향으로 모인다 — **가로막는 쪽으로 틀린다.** 잡아야 할 것을 놓치거나, 옳게 쓴 문장을 위반으로 판정한다. - 금지 문구 family가 대소문자 맹목이었다. 문장 첫머리 대문자는 이 재진술이 쓰이는 가장 흔한 형태이고, 같은 맹목이 조건부 규칙까지 조용히 무력화했다. 클래스는 첫 글자만이 아니라 글자마다 편다 — 첫 글자만 접는 형태는 전부 대문자 재진술을 여전히 놓치고, 글자별 클래스는 이 파일이 인용 정규화에서 이미 쓰는 기법이다(대소문자 무시 플래그는 sed에서 GNU 전용) - 후행 부정 창이 한국어 전용이라, 부정어가 구 뒤에 오는 평범한 영어 문장 셋을 위반으로 판정했다. 리터럴을 베끼지 않고 선행 쪽을 참조로 정의해 둘이 갈라질 수 없게 한다 — 갈라지는 것이 이 린트가 존재하는 이유인 결함이다 - 금지 family에 부정 예외가 없었다. 폐기된 계약을 부정하려고 진술하는 것은 이 표면들에 옳게 쓰는 방식이고, family는 리터럴 하나보다 여섯 배 넓다. 예외가 없으면 게이트가 자기가 지키는 텍스트를 깎는다 — 이번 확장이 스스로 만든 결함이다 - 필수 문구 검사만 행 단위·대소문자 의존으로 남아 있었다. 평탄화만으로는 부족하다 — 블록은 공백으로 이어 붙으므로 하이픈에서 줄바꿈된 토큰은 `self- cancel`이 되어 리터럴을 여전히 담지 않는다. 선택적 공백까지 있어야 닫힌다 - 부정 창이 로케일 의존이었다. UTF-8에서는 문자, C/POSIX에서는 바이트로 세어 두 CI 레그가 같은 텍스트를 다르게 판정했고, 움직이는 쪽이 음절당 3 바이트인 한국어 — 이 창이 지키려는 바로 그 텍스트다. 창을 바이트로 못 박고 두 방향의 폭을 언어의 밀도에 맞춰 다르게 준다 - 인용 앵커가 인용을 지나 절 끝까지 삼켰다. 쉼표·엠 대시·백틱·표 구분자에서 끊어 옳게 쓴 다섯 형태를 통과시킨다. 앵커 비교를 접두 비교로 바꾸는 더 깊은 수정은 「일치」의 정의를 바꾸는 별개 변경이라 여기 넣지 않는다 - 부정어의 후행 경계에서 하이픈을 뺀다. `no-op`이 주장을 허가하던 것이 닫히고, 공백으로 떨어진 `no longer`는 그대로 부정어로 남는다 픽스처 19→35. 각 신규 픽스처가 광고한 이유로 붉는지 진단 메시지로 확인했다. 기존 픽스처 하나는 인용을 접힌 어순으로 고쳐 쓴다 — 정본 어순이면 수정 전 추출기로도 만족되므로, 그 픽스처는 스윕의 같은-인식기 요구에 대해 아무것도 고정하지 못하고 있었다. 변이 36건을 매니페스트로 트리에 싣고 러너를 함께 둔다. **목록이 아니라 하네스가 산출물이다** — 문자열 목록만 넘기면 판정이 다음 사람이 만드는 기계에 달리고, 이 목록을 만들게 한 사고가 정확히 그것(스트림을 잘못 읽은 하네스가 「아무것도 고정하지 않는다」를 확신을 갖고 보고)이었다. 36건 전부 사살, 생존 0, 픽스처 35 중 22가 최소 한 변이의 단독 판별자다. 이 수치는 러너로 다시 뽑는 것이지 여기서 옮겨 적을 값이 아니다. 측정 도중 얻은 것 셋을 기록한다. 첫 실행에서 일곱 행이 빈 적색 집합으로 돌아왔고, 대응은 벡터 일곱 개를 고치는 것이 아니라 픽스처 넷을 새로 만들고 둘을 다시 쓰는 것이었다 — 벡터를 관측에서 쓰면 언제나 맞아떨어지고 무엇이든 승인한다. 다시 쓴 둘은 광고한 것과 다른 이유로 통과하고 있었다: 하나는 근접 부정어로 `fire-now`를 썼는데 그것은 후행 경계가 이미 막고 있어 선행 경계에 대해 아무 말도 하지 않았고, 다른 하나는 다섯 형태를 경계 없는 한 줄기에 늘어놓아 추출기가 하나로 합치는 바람에 넷이 시험되지 않았다. 그리고 러너의 앵커 읽기가 명령 치환의 후행 개행 제거 때문에 줄 삭제 변이를 연결 행 안의 빈 줄로 만들어 `bash -n`을 깨뜨렸다 — 같은 결함이 앞서 실은 디스패처 러너에도 잠복해 있어 함께 고쳤다(그쪽 판정은 변하지 않았다).
본문에 남아 있던 결함 다섯은 서로 다른 절에 있지만 한 종류다 — **문장이 자기 문서의 내용에 대해 참이 아니다.** 어떤 것은 스스로 만든 출구 없는 경로를 남기고, 어떤 것은 자기 아래 있는 불릿과 반대되는 말을 하며, 어떤 것은 규칙이 적용되지 않는다는 적극적 근거를 지어낸다. 빠진 규칙은 신중한 모델이 복원할 수 있어도 진술된 전제 너머로는 복원할 수 없다. - 게이트 1의 배제절에 재발 한정사 가드가 없어, 한정사가 하나도 없는 열거 발화가 어느 게이트에도 착지하지 못했다. "커밋할 때랑 배포할 때 알림 줘"는 두 배너를 요청하고 하나를 받는다. 값싼 검사를 앞에 두고, 한정사가 없으면 열거는 기본적으로 유한하다고 정한다 — 이유(하류 게이트가 전부 한정사 기반)를 함께 적어, 나중에 게이트 2a를 넓히는 사람이 이 가드가 그 사실에 의존한다는 것을 알게 한다. 이 수정이 캐스케이드 완결성 문장을 거짓으로 만들므로 그 문장도 같은 커밋에서 고친다 - 게이트 2a의 혼합 열거 절이 자기 사례를 스스로 배제하는 조건 아래 있었다. "시작할 때랑 각 커밋마다"는 클래스를 **하나**만 명명하므로, 가드를 지키는 모델은 그 문단을 건너뛰고 `시작`을 떨어뜨린다. 진입 조건 둘을 모두 적고, 작동하는 예시를 함께 싣는다 — 예시만 넣으면 문단의 가드가 바로 아래 불릿과 모순되고 모델에게 우선순위 규칙 없는 두 텍스트가 남는다 - §6 도입부가 자기 문서 내용에 대해 거짓이었다. 셀 수 있는 형태로 다시 쓴다 (하나·둘·셋이 각 절의 불릿 수와 맞는다). 아홉 줄 아래의 §6.2 첫 줄이 같은 과잉 진술을 담고 있어 함께 고친다 — 태생 거짓 문장을 고치면서 새 모순을 출하하는 것이 이 라운드가 없애려는 실패 방식 그 자체다 - §2.9의 무응답 분기가 「이 사이클은 더 발사할 것이 없다」는 전제를 세우고 맨 self-cancel을 지시했다. 그 전제는 거짓이다 — 바로 아래 항목이 사용자 작업 도구 호출을 담은 턴의 끝을 인스턴스 하한으로 규정하고, 작업이 끝나는 턴이 바로 그런 턴이다. 발사 우선 순서를 세 곳에서 진술하는 규칙과 어긋나던 것을 되돌린다 - §2.6의 예약 프롬프트 제약이 줄바꿈 형태에 침묵했다. 같은 정규식에 대해 §4.6이 이미 하는 말을 옮겨 온다. 이쪽이 더 비싸다 — §4.6의 실패는 사람이 보는 턴에서 나고, 이쪽은 아무도 없는 예약 턴에서 난다 훅 승인 조건 서술은 정규식보다 넓었다. `bash` 접두 하나만 명시하고, 경로가 절대여야 한다는 조건을 함께 적는다 — 두 문장 위의 `절대경로로 호출하라`가 가장 가까운 선행사가 아니라 그 사이 문장이 도입한 세 세그먼트 약칭이 가장 가까우므로, 위로 해소되기를 기대하는 대신 명시한다. 이 절은 훅의 경로 내용 가드가 착지한 트리에서만 참이며 그것이 빠지면 함께 빠져야 한다. 사다리 한 단이 도구를 명명하지 않던 것, 마무리 계약이 「예약에 도달하지 못한 모든 경로」를 전이 속성으로 읽히게 쓴 것, 예약 위임 예시가 스킬 디렉터리 절대경로를 쓰지 않은 것, 락이 닫지 못하는 경주를 정관사로 단정한 것도 함께 고친다. 72열 재감쌈은 편집 뒤에 합성 트리에서 다시 측정했다 — 재감쌈은 편집 아래에서 멱등이 아니고, 이번에도 자기 편집이 만든 과폭 줄 넷을 다시 감싸야 했다. 남는 과폭 한 줄은 쪼갤 수 없는 명령어 리터럴이라 그대로 둔다. 72열은 산문 관례이지 열 상한이 아니며, 0을 맞추려고 명령줄을 쪼개면 손해다.
프론트매터는 항상 적재되고 본문은 그렇지 않으므로, 둘이 다른 답을 내면 읽는 쪽은 갈린 두 답 중 하나를 반드시 고른다 — 프론트매터 자신이 모호성을 회피 사유로 쓰는 것을 금지하기 때문이다. 열거된 하위 이벤트가 둘 이상이면 `single --count=N`으로 보내는 규칙만 있었고, 재발 한정사가 명사·이벤트구를 수식할 때의 분기에는 배제 절도, 합집합 라벨도, 우선순위 규칙도 없었다. 열거와 재발이 섞인 발화에서 프론트매터와 본문이 서로 다른 배너 수를 냈다. 한정사를 최상위 판별자로 올려 열거·재발·혼합을 한 캐스케이드에서 가른다. 없다고 지목됐던 우선순위 규칙은 별도 문장이 아니라 **진입 조건으로 공급**되어 문장 수를 늘리지 않는다. 라벨 규칙은 「한정사가 수식하는 명사, 명명된 것 전부의 합집합으로 한 사이클」로 적어, 클래스만 세지 않고 유한 순간까지 포함한다 — 혼합 발화에서 유한 순간을 떨어뜨리던 것이 이 결함의 사용자 관측 증상이었다. `description`이 예시로 인용하는 발화가 규칙 블록에서 출구를 갖지 못하던 것도 함께 닫는다. 그것은 한 적재 상태 **내부의** 비일관이라 다른 표면에서 수리할 수 없으므로 지정 예비를 썼고, 대가를 기록한다: 이 커밋 뒤 프론트매터에 남은 인가된 기증자는 없고, `when_to_use`에 다음으로 무엇을 더하려면 경고선까지 18 코드포인트가 있다. 측정: 결합 1332 — 경고선 1350 기준 여유 18, 상한 1536 기준 여유 204. 두 기준을 함께 읽는다. 여덟 발화 캐스케이드를 프론트매터 바이트만으로 걷고 본문과 대조해 전 행이 일치함을 확인했다(열거 셋, 배제 발동 둘, 비열거 셋). 본문 커밋과 줄 범위가 겹치지 않는 것도 트리에서 확인했다 — 이 커밋은 5·7·13 줄, 본문 커밋의 최소 줄은 30이다. 공유 파일의 재감쌈 규칙은 이 커밋에 대상이 없다. 규칙은 「편집한 문단만, 주변 줄에 맞춰, 파일을 리플로우하지 말 것」이고 이 커밋은 공유 파일의 산문을 편집하지 않으므로, 그 파일의 문안을 고치는 표면 커밋이 함께 이행한다.
README의 세 진술이 조건 없이 적혀 있어 자기 문서 안에서 반증된다. - Bash 권한 다이얼로그가 뜨지 않는다는 단언은 이번 릴리스가 공시하는 사실과 정면으로 부딪친다 — 이어 붙인 형태나 경로에 공백이 있는 호출은 다이얼로그를 거친다. 공시와 그 부정을 같은 릴리스에 함께 내보내지 않도록 여기서 좁힌다. 경로 공백 형태를 함께 적은 것은 그것만이 **모든** 호출을 다이얼로그로 보내기 때문이며, 이 문단이 향하는 독자가 그 자리에 없는 사용자다 - 표 전체에 대한 자동 종료 전칭은 자기 시계 행 둘이 반증한다. 모델 판단으로 발송되는 행으로 한정하고, 시계 행은 삭제·만료를 다른 행위자가 처리한다는 이유와 함께 범위 밖임을 적는다. 시계 행 자체는 정확하므로 건드리지 않는다 - 두 이벤트를 한 사이클로 묶는 행만 중단 방법을 적지 않고 있었다. 상한 없는 사이클을 서술하는 유일한 행이 멈추는 법을 말하지 않던 것이라, 표의 다른 두 반복 행이 이미 쓰는 관례를 그대로 따른다 공유 절차 문서의 센티넬 서술 네 자리를 전부 트리와 맞춘다. 이 라운드의 계획은 네 번째 자리만 「fire-oneshot은 자기 센티넬을 쓴다」로 고치도록 되어 있었으나, 앞 커밋이 예약 틱 경로의 센티넬을 없앴으므로 그 문안은 쓰는 순간 거짓이 된다. 나머지 세 자리도 같은 이유로 이미 거짓이 되어 있었다 — 하나만 고치면 아는 채로 미완 스윕을 내보내는 것이고, 처방을 그대로 따랐다면 네 번째 거짓 진술을 새로 실었을 것이다. 지금 참인 것은: 사이클 경로의 힌트는 TMPDIR 수명당 한 번 센티넬로 묶이고, 예약 틱은 센티넬 없이 매번 찍는다 — 그 줄이 아무도 보지 않는 틱이 남길 수 있는 유일한 흔적이기 때문이다. 편집한 문단만 주변 줄에 맞춰 다시 감쌌고 파일을 리플로우하지 않았다. 새로 추가한 공유 문서 줄 중 폭을 넘는 것은 없으며, 드리프트 린트로 확인했다.
릴리스 노트의 공시 의무는 목록이 아니라 술어다 — 이행 여부는 항목을 몇 개 적었는가가 아니라 **분류표의 반영 항목 중 사용자가 관측할 수 있는 것이 빠짐없이 적혔는가**로 판정한다. 앞선 절이 항목 셋을 열거하고 넷째가 그대로 출하된 것이 이 의무가 없어서 생긴 결과였다. 훅 항목을 `Security`로 옮긴다. 이 릴리스에서 플러그인이 설치된 모든 세션의 모든 Bash 호출에 대한 권한 표면을 바꾸는 유일한 항목인데, 산문 제목과 린트 정규식에 관한 아홉 불릿 사이에 섞여 있었고 파일이 준수를 선언한 형식은 그 범주를 취약점에 배정한다. 그 자리에 세 항목이 함께 들어간다 — 명령줄 전체 승인, 세션 지속 승인 규칙 발행, 승인 대상 경로의 형태 검사. **앞 절의 짝 문장이 말하지 않은 사실을 이번에 적는다** — 다른 자동 승인 분기는 **여전히 명령줄 전체를 승인한다**. 그 문장은 무엇이 이번 변경 대상이 아닌지만 말했고, 무엇이 대상이 아니라는 진술에서 독자는 남은 것이 무엇인지 도출할 수 없다. 훅이 플러그인 설치만으로 모든 Bash 호출에 붙는다는 문장은 이미 있었으나 과거형이라 끝난 일처럼 읽혔으므로 현재형으로 고친다. 세션 지속 규칙 항목은 **훅 경계에서 측정된 것만** 진술한다 — 발행이 있었고, 그 패턴이 매처와 양방향으로 어긋났으며, 발행이 없어졌고, 그 호출의 승인 자체는 그대로다. 권한 엔진이 그 규칙을 존중했는지에 대해서는 아무 주장도 하지 않는다. 이번 라운드의 어떤 에이전트도 그 엔진을 구동할 수 없었다. 나머지 사용자 관측 가능한 항목도 함께 적는다 — 한정사 없는 열거가 어느 분기에도 닿지 못하던 것, 디스패처가 성공을 반환하며 아무것도 하지 않던 경로, 문자열 아닌 세션 ID가 알림을 유실시키던 것, 답 없는 명확화 질문 뒤 마지막 배너를 잃던 절차, 예약 프롬프트의 줄바꿈 형태, 예약 틱의 미설치 안내 빈도, 사용자 안내의 조건 없는 진술 셋. 수치 두 곳을 고친다. 인용 표기 확장의 9→11이 금지 표현 확장 바로 옆에 놓여 후자에 귀속돼 읽히던 것을 재배열하고, 늘어난 2건이 인용 검사의 것임을 적는다 (금지 확장의 기여를 0이라고 적지는 않는다 — 재측정하지 않은 부정 주장이다). 픽스처 열아홉 전부가 단독으로 붉어진다는 주장은 약화가 아니라 제거한다 — 동결된 절은 스무 번째 픽스처가 추가된 뒤에도 그 성질을 계속 주장하고, 릴리스 노트는 독자가 재도출할 수 없는 자리다. 대신 판별력이 어디에 실측으로 기록돼 있는지를 가리킨다. 픽스처 수는 이 커밋을 쓰는 시점에 트리에서 셌다. 버전 정수는 올리지 않는다. 덕분에 앞 절의 문장을 제자리에서 고칠 수 있고, 그것이 짝 문장 결함을 온전히 닫는 유일한 경로다 — 새 절은 덧붙일 수만 있고 위 절 안의 문장을 되돌리지 못한다.
두 변이 하네스는 자기 스크립트 위치에서 해소한 레포 파일을 **제자리 변이**하고 시작 시점 스냅숏으로 되돌렸다. 이것은 아무도 그 파일을 건드리지 않는 동안에는 올바르게 도는 것과 구별되지 않는다. 하네스가 도는 중에 누군가 그 파일을 편집하면 복원이 그 편집을 조용히 버리고, `git status`는 깨끗하게 돌아오며, 하네스는 전체 green을 보고한다. 실측 — 마커를 t+25초에 넣으면 소멸하고, 종료 시 부재하며, 이후 작업 트리가 깨끗하고, 하네스는 통과를 보고했다. - 두 드라이버에 **입력** 이음매를 낸다. 라이프사이클은 시험할 디스패처를, 드리프트는 시험할 린트를 받는다. 기존 변수를 재사용할 수 없는 이유는 두 드라이버에 대해 **서로 다르다** — 라이프사이클 쪽 변수는 드라이버가 매 실행 덮어써 픽스처로 내보내는 **출력**이라 밖에서 설정해도 아무것도 바뀌지 않고, 드리프트 쪽 변수는 린트가 **스캔할 표면**을 가리키므로 픽스처 트리는 옮겨도 시험 대상 스크립트는 옮기지 못한다. 두 이유를 하나로 합쳐 적으면 한쪽이 다른 쪽의 근거로 쓰이므로 각각 재도출해 주석에 적었다 - 추적 파일은 **한 번도 쓰이지 않는다**. 불변 스냅숏 하나를 뜨고(`chmod a-w` — 「불변」을 주석이 아니라 성질로 만든다), 변이는 별도 사본에만 적용하며, 드라이버를 그 사본으로 향하게 한다 - 시작·종료에 추적 파일의 sha256을 단언한다. 제자리 변이로 조용히 되돌아간 하네스는 모든 벡터를 통과시키면서 동시 편집을 파괴하는데, 이 검사만이 둘을 가른다 수정 후 같은 실측: 마커가 **생존**하고, `git status`에 편집이 **남으며**, 하네스는 통과를 보고하는 대신 해시 불일치로 **시끄럽게 실패**한다(rc=2). 두 드라이버 모두 동일하다. 이음매가 장식이 아니라 하중을 지는지도 확인했다 — 사살이 선언된 변이를 적용한 사본으로 향하게 하면 라이프사이클은 36/1로 선언된 그 픽스처만 붉어지고 드리프트는 34/1이 되며, 이음매를 풀면 각각 37/0·35/0으로 돌아온다. 첫 시도에 고른 값은 가드를 **깨는** 대신 **행사하는** 것이라 양쪽 다 green이었고, 그 음성 결과는 계측기가 기지 양성을 낸 뒤에야 채택할 수 있으므로 사살이 선언된 변이로 다시 쟀다.
앞선 규범이 실패한 것은 규칙이 어겨져서가 아니라 **정본 거처가 무시되는 디렉터리에 있어 지킬 수가 없었기 때문**이다 — 규정대로 무내용 인용을 했다면 죽은 링크가 됐을 것이다. 그래서 이 커밋이 세우는 거처는 **추적된다는 것 자체가 조항의 일부**다. 거처가 하나여야 하는 이유는 중복 제거가 아니라 **새 조항이 어디로 갈지를 정하기 위해서**다. 거처가 여럿이면 조항은 저자가 마침 열어 둔 파일에 떨어진다. 이 레포에서 실측된 발산이 정확히 그 형태였고, 코퍼스 매니페스트가 더 필요해지는 지금 사본을 그만큼 더 만들지 않으려면 지금이 그 자리를 정할 때다. - 조항 넷·문법 검사의 양상·읽기 규칙·잡히지 않는 양쪽 퇴화 끝의 공시를 한 파일에 둔다. 두 하네스와 코퍼스 매니페스트는 그것을 **인용**하고 다시 쓰지 않는다 - 행 디렉터리 스키마 검사를 호출 가능한 형태로 같은 자리에 둔다. 두 하네스가 **호출**하며, 스키마 위반은 핀 실패와 구별되도록 **구성 오류**로 끝난다 — 둘을 한 종료 코드로 합치면 「이 코퍼스가 기형이다」와 「이 변이가 빠져나갔다」를 가를 수 없다 - 코퍼스 매니페스트는 자기 고유의 것(공시·측정·행 목록)만 갖는다 거처를 정하는 것이 거짓을 닫지는 않는다. 문제의 사본들은 서로에 대해 발산하지 않았고 그래서 불일치를 찾는 어떤 검사도 발화하지 않았다 — 거처가 하나였어도 거짓 문장 하나를 한 곳에 보존했을 뿐이다. 그 정정은 별도로 나른다. 스키마 검사가 장식이 아님을 확인했다 — 앵커 누락·선언 파일 누락·주석만 남은 선언 셋이 각각 구성 오류로 끝나고 온전한 행은 통과하며, 코퍼스에 기형 행 하나를 넣으면 전체 실행이 첫 행 전에 중단된다. **관측된 플레이크 하나를 기록한다.** 라이프사이클 하네스가 한 번 6/1을 냈고 이어진 두 실행과 부하를 건 드라이버 세 실행이 전부 깨끗했다. **어느 행이었는지는 그 실행에서 확정하지 못했다** — 요약이 개수만 적고 이름을 적지 않았기 때문이다. 그래서 두 하네스의 요약이 이제 실패한 행 이름을 함께 출력한다. 원인을 세우지 못한 음성 결과를 「없음」으로 접수하지 않으려면 다음 재발이 스스로 귀속돼야 한다.
**통제군이 무력했다.** 통제 벡터가 실재하지 않는 픽스처 이름이라, 관측된 적색 집합은 **구성상** 실재 픽스처의 부분집합이고 따라서 어떤 비교 연산자로도 거절된다. 판별력이 1비트이며 그 1비트는 코퍼스가 실제로 틀릴 수 있는 양식을 하나도 덮지 않는다. 실측 — 등식 검사를 상위집합 검사로 바꿔 30개를 더 붉히는 변이까지 깨끗한 사살로 인증하는 하네스가 되어도, 자기 점검 통과를 포함해 완전 green이었다. - **표현 가능한 통제군 둘** — 실재 픽스처를 하나 더한 **과대 선언**과 한 원소를 뺀 **과소 선언**. 행은 위치가 아니라 **적색 집합 크기**로 고른다: 과소 선언은 단원소 행에서 표현 불가하므로, 첫 행이 단원소인 코퍼스에서는 그 절반이 조용히 사라진다 - **거절이 어디서 나왔는지를 단언한다** — 벡터 비교 거절에 전용 종료 코드를 주어 앵커·적용 실패와 가른다. 「0이 아니면 통과」인 통제군은 무관한 이유로 거절돼도 통과하며, 그것이 통제군이 시험한 적 없는 비교를 인증하게 되는 경로다 - **사전 측정 블록** — 벡터가 어느 픽스처 집합에 대해 도출됐는지를 **내용 해시**로 고정하고 첫 행 전에 대조한다. 리비전이 아닌 이유는 이 하네스를 다시 돌리라고 문서화된 발동 조건이 「대상이 바뀌면」, 곧 더러운 트리인데 **리비전 검사는 표를 무효화하는 바로 그 경우에 통과**하기 때문이다. 이름 목록만으로도 부족하다 — 픽스처의 **내용**만 고치면 이름 해시는 그대로인데 한 행의 벡터가 무너진다 - **단독 사살 계수기가 비교기와 같은 파스를 재사용한다.** 계수기가 원시 줄을 세는 동안 비교기는 주석을 걷어내고 있었으므로, **문서화된 형식 기능인 주석 한 줄**이 그 행을 공표 수치에서 조용히 떨어뜨리면서 행도 실행도 통과했다 기지 양성으로 각각 확인했다. 스키마 위반 셋은 구성 오류로 끝나고, 픽스처 **내용**만 바꾼 편집은 첫 행 전에 중단시키며(실행된 행 0), 통제군 둘은 벡터 비교 코드로 거절되고, 주석 한 줄은 공표 수치를 **옮기지 않는다** — 같은 주석이 옛 규칙에서는 22를 21로 떨어뜨린다. **이 커밋을 쓰는 도중 같은 결함을 한 번 재생산했다.** 계수기 패치의 형식 문자열 이스케이프가 어긋나 모든 행이 단원소로 세어졌고 공표 수치가 22에서 29로 **올랐다**. 독립 재계산이 22를 돌려주지 않았다면 그 29가 그대로 나갔을 것이다. 수치가 좋아지는 방향으로 움직였고 어떤 검사도 붉어지지 않았다는 점이 이 클래스의 전부다.
아무것도 붉히지 않는 행을 어떻게 선언할 것인가에서 나온 결론은 **막지 말고 표현 불가능하게 만들라**는 것이다. 벡터 파일 안의 센티넬 값은 픽스처 이름과 **같은 네임스페이스**에 새 값을 넣으므로 모든 판독기가 특수 처리해야 하고, 잊은 판독기는 그것을 **이름으로 읽는다** — 이 트리에 이미 살아 있는 결함을 의도적으로 재현하는 것이다. - 각 행은 기대 적색 파일 **또는** 무-적색 파일 중 **정확히 하나**를 갖는다. 오타가 벡터 파일 안에 만들어 낼 수 있는 어떤 문자열도 「아무것도 붉히지 않음」을 뜻하지 못한다 — 두 주장이 같은 파일에서 표현 불가능하다 - 스키마 단언은 **어느 파일이 존재하는가에 대한 3상 검사**가 되고, 실패 분기 셋은 전부 **구성 오류**로 끝난다(둘 다 있음·둘 다 없음·무-적색 파일이 빔). **이것은 기존 하드 에러의 보존이 아니라 동작 변경이다** — 기존 경로는 선언이 비었거나 없으면 그 행을 실패로 세어 1로 끝났고, 그 붕괴를 물려주면 「이 코퍼스가 기형이다」와 「변이가 빠져나갔다」를 종료 코드로 가를 수 없다 - 무-적색 파일이 비어 있으면 거절하는 이유는, 비면 「아무것도 선언하지 않음」이 「아무것도 붉히지 않는다고 선언함」으로 도로 무너져 이 정책이 만들려는 구별 자체가 사라지기 때문이다. 그 파일은 측정과 근거를 담는다 - **퇴화 선언은 반증 가능한 채로 유지된다** — 그런 행도 적용·실행되며 관측된 적색 집합이 비어 있어야 한다. 아니면 실패하는 행을 침묵시키는 수단이 된다 - **퇴화 행과 전 픽스처 적색 행은 단독 사살 수치에서 제외한다.** 아무것도 판별하지 못하는 행이 공표 커버리지에 들어가는 것이 핀 없는 계수가 조용히 좋아지는 결과다 기지 양성으로 확인했다 — 실제로 아무것도 붉히지 않는 행 하나를 만들어 돌리면 적용·실행되고 빈 집합으로 통과하며 **공표 수치를 올리지 않고**, 세 실패 분기는 각각 구성 오류로 끝난다(종료 코드 2, 셋 다 확인). 거짓 퇴화 선언은 기대 집합이 공집합인 채로 같은 등식 비교를 거치므로 비지 않은 관측과 같을 수 없다. 이 정책은 내용 해시에 **일방향으로** 의존한다 — 해시 없이는 출하하지 않으나, 해시만 있고 퇴화 선언이 없는 코퍼스는 그대로 출하 가능하며 퇴화 행을 표현하지 못할 뿐이다.
**기전 서술이 실제보다 넓었다.** 비정수 비용 상수가 조용한 성공으로 바뀌려면 조건 **셋**이 함께 성립해야 하는데 두 표면이 둘만 적고 있었다. 셋째가 폭발 반경을 한정하므로, 덜 적으면 존재하지 않는 넓이의 실패를 서술하게 된다. 재도출한 셋 — (1) 산술 실패가 두 이름을 UNSET으로 남기고 스크립트를 멈추지 않는다, (2) 첫 **사용**이 `set -u`에서 중단된다(`set -e`가 아니다), (3) 그때 이미 걸려 있는 EXIT 트랩의 꼬리 `|| :`가 상태를 공급한다. 실측: 가드를 빼고 트랩을 두면 bash 3.2.57은 플래그 없이 exit 0, 5.3.15는 exit 1. **가드와 트랩을 둘 다 빼면 양쪽 다 exit 1** — 트랩이 하중을 진다. 편집 가능한 두 표면(디스패처 주석·픽스처 헤더)을 고쳤고, **셋째 표면인 커밋 본문은 고치지 않는다** — 그 커밋만 단독으로 여는 사람은 여전히 틀린 기전을 읽으며, 이것은 이 수정이 닫지 못하는 것이다. **잔여도 함께 적는다** — 이 가드는 트랩이 이후 중단을 계속 가리는 것을 닫지 않는다. 트랩이 걸린 뒤 발생하는 어떤 `set -u` 중단도 그 셸에서는 여전히 exit 0으로 나온다. **가드가 조용히 고치던 것을 말하게 한다.** 값을 소리 없이 복구하면 깨진 상수가 그대로 남고 이후 모든 실행이 정상으로 보이므로, 고칠 수 있는 유일한 사람이 고칠 것이 있다는 사실을 영영 모른다. **예산 시임을 값이 아니라 측정으로 바꾼다.** 기존 고정값이 정당화된 위험은 존재하지 않았다 — 인라인 예산 아래로 내리면 픽스처가 아무것도 재지 않으면서 통과하고, 훨씬 위로 올리면 옳게 재면서 잡 안에서 끝나지 않는다. 두 경계가 호스트의 반복 비용을 따라 움직이므로 그 비용을 재서 시임과 경계를 함께 도출한다. 셸 내장만 쓴다 — macOS 시스템 bash에 `EPOCHREALTIME`이 없어 시계는 `SECONDS`이고 해상도가 1초라, 보정 루프는 나눌 수 있을 만큼 커질 때까지 **표본을 늘린다**(해상도 하한은 오판이 아니라 표본 증가로 이어진다). 호스트가 표본 상한마저 앞지르면 조용히 반올림한 0으로 나누는 대신 **시끄럽게 중단**한다. 보정 픽스처가 판별하는지 확인했다 — 정상 트리에서 통과(스위트 37/0, 약 21초)하고 예산 통합 회귀에서 붉어진다. 코퍼스 두 행. 하나는 진단 제거를 고정한다. 다른 하나는 **퇴화 행**이며 이 정책의 첫 소비자다 — 기전 서술은 산문이고 산문은 픽스처가 고정하지 못하므로, 적지 않으면 「이 수정에는 행이 없다」와 「이 수정은 고정할 수 없다」가 구별되지 않는다. 그 파일에 함께 기록한 실측: 시임을 고정값으로 되돌리는 변이도 이 호스트에서 아무것도 붉히지 않으며, 시임이 픽스처 안에 있어 디스패처를 대상으로 하는 행으로는 표현되지도 않는다. **둘 중 하나만 착지한다면 행이 아니라 보정 픽스처를 착지시킨다.** 픽스처 둘의 내용이 바뀌었으므로 사전 측정 해시가 첫 행 전에 중단시켰고, 벡터를 관측에 맞추는 대신 **재도출**했다. 기존 한 행의 앵커가 가드 편집으로 더는 유일하지 않게 되어 현재 대상에 대해 다시 앵커링했다 — 그 행과 새 진단 행은 이제 같은 두 줄을 공유하며, 둘을 가르는 것은 각자의 선언된 벡터다.
**이 규칙은 자기가 보호하는 계약을 어느 언어로도 0% 덮고 있었다.** 실제 파일 쌍의 사본에 세 반전을 걸면 전부 `exit 0`이고, 단독 단언 통제군은 `exit 1`이다 — 검사 자체는 건전한데 계약에 닿지 못했다. **원인이 하나가 아니라 둘이다.** - **추적 목록에 없는 철자.** 이 표면들은 계약을 한국어로도 진술하는데 그 철자가 조건부 문구에도 금지 정규식에도 없었다. **어떤 창 설정으로도 닿을 수 없는 구조적 공백**이며 튜닝 문제가 아니다. 문구를 이중 언어 계열로 넓히고, 표면마다 다른 띄어쓰기·하이픈 변형까지 받도록 철자 하나가 아니라 형태로 매치한다 - **창 포화.** 부정어가 **이웃 절**에 있어도 단언을 허가했다. 이 표면들이 계약을 「... turn-end auto-fire, and the dispatcher owns no timer」로 적으므로, 절 경계를 쓰지 않으면 옆 절의 부정어가 사정거리 안에 들어온다 **절 경계는 창을 대체하는 것이 아니라 교집합이다.** 창을 절 경계로 갈아 끼우면 긴 단일 절 블록이 깨진다 — 그 안의 모든 것이 서로의 사정거리에 남고, 그것이 이 규칙이 막으려는 포화다. 창만 두면 옆 문장의 부정어가 허가한다. **둘 중 작은 쪽을 취하는 것이 두 방향을 한 번에 닫는다.** 경계는 문장이 아니라 **절** 단위이며 쉼표가 결정적이다. 수정 후 같은 실측: 네 반전(한국어 프론트매터·영어 공유 문서 둘·한국어 띄어쓰기 변형)이 전부 포착되고, 옳게 쓴 부정 둘은 초록으로 남으며, 출하 트리 초록, 픽스처 39/39 초록. **그리고 이 수정이 무엇을 닫지 않는지를 헤더에 자기완결적으로 적는다.** 프론트매터가 부정으로 진술하는 계약은 **여섯**이고 이 검사가 덮는 것은 **넷째 하나**다. 여섯을 헤더 안에 열거해 그 수치가 헤더와 함께 이동하게 했다 — 열거가 없으면 린트 헤더로 들어간 「여섯」에 지시 대상이 없어진다. 코퍼스 네 행을 더하고 아홉 행을 **재도출**했다. 픽스처 셋이 늘었으므로 사전 측정 해시가 첫 행 전에 중단시켰고, 벡터를 관측에 맞추는 대신 성질에서 다시 도출했다: - 새 픽스처가 정당하게 합류한 다섯 행(한국어 부정 픽스처가 그 성질에 걸린다) - 창 변이 두 행은 **사정거리가 줄었다** — 절 경계가 먼저 자르므로 절을 가로지르는 창 변이는 더 이상 감지되지 않는다. 그중 선행 창 쪽은 그대로 두면 퇴화 행이 됐을 것이라, **한 절 안에서** 창이 하중을 지는 픽스처를 새로 넣어 다시 판별하게 했다 - 줄 이어붙이기 행은 조건부 계열이 하이픈·공백 변형을 받게 되면서 조건부 픽스처를 더는 초록으로 만들지 못한다 — 인식이 넓어진 결과다 - 한 행은 앵커가 재작성된 호출부와 맞지 않아 현재 대상에 다시 앵커링했다 **예측 하나가 반증됐고 그것을 기록한다.** 새 `FAIL-*` 픽스처가 인용 규칙 변이로 함께 붉을 것이라 예측했으나 아니었다 — 그 픽스처들은 이미 exit 1을 기대하므로 위반이 하나 더 생겨도 판정이 바뀌지 않는다. 뒤집히는 것은 `OK-*`뿐이다. **한 픽스처가 내 패치의 버그를 잡았다.** 절 경계 절단이 종결자가 없을 때 문자열 전체를 버려, 옳게 부정된 단일 문장 블록을 위반으로 만들었다. 스위트가 그 자리에서 붉어졌다. 드라이버 헤더에서 라이선싱 산문을 정정한다 — 그 파일은 단독 사살 수치를 **계산하지 않으며**, 계산하지 않는 양에 대한 유지 규칙을 그 헤더가 저술하고 있었다. 규칙의 최종 거처는 정해지지 않았고, 근거가 여기 없다는 사실을 적는 것이 그 규칙이 이 헤더에서 다시 도출되는 것을 막는다.
**허용목록 추출기는 자기가 아는 철자만 읽고 나머지에 대해 눈이 먼다.** 다른 방식으로 적힌 arm은 조용히 빠지고, 디스패처가 실제보다 적은 서브커맨드를 선언한 것으로 판정되며, 규칙들이 그 차이를 **표면 탓으로 돌린다.** 실측 — 따옴표를 두른 arm과 글로브 문자로 시작하는 arm이 각각 「3개 선언」으로 통과하고 「서브커맨드가 표면에 없다」를 보고했다. **그 메시지를 문자 그대로 따르면 훅 정규식에서 살아 있는 서브커맨드를 지우게 된다** — 린트가 보안 회귀를 설득하는 형태다. - **깊이 인식 위치 기반 추출기**로 바꾼다. arm 자리에 있는 것은 철자와 무관하게 arm 으로 읽고, 평범한 소문자 이름이면 서브커맨드, `*`면 포괄 분기, 그 밖이면 **보고** 한다. 기본값이 뒤집혔다 — 인식 못 한 것은 보이지 않는 것이 아니라 시끄러운 것이다. 중첩 `case`는 깊이로 갈라지므로 철자 예외가 필요 없다 - `|`를 일부러 제외하지 않는다. 다중 패턴 arm이야말로 **보이고 나서 보고돼야 하는** 형태이며, 제외하면 그 arm이 다시 사각지대로 들어간다 - 실트리 출력이 **바이트 동일**하고 픽스처 15/15 초록이다 **오탐 둘을 제거하고 그 자리에 픽스처를 넣었다.** 4칸 들여쓴 arm은 arm이므로 읽어야 하고(그러면 표면이 실제로 낡은 것이니 exit 1이 옳은 답이다), 들여쓴 `esac`는 bash가 받으므로 읽어야 한다. 두 픽스처가 옛 기대값을 담고 있었으므로 **현재 동작에 맞춰 재도출**하고, 각 픽스처의 원래 취지(분류 불가한 arm / 정말로 닫히지 않은 블록)는 다른 형태로 보존했다. **규칙 1의 경계 이유를 교체한다 — 적힌 두 이유가 둘 다 거짓이다.** 실측: `\b`는 이 레포 CI가 쓰는 BSD·GNU 양쪽 grep이 지원하고, `\b`는 `fire-oneshot` 안에서 `fire-now`에 매치하지 않는다. **참인 이유는 반대 방향이다** — `\b`는 하이픈을 경계로 치므로 긴 하이픈 낱말의 **후행 마디**로 들어간 서브커맨드가 경계를 만족한다. `self-cancel`과 `post-cancel`이 그 형태이고 `self-cancel`은 이 표면들에 **살아 있다.** **접두 형태가 아니다** — `cancel-worker`는 양쪽 다 걸리므로, 접두를 찾으러 간 편집자는 아무것도 못 찾고 이유가 틀렸다고 판단해 되돌린다. 이유 없는 경계 선택은 되돌림을 부르고 틀린 이유는 그것을 더 빨리 부른다. 후행 마디가 커버리지로 세어지면 붉어지는 픽스처를 함께 넣었다. 규칙 1 헤더의 반대 진술도 고친다 — 헤더는 「파일 어딘가에 소문자형이 있어야 한다」고 적었으나 검사는 대소문자를 접으며, 대문자만 있는 표면이 통과하는 픽스처가 이 스위트에 있다. 여덟 검사 분해에는 그것을 유지시키는 것이 없다는 사실을 적었다. **변이 코퍼스를 신설한다.** 이 린트의 드라이버에는 이름 필터가 없어 매니페스트를 픽스처 루트에 두면 픽스처로 읽히므로 **형제 트리**에 둔다. 여섯 행 중 넷이 판별하고 **둘은 퇴화 행**이다 — 정규화의 두 결정(별표를 벗기는 것과 밑줄은 일부러 벗기지 않는 것)은 어느 픽스처도 고정하지 않으며 되돌려도 스위트가 전부 통과한다. 선언 전에 측정했고 둘 다 0 red였다. **예측 하나가 반증돼 재도출했다.** 추출기 되돌림이 픽스처 셋을 붉힐 것이라 예측했으나 하나였다 — 4칸 arm 픽스처는 자기 사용법 문자열의 대체 표기가 별개 이유로 규칙 2에 걸려 같은 종료 코드를 내고, 들여쓴 esac 픽스처는 이 변이가 깊이 추적을 건드리지 않아 영향받지 않는다. **통제군 (b)가 이 코퍼스에서 표현 불가함을 하네스가 말한다** — 두 픽스처 이상을 선언하는 행이 없기 때문이며, 조용히 건너뛰면 돌지 않은 통제군이 돈 것처럼 읽힌다. macOS 잡의 경로 필터에서 그 잡의 어느 단계도 실행하지 않는 스크립트를 고르던 글로브를 좁힌다 — 읽지도 않는 파일 때문에 잡을 깨우는 필터는 CI를 쓰면서 그 잡이 덮지 않는 것을 덮는다고 가르친다.
훅의 α 경로 주석은 매처가 받는 명령 집합과 발행되는 권한 규칙이 덮는 집합의 관계를 확정된 어조로 적고 있었다. 그 관계는 어느 방향으로도 측정된 적이 없다. 혐의는 「거짓」이 아니라 「입증되지 않았다」이며, 후자는 나중의 측정으로 무를 수 없다 — 근거 없이 확신에 찬 문장의 기능은 다음 감사자에게 여기 볼 것이 없다고 말하는 것이기 때문이다. - 근거 주석을 넷으로 재작성: 무엇이 측정되었는가(훅 경계에서 매 호출을 다시 판정한다) / 무엇이 측정되지 않았는가(두 집합의 포함 관계는 어느 방향도) / 무엇이 결판을 내는가(권한 엔진을 실제로 돌려 두 집합을 비교하는 것뿐) / 상시 울타리(이 파일에 그 관계를 적지 말 것). 결판 관측을 이름으로 적어 두어 다음 사람이 「측정하면 된다」가 아니라 무엇을 측정할지 알고 시작한다. - 도달 불가 갈래 삭제: 캡처 그룹이 항상 리터럴 notify.sh로 끝나므로 `*"/.."`로 끝나는 경로는 그 검사에 닿을 수 없다. 없는 커버리지를 있는 것처럼 읽힌다. - here-string 근거 주석 정정. 앞 라운드는 두 형태를 구별할 수 없다고 적었는데, 그것은 한 줄 입력으로 재현을 시도한 부정 결과였다. 첫 줄에 매치를 두고 큰 꼬리를 붙이면 재현된다 — 8KB에서는 두 형태 모두 10/10 통과, 70KB·96KB에서 here-string 10/10 대 파이프라인 0/10. 경계가 64KB 파이프 버퍼를 감싼다. 붙인 픽스처 둘과 변이 코퍼스: - pretool-separator-required — 경로와 서브커맨드 사이 분리자가 선택이 되면 notify.shcancel 같은 붙은 형태가 승인된다. 세 형태 모두 DEFER를 단언한다. - pretool-large-input-bypass — 96,590바이트 다중 행 우회 명령. 구성한 크기가 실제로 파이프 버퍼 경계를 넘는지도 함께 단언한다. 헤더에 이 픽스처가 흔드는 축이 명령의 모양이 아니라 바이트 길이임을 적었다 — 모양 축을 아무리 촘촘히 훑어도 길이가 방아쇠인 결함에는 닿지 않으며, 앞 라운드가 「구성 불가」로 보고한 이유가 정확히 다른 축을 뒤진 것이다. - 변이 행 10개. 각 행의 기대 적색 집합은 실행 전에 성질에서 선언했고 관측에서 다시 쓰지 않았다. 10행 전부 선언과 일치, 자기 점검 통제군 둘 통과, 픽스처 12개 중 6개가 적어도 한 행의 단독 사살자다. 부수적으로 드러난 결함 둘: - pretool-bypass-skill-block-verbatim이 SKILL.md의 위치를 훅 경로에서 거슬러 올라가 구했다. 훅 경로는 시험 대상이므로 변이 하네스가 사본을 가리키는 순간 이 픽스처는 자기가 단언하는 것과 무관한 이유로 붉어진다. 드라이버가 REPO_ROOT 를 출력으로 내보내고 픽스처는 그것을 쓴다. - 세 픽스처 헤더가 α 경로를 「allow + applyPermissionRules 발행」으로 적고 있었다. 그 문장들은 쓰일 당시 참이었고, 밑의 갈래가 규칙 발행을 멈추면서 거짓이 되었으며, 같은 변경이 단언까지 함께 고쳤으므로 아무것도 붉어지지 않아 아무도 그 위를 다시 읽지 않았다. 교정 읽기로는 잡히지 않는 부류다 — 그 문장은 잘못 쓰인 적이 없다. 예방 문구는 파일 헤더가 아니라 그 단언을 핀하는 변이 행의 문단에 적었다: 그 행의 기대 적색 집합이 바로 그 세 픽스처이므로, 단언을 뒤집는 사람이 곧 그 문구를 읽는 사람이 된다.
본문의 self-cancel 절은 「더 나올 인스턴스가 없다」가 예측이고 모델은 자기가 만들지 않는 이벤트에 대해 그런 예측을 할 자격이 없다고 축자로 적은 뒤 조건 (a)를 요구하고, (a)가 실패하면 self-cancel을 금지한다. 프론트매터는 정확히 그 예측 기준으로 허가하고 있었고 (a)가 없었다. 그 절은 이번 델타 전후로 바이트가 같아 선재하지만, 델타가 그 절이 다스리는 인구를 넓혔다 — 한정사만 있는 계열과 혼합 열거가 이 표면에서 반복 모드로 들어오고 self-cancel은 반복 전용이다. 프론트매터에 실은 세 항목: - self-cancel은 repeat 전용 - 이번 사이클의 인스턴스가 전부 모델이 디스패치한 것의 완료일 때만 허가 - 아니면 금지하고 사용자 CANCEL 대기 이 표면은 예산이 있는 표면이라 세 항목을 그냥 더할 수 없다. 기증자 둘로 자리를 만들었고, 인가 근거는 사람의 허가가 아니라 삭제의 성질이다 — 예산을 넘길 때 조용히 잘리는 것은 `when_to_use`의 꼬리이지 `description`이 아니므로, 취약한 키에서 지우고 견고한 키를 남기는 중복 제거는 구성상 악화가 아니며 초과 가능성 자체를 낮춘다. - 종료 절이 여덟 줄 위 `description`에서 축자로 반복하던 취소 어휘 예시 삭제 - 포인터 줄의 죽은 단어 — 판정 기준이 위에 인라인됐으므로 「self-cancel 판정」 의 「판정」이 가리킬 것이 없다 실측: description=354, when_to_use=991, 합계 1345(경고선까지 5, 상한까지 191). 기준선 1332에서 순증 13. 예산 린트의 OK 줄이 하드 상한만 출력하던 것을 함께 고쳐, 경고선과 두 여유를 항상 병기한다. 독자는 남은 자리를 204로 읽는데 실제로 구속하는 값까지는 18 이었다. 이 커밋이 그 여유를 5까지 소비하는 당사자이므로, 여유를 보이지 않게 만든 채로 여유를 쓰는 일이 없어야 한다.
같은 커밋이 훅 승인 조건을 절대 경로로 좁혀 놓고, 그 바로 아래 정본 예시 넷은 계속 축약 세 세그먼트 형태를 보여 주고 있었다. 모델이 실제로 베끼는 것은 산문이 아니라 예시이므로, 예시가 보여 주는 형태는 훅이 거부하는 형태였다. - 네 예시를 `/absolute/path/to/…` 형태로 바꾼다. 그 접두는 설치된 스킬 디렉터리를 대신하는 자리표이고, 하중을 지는 것은 그 단어들이 아니라 앞의 슬래시임을 산문에 적었다. 네 형태 모두 훅이 실제로 allow를 내는지 확인했다. - 예시를 가리키며 「예시가 보여 주는 축약 형태가 아니라」라고 적던 문장은 이제 거짓이 되므로 함께 고친다. 게이트 1의 배제절은 왜 그 가드가 필요한지에 대해 관측적 이유만 싣고 있었다 — 「그렇게 흘려보내면 어떤 게이트에도 닿지 않는다」는 지금 캐스케이드를 걸어 본 결과다. 지속적 이유는 본문에 아예 없었다: 이 아래의 모든 게이트가 재발 한정사를 열쇠로 쓰므로 한정사가 없는 발화는 그중 무엇으로도 잡히지 않는다. 그 의존을 문장 안에 적어, 나중에 하류 게이트를 한정사 없이도 발화하도록 넓히는 사람이 이 기본값이 딛고 선 지반을 치우고 있음을 알게 한다. 삭제가 아니라 교체다. 이 수정이 닫지 못하는 것: 위 사실들을 처음 적은 커밋 본문의 두 문장이 자기가 서술하는 파일에 대해 여전히 거짓이다(「아홉 줄 아래」는 실제로 55줄 아래다). 그 커밋은 팁에서 네 번째라 고치려면 해시 넷이 움직이고 리포트 인용 열 줄과 제목 둘이 깨진다. 재저작을 강제하는 범위는 릴리스 커밋 하나뿐이므로 여기서 멈추며, 그 커밋만 단독으로 여는 사람은 여전히 거짓 문장을 읽는다.
릴리스 노트의 세션 지속 규칙 항목이 매처와 발행 규칙의 관계를 **양방향으로** 단언하고 있었다 — 규칙이 담지 못하는 형태 하나와 규칙이 다시 받아들이는 형태 하나를 각각 적었다. 그 관계는 어느 방향으로도 측정된 적이 없다. 혐의는 거짓이 아니라 입증되지 않았다는 것이고, 이 둘은 다르다: 나중에 측정해서 참으로 밝혀져도 그 문장이 근거 없이 쓰였다는 사실은 무를 수 없으며, 근거 없는 확신의 기능은 다음 감사자에게 여기 볼 것이 없다고 말하는 것이다. 이제 두 형태가 서로 다른 기계가 서로 다른 규칙으로 읽는다는 사실만 적고, 어느 쪽이 넓은지는 권한 엔진을 실제로 구동해야 정해지며 그 측정은 하지 않았다고 적는다. 규칙을 발행하지 않는 것은 관계를 알아서가 아니라 알 필요를 없애기 때문이다. 공유 절차 문서의 조건 없는 진술 다섯 자리를 참인 범위로 좁혔다. 어느 자리인지는 축자로 적혀 있지 않았고 앞 커밋이 이미 그 파일을 편집했으므로 지금 트리에서 다시 셌다 — 넷이 아니라 다섯이다. - 사전 조건 헤더의 「전부 발화 전에 확인된다」. 상태 무관 배너 경로는 넷 중 둘만 거치며 그 사실은 열한 줄 아래에 이미 적혀 있었다 - 가드 3과 가드 4의 플래그 처리 서술 둘. 같은 가드가 플래그 없는 표면에도 적용된다고 아래에서 말하므로, 그 독자는 일어날 수 없는 처리를 읽는다 - 불변량의 「전부 조용히 건너뛴다」 전칭. 앞의 두 가드는 사이클 경로 전용이다 - 「그 경로는 결코 조용한 실패로 넘어가지 않는다」. 바로 앞 절이 서술하는 센티넬이 정확히 그 전이를 만든다 — 첫 힌트 뒤로 그 TMPDIR 수명 동안 조용하다. 한 문장이 자기 앞 절과 모순되던 것이라, 실패가 공시되지만 매번 공시되지는 않는다고 적는다 릴리스 노트의 두 수치도 처리했다. 인용 표기 확장의 9→11은 새 숫자로 갈지 않고 **그 수치를 낸 계기를 문장 안에 넣었다** — 같은 릴리스에서 그 검사의 인식층이 교체됐으므로 지금 다시 재면 같은 수가 나오지 않는다. 픽스처 수는 이 커밋을 쓰는 시점의 트리에서 셌다(드리프트 39, 서브커맨드 15, 라이프사이클 37, 훅 12). 「필터」 명사도 지금 트리에서 다시 세니 두 파일 세 자리가 아니라 한 자리다. 나머지 둘은 정본 거처 신설이 흡수한 것이 아니라 그 파일 자체가 다시 쓰이면서 사라졌다. 남은 자리를 고친다 — 그 신호로는 어떤 행도 걸러지지 않으며, 행은 어느 쪽이든 실행되고 파싱되지 않는 변이체라는 사실은 판정 옆에 인쇄될 뿐이다. 네 코퍼스 전부에 매니페스트를 만든다. 부족분은 셋으로 세어져 있었으나 트리에서 다시 세니 넷이다 — 드리프트 코퍼스도 사전 측정 블록만 갖고 있었다. 각 매니페스트는 조항을 다시 쓰지 않고 정본 거처를 인용하며, 코퍼스 고유의 것만 갖는다: 무엇이 잡히지 않는지의 공시, 오늘 잰 판별력, 행 목록과 각 행의 기대 적색 집합. 공시할 자리가 없으면 퇴화 행 정책이 출하되면서 그 행들이 「측정 결과 아무것도 붉히지 않음」으로만 남고, 그것이 코퍼스가 픽스처 편집 산물을 핀 불가능한 성질로 문서화하게 되는 경로다. 라이프사이클 매니페스트는 단독 사살 수치를 형제에서 유추해 적지 않고, 이 하네스가 그 수치를 발행하지 않는다는 사실 자체를 적는다. 서브커맨드 매니페스트는 통제군 둘 중 하나만 표현 가능함을 적는다 — 유효 행 넷이 전부 단원소라 과소 선언을 만들 수 없다. 이번 라운드가 사용자 관측 가능하게 바꾼 것 넷도 릴리스 노트에 공시했다.
네 변이 코퍼스와 그것을 돌리는 하네스가 트리에 있는데 어떤 CI 잡도 그중 하나를 부르지 않았다. Makefile 타깃만 있었고, 그 타깃은 로컬에서 누군가 손으로 칠 때만 돌았다. 그동안 선언된 벡터는 아무도 그것을 실행하지 않는 채로 얼마든지 낡을 수 있었고, 이 라운드가 고친 결함 여럿이 정확히 그 창에서 자랐다. - 별도 잡으로 둔다. 한 번 통과에 각 픽스처 스위트를 선언된 변이 수만큼 다시 돌리므로 위 잡이 초 단위인 자리에서 이쪽은 분 단위다. 한 잡에 접으면 린트만 고친 PR도 그 시간을 기다린다. 분리는 실패가 「notify 잡이 붉다」가 아니라 하네스를 이름으로 가리키게 만드는 효과도 있다 - 네 하네스 전부 `--self-check`로 돈다. 실행이 벡터를 대조하는 것만으로는 부족하고, 그 대조가 틀린 벡터를 거절할 수 있는지, 그리고 그 거절이 우연이 아니라 대조에서 나왔는지까지 같은 실행이 보여야 한다 - 경로 필터에 하네스와 네 코퍼스, 그리고 조항의 정본 거처를 더한다. 앞선 주석은 글로브가 아무도 부르지 않는 스크립트를 깨운다는 이유로 하네스를 빼 두었는데, 이제 부르는 잡이 있으므로 그 이유가 성립하지 않는다. 코퍼스를 함께 넣는 이유는 변이 행이 자기가 선언된 픽스처 집합에 대한 주장이라서다 — 한쪽만 고치는 것이 사전 측정 블록이 잡으려는 바로 그 편집이다
신설한 CI 잡이 첫 실행에서 잡았다. 사전 측정 블록이 서브커맨드 코퍼스에 대해 「픽스처 집합이 바뀌었다」를 보고했는데 아무것도 바뀌지 않았다. 해시를 계산하는 `sort`가 콜레이션을 고정하지 않아, 같은 트리가 실행자의 로케일에 따라 다른 값으로 해시된다. 축은 대소문자다. C는 바이트로 비교하므로 `SKILL.md`가 `scripts/…`보다 앞서고 (`S`는 0x53, `s`는 0x73), UTF-8 로케일은 대소문자를 접어 반대로 놓는다. 픽스처 트리가 그 둘을 한 디렉터리 아래 갖고 있으므로 순서가 갈리고 해시가 갈린다. `LC_ALL=C`를 이 함수의 두 `sort`에 고정한다. **네 코퍼스 중 셋은 로케일과 무관하게 같은 값이 나온다** — 이름이 우연히 양쪽에서 같게 정렬되기 때문이다. 그래서 로컬에서는 넷 다 통과했고 CI에서 하나만 어긋났다. 대부분의 입력에서 우연히 옳은 계측기는 실제로 옳은 것과 구별되지 않는다. 회귀 핀을 붙인다. 이 헬퍼들은 자기 코퍼스가 없으므로(모든 코퍼스가 의존하는 것이라서) 임시 트리에 입력을 만들어 단언한다. - 해시가 로케일에 의존하지 않을 것 - **그리고 그 트리가 두 로케일에서 실제로 다르게 정렬될 것.** 이 통제군이 없으면 양쪽에서 같게 정렬되는 트리로도 첫 단언이 통과하고, 그것이 이 결함이 살아남은 경로 그대로다. 실제로 첫 시도의 입력이 그런 트리였고 이 통제군이 잡았다 - 이름을 바꾸지 않은 내용 편집이 해시를 움직일 것 - 접두 밖 형제가 해시에 들어가지 않을 것 - 퇴화 행이 대역 밖 파일로 판별되고, 기형 세 형태가 전부 exit 2일 것 재현 쌍은 긴 공통 접두 아래의 `…/active-notify/SKILL.md` 대 `…/active-notify/scripts/notify.sh`이며 짧은 이름으로는 재현되지 않는다. 그래서 테스트 픽스처를 실제와 같은 PLUGIN_ROOT 모양으로 만든다. 원인을 처음에는 구두점 접힘으로 짚었고 그것은 틀렸다. 기록된 해시 넷은 그대로다 — 원래 C 콜레이션 값이었고, 고정으로 CI가 그 값에 동의하게 된 것이다. 세 로케일(`C`·`en_US.UTF-8`·`C.UTF-8`)에서 네 게이트가 모두 통과함을 확인했다.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #88이
active-notify의 발화 축을 턴 경계에서 이벤트 클래스 인스턴스로 옮기면서 본문만 개편하고 상시 로드 표면을 손대지 않았다. 이 PR은 그 어긋남을 닫고, 그 과정에서 새로 드러난 것들을 마저 닫는다. 리뷰 한 차례를 받아 P1 6 / P2 19 / P3 21을 반영했고 선행 리뷰의 미반영분도 함께 처리했다.왜
여섯 건의 확인된 결함이 서로 다른 표면에 있으나 두 기전을 공유한다.
기전 1 — 커버리지 지표가 정확성을 세지 못한다. "그 텍스트가 열렸는가"는 참이고 "옳게 고쳐졌는가"는 거짓인 사례가 이 작업에서 셋 확인됐다. 선행 리뷰의 README 항목은 요구된 네 가지가 전부 반영됐는데 반영된 문안이 본문 규약이 금지하는 동작을 약속했다. 신설 린트는 설계가 지시한 제약 여섯 종을 전부 구현했는데 겨냥한 드리프트 클래스를 표기 한 종으로만 인식했다. 세 번째는 절이 눈에 띄게 재구조화되어 반영으로 계상됐으나 요구된 분할 축과 다른 축으로 나뉜 경우다.
기전 2 — 처음부터 거짓인 자기 진술은 리뷰에 원리적으로 걸리지 않는다. 한 문단이 어떤 비용을 두 자리에 적으면서 "이 비용은 한 곳에서만 서술된다"는 문장도 함께 새로 적었다. 세 문자열 모두 이 브랜치의 분기점에 없다 — 어느 커밋에서도 참인 적이 없다. 리뷰어는 직전 상태와의 차이를 보는데 이 텍스트에는 직전 상태가 없으므로, 대조할 기계가 없으면 통과한다.
무엇이 바뀌나
notify.sh호출을 승인하는 분기가 매치된 부분 문자열이 아니라 명령줄 전체에allow를 내고 있었다. 주입 페이로드 5종이 전부 실제로 실행됨을 센티넬로 확인했다. 앵커·매처 전환·문자 클래스 교체 셋이 함께 가야 닫힌다 —grep은 행 단위라 앵커가 행에 걸리고,[[:space:]]는 bash ERE에서 개행에 매치하므로 매처만 바꿔도 두 줄 명령이 뚫린다.arm의 플래그 쓰기를 원자 교체로 — 제자리 재기록이 파일을 수십 마이크로초 비우고, 그 순간을 읽은 fire가 플래그를 스테일로 간주해 삭제했다. 수정 전에는 표집기가 길이 0 플래그를 관측하고 수정 후에는 관측하지 않는다. 관측 횟수는 적지 않는다 — 표집률이 호스트와 부하에 좌우되고 실제로 두 실행이 서로 다른 값을 냈다. 이 성질은 픽스처가 갖고 있으므로 그 자리에서 다시 재는 것이 맞다.분할된 것 — 훅의 다른 승인 분기는 열린 채 남는다
이 PR은 훅의 두 자동 승인 분기 중 하나만 좁힌다. 권한 테스트 분기(
terminal-notifier)는 승인해야 할 형태가$(uname -s)·||·{ }를 포함하는 다행 셸 블록이라 구조적으로 앵커 불가이고, 해결에는 규약 재작성과 새 디스패처 서브커맨드, 그리고Makefile의 정규식 사본 동반 이동이 필요하다. 그 분기는 여전히 명령줄 전체를 승인하며, 별도 이슈로 남긴다. 이 PR을 두고 "훅 승인이 이제 한정됐다"고 읽어서는 안 된다. 그 사실은 릴리스 노트에도 적혀 있다 — 앞 초안은 무엇이 이번 대상이 아닌지만 말했고, 무엇이 대상이 아니라는 진술에서 독자는 무엇이 남았는지 도출할 수 없다.좁힌 분기에도 잔여가 있다. 명령어 단어가 절대경로여야 하고
..·~·글로브를 거절하게 됐지만, 어느 루트 아래든 깨끗한 절대경로는 설치된 것이 무엇인지 알지 못하는 한 설치된 것과 구별되지 않는다. 별도 이슈로 남긴다.사용자 관측 가능한 동작 변경 셋
"각 커밋마다, 각 배포마다 알림 줘"가 배너 2회 후 조용한 만료에서 상한 없는 반복 사이클로 바뀐다(repeat은 그룹 없이 발화해 알림 센터에 누적된다)."시작할 때랑 각 커밋마다 알림 줘"처럼 열거에 한정사 없는 유한한 순간이 섞인 요청도 같은 방향·같은 규모로 바뀐다. 그 순간은 버려지지 않고 같은 사이클의 구성원으로 합류한다.cd … && bash … notify.sh …). 무앵커 정규식이 master에 있으므로 이는 사용자가 설치한 상태에서의 변화다.검증
make lint/make test/make check모두 exit 0.버전은 2.1.1(patch) — 사용자에게 도달 가능한 기능을 더하지 않고 v2.1.0이 이미 주장한 것을 실제와 맞춘다. 훅 좁히기는 출하된 과잉 권한의 회수이며, 훅 헤더가 스스로 선언한 동의 범위가
notify.sh호출이었으므로 제공된 능력의 철회가 아니다.2차 리뷰 반영 (8커밋 추가)
같은 브랜치의 3라운드째다. 재리뷰가 P1 2 / P2 25 / P3 37을 냈고, 두 P1은 위 「기전 2」의 실례다 — 항상 적재되는 표면이 본문과 다른 배너 수를 내는 것, 그리고 릴리스 노트가 공시해야 할 사실을 공시하지 않는 것.
notify.sh레그에서 없애고, 명령어 단어에 형태 검사를 넣었다. 우회 레그의 규칙은 유지한다 — 그 레그는 이미 명령줄 전체를 승인하므로 규칙이 더하는 도달 범위가 없고, 유지된 그 규칙이 「우회 레그 규칙까지 제거」 변이를 단독으로 붉히는 핀이 된다.5→5.처럼 한 글자 추가로도 발동한다.Security로 옮겼다.Refs #18
Refs #112
Refs #116
Refs #128
Refs #129
3차 리뷰 반영 (12커밋 추가)
같은 브랜치의 4라운드째다. 재리뷰가 낸 P1 다섯 건은 「무엇이 참인가」가 아니라 **「무엇이 근거 없이 확신에 차 있는가」**를 공유한다. 근거 없는 확신은 나중의 측정으로 무를 수 없다 — 그 문장의 기능이 다음 감사자에게 여기 볼 것이 없다고 말하는 것이기 때문이다.
git status는 깨끗하고 하네스는 전부 통과를 보고한다. 리뷰 팀이 세 번 당했다. 이제 원본은 읽기 전용 스냅숏으로 한 번 복사되고 변이는 별도 사본에만 적용되며, 드라이버는 입력 이음매로 그 사본을 가리키고, 실행 전후로 원본 해시가 같은지 단언한다.함께 들어간 것
notify.shcancel같은 붙은 형태가 승인된다는 것, 그리고 96,590바이트 다중 행 우회 명령이 여전히 인식되는지. 후자의 축은 명령의 모양이 아니라 바이트 길이이며, 앞 라운드가 「구성 불가」로 보고한 이유는 다른 축을 뒤졌기 때문이다(8KB에서는 두 형태가 같고 70KB·96KB에서 갈린다 — 경계가 64KB 파이프 버퍼를 감싼다).이번 라운드에서 재도출한 수치 (이 커밋들의 트리에서 잰 값)
닫지 못한 것