Skip to main content

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

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

跳到安装

来源信息

仓库
blas1n/claude-skills
最近来源活动
2026年9月9日 09:17
检测到的 SKILL.md 语言
韩语
星标
2
分支
0

安装方式

默认使用会先检查来源的 Prompt;你也可以切换为直接命令,或下载本地副本。

检查来源文件

决定是否安装前,请先阅读 SKILL.md,以及 SkillsMP 当前展示的配套文件。

正在显示 SKILL.md

SKILL.md
来源说明 · 只读预览
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 에 개수가 보이면 명제를 그대로 써라
在 GitHub 查看