Community라이팅 & 에디팅github.com

jessegall/commandments-writing-detectors

프로젝트 로컬 규칙을 직접 작성하는 방법 — 이후 모든 파일을 판단하는 규칙을 `.commandments/custom/`에 추가하는 방법을 설명합니다. 새 규칙/검사/탐지기를 요청하거나, `commandments make`가 이곳을 안내하거나, 코드 검사를 위해 정규 표현식을 사용하려 할 때 먼저 읽어야 합니다.

commandments-writing-detectors란 무엇인가요?

이 스킬은 `commandments` 도구를 사용하여 사용자 정의 코드 검사 규칙(계명)을 작성하는 방법을 설명합니다. 핵심 개념은 규칙이 단순히 정규 표현식이 아니라, 엔진이 실행하는 질문이라는 것입니다. `.commandments/custom/` 디렉토리에 JSON 파일로 규칙을 정의하며, 각 규칙은 검사할 엔진(예: PHP), 위반 사항(sin), 그리고 선택기(selector)와 `where`/`reject` 단계로 구성된 `find` 쿼리를 포함합니다. 각 단계는 정확히 하나의 검사(check)를 수행합니다. 규칙은 재사용 가능하며, 이미 작성된 규칙을 다른 규칙 내에서 참조할 수 있습니다. 또한 `either`, `not`, `all`과 같은 논리 연산자를 지원하며, 다양한 검사 유형(이름, 유형, 참조 대상, 위치 등)을 제공하여 강력하고 유연한 코드 분석이 가능합니다. 주로 커맨드라인 도구이지만, `composer sins` 명령어로 생성되는 검사 목록을 통해 문서화되어 있습니다.

지원 대상✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/jessegall/code-commandments/tree/HEAD/skills/writing-detectors

Installed? Explore more 라이팅 & 에디팅 skills: steipete/notion, langchain-ai/langchain, bytedance/podcast-generation · View all 6 →

즐겨 사용하는 AI에게 물어보기

이 에이전트 스킬이 미리 로드된 새 채팅을 엽니다.

문서

사용자 정의 계명(Commandment) 작성하기

기본 제공 규칙이 한계가 아닙니다. 리뷰에서 반복해서 언급하는 관행은 아직 작성되지 않은 규칙입니다. 한 번 작성하면 이후 모든 파일을 판단합니다.

규칙은 엔진이 답하는 질문입니다

사용자 정의 규칙은 바이너리가 실행하는 데이터입니다 — .commandments/custom/ 디렉토리나 그 하위 디렉토리에 위치한 .json 파일입니다. 프로젝트의 여러 부분에 대한 규칙을 그룹화할 수 있습니다 (custom/core/, custom/viewer/). 규칙은 파일 이름으로 식별되므로, 폴더로 이동해도 설정 파일에서 해당 규칙을 참조하는 모든 부분이 유효하며, 같은 이름의 파일이 두 개 있으면 하나가 조용히 승리하지 않고 모두 거부됩니다. 규칙은 엔진, 위반 사항(sin), 그리고 쿼리를 지정합니다: 선택기(selector) 로 시작하여 노드를 유지하는 where 단계와 노드를 제거하는 reject 단계로 구성됩니다. 각 단계는 정확히 하나의 검사(check)를 수행합니다; 규칙은 각 단계가 묻는 질문만큼만 좋습니다.

{
    "engine": "php",
    "sin": {
        "name": "raw-sql",
        "description": "호출 지점에 인라인으로 작성된 SQL.",
        "rule": "쿼리는 해당 테이블을 소유한 리포지토리를 통해 전달되어야 합니다.",
        "skill": "no-raw-sql"
    },
    "find": {
        "select": "call",
        "where": [
            {"resolves": "Illuminate\\Support\\Facades\\DB", "of": "child:class"},
            {"nameIn": ["select", "statement", "unprepared"]}
        ],
        "reject": [
            {"file": "*Repository.php"}
        ]
    }
}

선택기(selector) 는 모든 언어에서 동일하게 읽히는 중립적인 종류입니다 — call, construction, function, type-declaration, assignment, loop, branch, return, throw, catch, comparison, literal, member-access, null-safe, identifier, import, parameter, block, bail-out, expression-statement, self-reference — 또는 kind:<Kind>로 언어 자체 노드 종류를 지정합니다 (kind:Expr_MethodCall).

