Skip to main content

absence-guard-listing-spellings-proves-only-imagination

부재 가드를 "내가 본 적 있는 철자 목록"으로 쓰면 상상 밖의 철자로 살아남은 인스턴스를 통과시킨다 — 가드는 green, 결함은 생존. 패턴 목록 대신 **결과 집합(파일/호출자/심볼)을 핀으로 박아라**: 어떤 이름으로든 새 인스턴스가 생기면 목록에 없어서 실패한다. 핀이 썩는 것도 같이 막아라. 트리거 - 부재/금지를 grep 으로 주장하는 테스트, 삭제 PR 의 가드, "이제 아무도 X 를 안 읽는다" 주장, 감사 후속 정리.

Zur Installation springen

Quellinformationen

Repository
blas1n/claude-skills
Letzte Quellaktivität
9. September 2026 um 09:17
Erkannte Sprache von SKILL.md
Koreanisch
Sterne
2
Forks
0

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
absence-guard-listing-spellings-proves-only-imagination
description
부재 가드를 "내가 본 적 있는 철자 목록"으로 쓰면 상상 밖의 철자로 살아남은 인스턴스를 통과시킨다 — 가드는 green, 결함은 생존. 패턴 목록 대신 **결과 집합(파일/호출자/심볼)을 핀으로 박아라**: 어떤 이름으로든 새 인스턴스가 생기면 목록에 없어서 실패한다. 핀이 썩는 것도 같이 막아라. 트리거 - 부재/금지를 grep 으로 주장하는 테스트, 삭제 PR 의 가드, "이제 아무도 X 를 안 읽는다" 주장, 감사 후속 정리.
# 철자를 나열하는 부재 가드는 저자의 상상력만 증명한다 ## Problem 부재 가드는 보통 이렇게 쓰인다 — **자기가 본 적 있는 형태를 나열**해서. ```python hits = subprocess.run([ "grep", "-rn", "--include=*.py", "-e", "await workspace_region(", "-e", "WorkspaceRow.region,", "-e", "select(WorkspaceRow.region", "-e", "row.region", # ← 내가 본 receiver 이름 str(repo / "backend"), ], capture_output=True, text=True).stdout.strip() assert not hits, f"판독기가 살아남았다:\n{hits}" ``` **이 가드는 green 이었고, 살아있는 판독기 둘이 그대로 남아 있었다.** BSVibe 2026-08-28 실측(#844). vault 경로를 만드는 `region` 을 단일 정의로 모으면서 위 가드를 붙였다. 통과했다. 그런데 전체 스위트가 낸 실패를 고치려고 파일을 열자: | 살아남은 곳 | 철자 | 왜 안 걸렸나 | |---|---|---| | `product_bootstrap_runtime.py` | `region = ws.region` | receiver 가 `ws` — 내 목록엔 `row` 뿐 | | `bootstrap_anchor_backfill.py` | `region=ws.region` → `target.region` | `target` 은 생각도 못 한 이름 | 두 번째가 더 나쁘다. 컬럼이 배포 기본값과 다른 워크스페이스를 **아무도 읽지 않는 디렉터리에 retrofit 하고 `anchor_backfill_done` 을 찍는다** — 성공 로그를 남기는 무동작. > 패턴 목록으로 쓴 부재 가드는 **부재를 증명하지 않는다. 저자가 몇 가지 형태를 떠올렸는지를 > 증명한다.** 그리고 그 수는 항상 실제 형태보다 적다. ### 왜 특별히 잘 속나 - **부재 가드는 green 이 기본 상태다.** 아무것도 못 찾는 것과 찾을 게 없는 것이 같은 출력이다. (→ [[absence-measurement-validity-check]]) - 목록이 길수록 **더 꼼꼼해 보인다.** 4개를 나열하면 1개보다 안전해 보이지만, 놓친 5번째가 있으면 둘 다 똑같이 0점이다. - 리팩터 **직후**에 쓰기 때문에 목록이 "방금 내가 고친 것들"이 된다. 고치지 **못한** 것은 정의상 목록에 없다. ## Solution ### 결과 집합을 핀으로 박아라 — 패턴이 아니라 가드가 물어야 할 질문을 바꾼다. *"내가 아는 나쁜 형태가 있나"* 가 아니라 **"이 명제를 건드리는 곳 전부가 내가 승인한 목록과 같나"**. ```python # 넓게 잡는다 — receiver 이름을 묻지 않는다 hits = subprocess.run([ "grep", "-rnE", "--include=*.py", r"[A-Za-z_][A-Za-z0-9_]*\.region\b", # 어떤 receiver 든 str(repo / "backend"), ], capture_output=True, text=True).stdout.strip() # 산문은 뺀다 — 왜 지웠는지 설명하는 docstring 이 자기 이름을 부른다 files = { line.split(":", 1)[0].replace(str(repo) + "/", "") for line in hits.splitlines() if "``" not in line } allowed = { # 각 항목에 **왜 허용되는지**를 적는다. 이유를 못 적으면 허용하면 안 된다. "backend/api/v1/workspace_compliance.py", # 공시: 저장값을 그대로 보고, 경로를 안 만든다 "backend/mcp/tools/account_tools.py", # 같음 "backend/api/v1/workspaces.py", # 쓰기 측(컬럼이 아직 존재) "backend/.../settle_worker.py", # carrier: 값의 출처가 settings, 타입이 row 가 아님 } assert not sorted(files - allowed), \ "새 판독기가 생겼다:\n" + "\n".join(sorted(files - allowed)) ``` **어떤 철자든** 새 인스턴스가 생기면 그 파일이 `allowed` 에 없어서 실패한다. 내 상상력이 아니라 **승인 목록**이 기준이 된다. ### 핀이 썩는 것도 같이 막아라 허용 목록은 시간이 지나면 **더 이상 읽지 않는 파일**을 가리키게 된다. 그 항목이 남아 있으면 같은 파일에 생긴 **진짜 새 판독기**를 가려준다. ```python assert not sorted(allowed - files), \ f"허용 목록이 더 이상 읽지 않는 파일을 가리킨다: {sorted(allowed - files)}" ``` ### 가드가 진짜로 무는지 확인하라 — 비어 있지 않음을 심어서 ```bash cp target.py /tmp/bak printf '\ndef _probe(ws): return ws.region\n' >> some/unrelated/file.py uv run pytest tests/.../test_guard.py -q # ← 반드시 FAIL 해야 한다 cp /tmp/bak target.py ``` 일부러 **가드가 모르는 receiver 이름**(`ws`)으로 심어라. 목록형 가드였다면 이 대조군이 통과해버린다 — 그게 곧 진단이다. ### AST 로 옮겨도 끝이 아니다 — **어떤 AST 모양**을 세는지가 명제다 (2026-08-29) grep 을 AST 로 바꾸면 산문·주석·docstring 이 후보에서 빠진다. 거기서 멈추기 쉽다. 그런데 **무엇을 세느냐가 곧 무슨 명제를 증명하느냐**다. `source_ref` 삭제 PR 에서 가드 하나와 양성 대조군 하나에 **같은 스캐너**를 썼다: ```python def _dict_string_keys(tree): # dict 리터럴 키 · 첨자 · .get() 전부 ... ``` 부재 가드에는 맞았다("이 키를 쓰는 코드가 없다"). 그런데 양성 대조군 *"`write_seed` 가 `data` 에서 읽는 키는 title/tags/content 뿐"* 에 같은 걸 쓰자 함수가 **내보내는 이벤트 페이로드**의 `"path"` 가 잡혀 실패했다. AssertionError: write_seed 가 읽는 키가 바뀌었다: ['content', 'path', 'tags', 'title'] `"path"` 는 `data` 에서 읽은 게 아니라 `emit_event(..., {"path": ...})` 로 **쓴** 것이다. 스캐너가 "이 함수에 등장하는 dict 키"를 셌고, 내가 증명하려던 건 "이 함수가 `data` **에서 읽어 가는** 키"였다. **두 명제는 다르다.** 고침은 대상 변수에 묶는 것이었다 — `data[k]` · `k in data` · `data.get(k)` 세 형태만: ```python case ast.Subscript(value=ast.Name(id=name), slice=ast.Constant(value=str() as key)) if name == variable: ``` **교훈 둘:** - **하나의 스캐너를 부재 가드와 대조군에 돌려 쓰지 마라.** 부재 가드는 넓게 잡아도 되고(과잉 수집은 false positive 로 시끄럽게 실패한다), 소비자 계약을 재는 대조군은 **정확히** 그 방향만 잡아야 한다. 넓은 쪽을 그대로 쓰면 조용히 다른 질문에 답한다. - **대조군이 나를 잡았다.** 부재 가드만 있었으면 스캐너가 과잉 수집하는 채로 green 이었다 — 이번엔 우연히 오탐이 없었을 뿐이다. [[a-control-that-counts-is-blind-to-what-it-guards]] ### 삭제 PR 은 **자기가 만든 유령**을 잡는 가드가 필요하다 (2026-08-29) 가장 놓치기 쉬운 인스턴스는 트리에 원래 있던 것이 아니라 **이 PR 이 방금 만든 것**이다. 심볼을 지우면 그것을 가리키던 **상호참조가 그 순간 죽는다.** `VerifierWorker` + `SafeModeQueue.expire` 삭제 PR 에서 실제로 그랬다. 가드는 발견해 둔 유령 이름 하나(`expire_all_due`)를 텍스트로 박아뒀고 **green 이었다**. 그런데 방금 지운 `SafeModeQueue.expire` 를 `:meth:` 로 가리키는 docstring 이 **다섯 군데** 살아 있었다. 그중 하나는 단순 언급이 아니었다: Per-workspace callers should keep using :meth:`expire` (single-statement update, no audit emission) **없는 메서드를 쓰라고 지시한다.** 원래 있던 유령보다 나쁘다 — 이건 내가 만들었고, 독자에게 "이걸 쓰라"고 말한다. ⇒ **삭제하는 이름마다 "이걸 가리키던 것이 무엇이었나"를 세라.** 지운 심볼은 가드의 needle 목록에 **자동으로 들어가야 한다.** 발견 시점에 알고 있던 이름만 넣으면, 가드는 자기 PR 이 만든 유령에 대해 구조적으로 눈이 먼다. **그리고 무엇을 금지할지 정확히 정하라 — 언급이 아니라 가리킴이다.** 지운 이름을 *왜 지웠는지 서술하는 산문*은 정당하다(가드 파일 자신이 그렇다). 정당하지 않은 것은 **독자를 없는 곳으로 보내는 포인터**다. 그래서 needle 을 이름이 아니라 **역할 + 이름**으로 잡는다: ```python _DEAD = ("SafeModeQueue.expire", "mark_expired_bulk", "expire_all_due") # ✅ 가리킴만 막는다 if f":meth:`{dead}`" in text or f":meth:`~{dead}`" in text: survivors.append(...) # ❌ 이름을 통째로 막으면 "왜 지웠는지" 를 못 쓴다 — 가드가 자기 PR 의 # 커밋 메시지·체크리스트·자기 docstring 을 물어서 영원히 빨갛다 ``` 음성 대조군을 **양방향**으로 돌려라: `:meth:` 부활은 물어야 하고, `:meth:` 없는 단순 언급은 **통과해야** 한다. 후자를 확인하지 않으면 과잉 차단인 줄 모른다. ### 거울상 — **존재**를 주장하는 가드는 산문 때문에 *초록*이 된다 (2026-08-30) 여기까지는 전부 *부재* 가드 얘기였다. 같은 병이 반대 방향으로도 온다. 방화벽 실패-폐쇄 PR 에서 *"실패 경로가 PID 1 을 죽인다"* 를 이렇게 검사했다: ```python assert "kill" in script.lower() # ❌ ``` 음성 대조군에서 `kill 1` → `true` 로 바꿨는데 **통과했다.** 내가 같은 PR 의 헤더 주석에 써 둔 설명 문장이 grep 을 만족시켰기 때문이다: # 2. On any failure, KILL PID 1. A daemon that cannot prove its isolation # must not accept work — dying loudly beats serving silently unprotected. **가드가 자기 산문에 걸렸다.** 그리고 그 산문은 내가 방금 썼다 — 즉 가드를 잘 설명할수록 가드가 더 확실히 초록이 된다. 최악의 인센티브다. | 주장 | 산문이 하는 일 | 증상 | |---|---|---| | **부재** ("X 가 없다") | 후보를 만들어낸다 | 영원히 **빨강** → 백틱 필터 같은 미봉책을 부른다 | | **존재** ("X 를 한다") | 조건을 만족시킨다 | 영원히 **초록** → **결함이 살아남는다** | ⇒ 어느 쪽이든 규칙은 하나다: **산문은 후보가 아니어야 한다.** * Python 이면 AST 를 걸어라(§위 항목). * 셸/설정처럼 AST 가 없으면 최소한 **주석 줄을 걷어내고** 세라: ```python def _code_only(text): return "\n".join(l for l in text.splitlines() if not l.lstrip().startswith("#")) assert "kill 1" in _code_only(script) # ✅ true 로 바꾸면 떨어진다 assert _code_only(script).count("verify_firewall") >= 2 # 정의 + 호출 ``` * **정의만 세지 마라.** 함수가 있는 것과 불리는 것은 다른 명제다 — 호출까지 포함해 2회 이상을 요구하면 "정의해 놓고 아무도 안 부르는" 죽은 가드를 잡는다. **그리고 이건 음성 대조군이 없었으면 절대 못 잡았다.** 테스트는 green 이었고, 스크립트도 옳았다. 틀린 것은 *"이 테스트가 그 사실을 증명하는가"* 뿐이었고, 그건 **고의로 코드를 망가뜨려 봐야만** 드러난다. ### 집합 핀도 **자기가 고른 축**에서만 문다 (2026-09-09) 여기까지는 "패턴 목록 대신 집합 핀" 이었다. 그런데 **집합 핀 자체가 축을 하나만 고른다.** 지운 것에 이름이 여럿이면 나머지 축은 구조적으로 빨개질 수 없다. `audit_events` 테이블 삭제 PR 에서 가드를 이렇게 짰다 — 스킬대로 파일 집합을 핀으로 박고, 면제가 썩는 것까지 막고, `hasattr` 심볼 검사도 붙였다: ```python needles = (f'"{_DEAD_TABLE}"', f"'{_DEAD_TABLE}'", f"ix_{_DEAD_TABLE}_") # 테이블 리터럴 축 _DEAD_NAMES = (("plugin.audit.models", "AuditEvent"), ("plugin.audit", "AuditEvent")) # 심볼, 단 2개 모듈 ``` **전부 초록이었다.** 그리고 `AuditEvent` 를 **심볼로 import 하던 파일 넷**이 그대로 살아 있었다: plugin/audit/tests/test_models.py from plugin.audit.models import AuditEvent tests/test_bundle1_imports.py assert audit_mod.AuditEvent is not None tests/extensions/test_lift_r2a_....py "AuditEvent", ← 재export 기대 목록 tests/extensions/test_import_surface.py "AuditEvent", ← 같음 잡은 건 가드가 아니라 **스위트의 수집 에러**였다. 가드는 0점이다. | 축 | 내 가드 | 결과 | |---|---|---| | 테이블 리터럴 `"audit_events"` | 집합 핀 ✅ | 물었다 | | ORM 심볼 `AuditEvent` | `hasattr` — **내가 나열한 2개 모듈만** | 넷을 놓쳤다 | | 인덱스 접두사 `ix_audit_events_` | 집합 핀 ✅ | 물었다 | `hasattr` 은 스킬의 감별표가 *"정확하고 철자 문제가 없다"* 고 한 도구다. 맞다 — **다만 내가 이름 댄 모듈에 대해서만** 정확하다. 심볼을 **다른 곳에서 import 하는 파일**에 대해서는 아무 말도 안 한다. 두 도구 사이의 틈으로 축 하나가 통째로 빠졌다. **고침 — 지운 이름의 축을 나열하고 각각에 스캔을 건다:** ```python #: 지운 ORM 심볼. AuditEventBase / AuditEventSubscriber 는 **현역**이라 #: 접두사 매칭으로는 못 센다 — 뒤에 식별자 문자가 오지 않는 것만 잡는다. _DEAD_SYMBOL = re.compile(r"\bAuditEvent(?![A-Za-z0-9_])") def test_no_source_still_names_the_orm_symbol() -> None: offenders = _scan(lambda line: _DEAD_SYMBOL.search(line) is not None) assert not offenders, f"지운 ORM 심볼을 아직 가리킨다: {offenders}" ``` ⚠️ **살아 있는 형제가 접두사를 공유하는 것이 바로 이 축을 건너뛰게 만드는 이유다.** `AuditEvent` 를 그냥 찾으면 현역 `AuditEventBase`·`AuditEventSubscriber` 가 수십 건 잡혀서 "이 축은 스캔이 안 되겠다" 고 포기하게 된다. **negative lookahead 한 줄이면 된다** — 포기하지 마라. ⇒ **가드를 쓰기 전에 "이것의 이름이 몇 개냐" 를 먼저 적어라.** 테이블이면 보통 셋이다(리터럴 · ORM 심볼 · 인덱스/제약 접두사). 클래스면 둘이다(심볼 · import 경로). 축 목록을 안 적으면 **가장 조용한 축이 남는다.** ## 감별 — 언제 목록이 맞고 언제 집합이 맞나 | 목표 | 도구 | |---|---| | 특정 **심볼**이 사라졌나 | `assert not hasattr(mod, "name")` — 정확하고 철자 문제가 없다. ⚠️**단 내가 이름 댄 모듈에 대해서만** — 그 심볼을 import 하는 다른 파일에는 눈이 멀다(위 §축) | | 특정 **import 경로**가 안 쓰이나 | `grep "from backend.foo"` — 동명이인을 안 문다 (→ [[deletion-pr-needs-an-absence-guard-and-a-control]] #5) | | **어떤 형태로든** 이 개념을 만지는 곳이 승인 목록뿐인가 | **집합 핀** (이 스킬) | 앞의 둘은 목록이어도 된다. **세 번째만 목록이 원리적으로 실패한다** — 형태가 열려 있기 때문이다. ## Verification - [ ] 가드가 receiver/변수 이름을 열거하지 **않는다** - [ ] 허용 목록의 **모든 항목에 이유가 적혀 있다** - [ ] 무관한 파일에 가드가 모르는 철자로 심어 **FAIL 을 봤다** - [ ] 허용 목록의 stale 항목도 실패시킨다 - [ ] 가드가 산문(docstring/주석)을 매칭하지 않는다 — 안 그러면 "이미 다 했다"고 보고한다 ## Related - [[absence-measurement-validity-check]] — 빈 출력이 "없음"이 아닌 더 아래 층 - [[deletion-pr-needs-an-absence-guard-and-a-control]] — 삭제 PR 전반. 이름 대신 import 를 세라(#5)는 **거짓 양성**을 막고, 이 스킬은 **거짓 음성**을 막는다. 둘 다 필요하다 - [[a-control-that-counts-is-blind-to-what-it-guards]] — assert 에 개수가 보이면 명제를 그대로 써라
Auf GitHub ansehen