규칙은 다른 규칙으로 구성됩니다. 이미 작성한 검사는 이름으로 참조되어 다시 작성되지 않습니다: {"rule": "ActionClass"}는 해당 이름의 규칙이 노드를 찾을 때 통과하며, find에서 "select" 대신 "from": "ActionClass"를 사용하면 해당 규칙이 찾은 대상을 좁힙니다. 이는 하나의 규칙이 다른 규칙 내에서 읽히는 방식입니다. 두 경우 모두 .commandments/custom/에 있는 동일한 엔진의 모든 규칙에 도달하며, 각 부분은 자체적으로 나열되고 실행되는 독립적인 규칙으로 유지됩니다. 선언되지 않은 이름, 다른 엔진의 규칙, 그리고 자기 자신으로 돌아오는 체인은 규칙이 읽힐 때, 어떤 것도 판단되기 전에 거부됩니다.

두 번째 규칙 없이 either와 not을 사용하세요. {"any": [{"is": "loop"}, {"is": "branch"}]}는 하나의 단계가 통과할 때 통과하고, {"all": [...]}는 모든 단계가 통과할 때 통과하며, {"not": {...}}는 해당 단계가 통과하지 않을 때 통과합니다 — 이는 where가 reject만이 할 수 있는 일을 표현하는 방법입니다.

단계의 단일 검사 — 도구 자체에서 생성된 모든 검사입니다:

그것이 무엇이며 어떻게 불리는가

검사노드를 유지하는 조건
{"is": "loop"}해당 중립적 종류에 속할 때
{"kind": "Expr_StaticCall"}언어 자체의 종류일 때, 언어가 작성하는 방식 또는 용어가 철자하는 방식으로: Expr_StaticCall과 static-call은 동일한 구조입니다. 철자는 종류에서 파생되므로, 편집기의 자동 완성 목록은 언어가 실제로 내보내는 것을 나열하며, 두 종류가 공유하는 철자는 어느 것의 철자도 아닙니다
{"name": "select"}그 이름(호출이나 접근의 경우 이름 자식 노드)이 해당 값일 때
{"nameIn": ["select", "statement"]}그 이름이 그 중 하나일 때
{"nameLike": "get*"}그 이름이 글로브(glob) 패턴과 일치할 때, *는 모든 문자 시퀀스, ?는 한 문자
`{"nameMatches": "^(getset)[A-Z]"}`
{"nameMood": "question"}그 이름이 해당 언어적 양식(mood)에 속할 때: bare-verb는 동사 자체(holds, ships), third-person은 동사를 서술(entersTestMode), question은 의문사로 시작(is, has, can, should). 이것은 이름의 양식을 읽을 뿐, 규칙이 작성하는 단어 목록을 읽지 않습니다. 여기서 이름을 판단하는 유일한 검사이며, 이름이 특정 방식으로 읽히는지 여부만 답합니다: 양식이 무엇을 하는지에 대해 무엇을 위반으로 만드는지는 해당 멤버가 답하는 것과 결합하거나 변경 사항이 있는지 확인하세요. 기본 제공 규칙이 그러하듯이
{"nameCase": "snake"}그 이름(선행 $ 또는 _ 제외)이 camel, pascal, snake, upper 또는 kebab 케이스로 작성되었을 때
{"hasModifier": "static"}해당 수정자(modifier)를 가질 때
{"hasFlag": "byRef"}해당 언어의 트리가 설정하는 플래그(flag)를 가질 때
{"operator": "*"}그 연산자가 정확히 해당 값일 때: *와 ??는 두 이항 표현식을 구분하고, +=는 할당, !는 단항, extends는 상속 절을 구분합니다
{"operatorIn": ["*", "/"]}그 연산자가 그 중 하나일 때

그것이 말하는 것

검사노드를 유지하는 조건
{"text": "select * from users"}리터럴의 텍스트가 정확히 해당 값일 때
{"textLike": "*where id*"}리터럴의 텍스트가 글로브 패턴과 일치할 때
{"textMatches": "(?i)^select"}리터럴의 텍스트가 정규 표현식과 일치할 때
{"commentIs": "history"}주석이 교정된 산문 판독 중 하나일 때: history는 코드가 무엇을 하는지가 아니라 왜 변경되었는지 서술하고, strawman은 아무도 하지 않을 실수에 대해 방어하며, restates는 문서화하는 코드가 하지 않는 것을 말합니다. 판독은 산문 패키지 자체의 것이며, 모든 언어에서 자체 테스트로 측정되며, 집합은 닫혀 있습니다: 규칙은 직접 작성한 단어와 일치하는 산문이 아니라 교정된 판정을 요청합니다. 주석에 대해 ("select": "comment") 해당 주석을 판단하고, 코드에 대해 요청하면 해당 코드에 대해 작성된 주석을 판단합니다
{"docReference": {"resolves": false}}문서 주석이 사라진 코드를 명명할 때: 그것이 만든 참조(<see cref>, {@see}, @see, Sphinx :class:)가 아무것도 해석되지 않고, 프로젝트가 소유해야 했을 때. 두 참조는 그대로 둡니다. 이것이 깨진 링크 스캐너와의 전체 차이점입니다: 컴파일러가 어차피 해석할 수 없었던 하나와 소유자가 스캔 외부에 선언된 하나. 해석은 브리지 자체의 것입니다
{"commentLike": "*TODO*"}그 위의 주석 또는 바로 위의 주석 시퀀스가 글로브와 일치할 때 — 주석 자체에 대해 ("select": "comment") 요청하면 해당 주석이 말하는 것과 일치하므로, 하나의 키는 규칙이 도달한 방식에 관계없이 주석을 읽습니다
`{"commentMatches": "(?i)\b(TODOFIXME)\b"}`
{"docTag": "deprecated"}문서화가 해당 태그를 가질 때: PHPDoc과 JSDoc에서는 @tag, C#에서는 XML 요소, Python 독스트링에서는 Sphinx 필드/지시문 또는 Google/NumPy 섹션
{"documented": false}문서 주석을 가질 때, false로 설정하면 갖지 않을 때

그것이 참조하는 것

검사노드를 유지하는 조건
{"resolves": "App\\Models\\User"}그 이름이 해석되어 참조하는 것이 해당 값일 때 — 또는 아무것도 명명하지 않는 값(변수, 속성)의 경우, 그것이 보유하는 클래스: 정확히 그 클래스, 확장하는 클래스가 아님
{"resolvesLike": "App\\Models\\*"}그 이름이 해석되어 참조하는 것이 글로브와 일치할 때, 또는 값이 보유하는 클래스가 그럴 때: {"resolvesLike": "*Service", "of": "child:var"}는 모든 *Service의 수신자
{"declaredBy": "App\\Models\\*"}그것이 읽는 멤버가 해당 글로브가 명명하는 클래스에 의해 선언될 때: 수신자가 보유하는 클래스에서 시작하여 그 부모를 따라 올라가 멤버를 선언하는 클래스를 찾습니다. 이것은 어떤 이름도 말할 수 없는 이 필드가 누구의 것인지입니다; 스캔에서 수신자에 유형이 없거나 선언이 멤버를 선언하지 않으면 비어 있습니다 — 따라서 단계는 아무것도 유지하지 않습니다
{"answeredBy": false}그것이 읽는 것을 선언하는 유형이 이미 그것이 묻는 질문을 명명할 때 — 전체 본문이 같은 것을 반환하는 해당 유형의 메서드(어느 방향으로든 읽힘) — 또는 false로 설정하면 그렇지 않을 때. 이것은 존재하는 이름을 사용하라와 이름을 붙여라를 구분하며, 그것을 명명하는 바로 그 메서드가 묻는 질문은 결코 그것을 묻는 장소 중 하나가 아닙니다
{"sameAs": {"of": "closest:loop", "target": "child:iter"}}같은 파일의 다른 노드와 같은 것을 명명할 때, of를 따라 그 노드에 도달한 다음 target을 따라 거기서 도달합니다 — 규칙이 물을 수 없는 유일한 질문: 루프가 순회하는 바로 그 컬렉션에 대한 remove, 방금 읽은 필드인 인수. 둘은 동일하게 작성되어야 하며, 그렇지 않으면 동일한 트리로 읽혀야 합니다; 경로 끝에 노드가 없으면 통과하지 않습니다
{"declaredElsewhere": true}그것이 명명하는 것이 그것이 위치한 파일이 아닌 다른 파일에 선언될 때, false로 설정하면 그렇지 않을 때. 그것을 선언하는 파일 외부에서 설정된 필드는 해당 클래스가 자신을 변경하기 위해 작성한 메서드를 우회했으며, 이는 유형의 고유 작업에 대한 규칙이 묻는 질문입니다; 스캔에서 선언이 그것이 명명하는 것을 선언하지 않으면 아무것도 보유하지 않으므로, 규칙은 소유자도 함께 말합니다
{"type": "number"}표현식이 가진 유형일 때, 언어가 읽는 방식: 검사기가 부여한 것, 형태가 말하는 것, 또는 선언이 작성하는 유형 — 그중 원시 타입, resolves는 클래스만 답합니다
{"typeLike": "Money*"}표현식이 가진 유형이 글로브와 일치할 때
{"calls": {"name": "dd"}}그것의 자체 호출(중첩된 함수 내부가 아닌)이 단계를 통과할 때
{"argument": {"at": 0, "is": "literal"}}해당 위치의 인수가 단계를 통과할 때; 음수 at는 끝에서부터 세며, -1은 마지막, 없으면 아무것도 통과하지 않습니다. at가 전혀 없으면 어떤 인수든 단계를 통과하는 것으로 충분하며, 이는 이름으로 전달된 인수(위치가 식별자가 아닌)에 대해 규칙이 묻는 방식입니다
{"constructs": "Date*"}생성(construction), 또는 다른 것의 자체 생성이 글로브가 명명하는 유형의 인스턴스를 생성할 때 — Python에서는 클래스 호출
{"unused": true}아무것도 참조하지 않을 때: 외부 자체의 함수나 유형, 스캔이 해석하는 이름과 호출 및 해당 언어의 호출 그래프를 통해; 매개변수는 함수 내에서 이름이 읽히는 방식으로; 필드나 속성은 수신자가 보유한 유형을 통해 선언에 도달하는 것, 필드는 대부분의 언어에서 어떤 기호도 참조하지 않으므로. 언어 자체가 이름으로 도달하는 멤버와 재정의(override)하는 멤버는 해당되지 않습니다: 프로젝트가 작성하는 어떤 것도 그들을 명명할 필요가 없습니다
{"referredFrom": {"is": "parameter"}}그것을 참조하는 것 — 호출, 이름, 작성된 유형 — 이 단계를 통과할 때. 이것은 참조 자체에 대해 묻는 calledFrom이며, 파일 위치가 아니라 한 쪽을 다른 쪽과 구분합니다: 디스패치에 의해 명명되고 어떤 리스너의 매개변수에도 없는 이벤트 클래스는 아무도 듣지 않으며, {"not": {"referredFrom": …}}는 정확히 그것을 말합니다
{"calledFrom": "app/Http/**"}그것을 참조하는 것 — 대개 호출 — 이 file이 읽는 방식과 동일하게 글로브와 일치하는 파일에 있을 때

그것이 위치한 곳

검사노드를 유지하는 조건
{"file": "*Repository.php"}프로젝트(판단되는 리포지토리 또는 폴더)로부터의 경로 또는 그 끝부분이 글로브와 일치할 때: *는 폴더 내에 머물고 **는 폴더를 넘어 .gitignore처럼 교차합니다
`{"fileMatches": "(^/)pages/.*(Create
{"namespaceLike": "App\\Http\\*"}네임스페이스, 패키지 또는 모듈이 글로브와 일치할 때
{"layer": "App\\Domain"}프로젝트가 선언한 스택 계층(백엔드, Python, C#)에 위치할 때
{"topLevel": true}모든 함수(클로저 포함)와 모든 유형 외부에 위치할 때 — 파일이 로드될 때 실행되는 코드
{"withinLoop": true}루프 내부에 위치할 때, false로 설정하면 그렇지 않을 때
{"position": "first"}형제 중 first, last 또는 only일 때 (하나만 있으면 first이자 last입니다)
{"field": "cond"}트리에서 부모의 해당 슬롯을 채울 때 — 언어 자체의 슬롯 이름(예: cond, body, else, args) — 다른 검사가 묻지 않는 위치의 축입니다: 인수로 전달되는 호출이 아니라 조건으로 서 있는 호출. 이름은 해당 언어의 것이므로, 이 이름으로 작성된 규칙은 해당 언어를 판단합니다
{"role": "member"}모든 브리지가 작성하는 단어로 해당 종류일 때: statement, expression, member(유형, 함수, 속성 또는 열거형 케이스의 모든 선언), type(작성된 유형), pattern, markup 또는 other. 이것은 언어별로 종류를 명명하지 않고 선언을 말하는 방법이며, 멤버의 순서나 동반 관계에 대한 규칙에 필요합니다
{"descendant": {"is": "return"}}내부의 어떤 노드가 단계를 통과할 때; "within": "ownBody"를 사용하면 자체가 보유한 노드만 단계에 들어가며, 중첩된 함수에서 멈춥니다. 이는 저장하는 메서드와 저장을 클로저에 넘겨 큐에 넣는 메서드의 차이입니다
{"child": {"is": "return"}}한 수준 내부의 어떤 노드가 단계를 통과할 때, descendant는 모든 깊이에서 말할 수 없습니다: 자체 본문이 반환하는 함수, if 내부에서만 반환하는 함수는 해당되지 않습니다. "field"를 사용하면 트리의 해당 필드를 채우는 자식만 단계에 들어가며, 카운트의 필드가 카운트하는 대상을 좁히는 것과 같습니다
{"body": {"is": "branch"}}본문의 어떤 문(statement)이 단계를 통과할 때, 언어가 본문을 감싸는 방식으로 읽힙니다: C#은 블록 문을 작성하고 Python은 베어 문을 작성하므로, child는 다르게 답하고 이것은 동일하게 답합니다. 이것이 루프나 함수의 본문에 대한 하나의 규칙이 모든 언어에 대해 한 번 작성되는 방식입니다
{"inside": {"is": "catch"}}위의 어떤 노드(파일 루트까지)가 단계를 통과할 때
{"all": [{"name": "store"}, {"hasModifier": "pu

관련 스킬