- name
- deleting-inert-config-row-disables-hidden-feature
- description
- 설정 행(룰·바인딩·커넥터·플래그 테이블)을 "한 번도 발화한 적 없으니 중복/불필요"라며 지우기 전에 — 그 행의 *존재*를 `has_any`/`exists`/`count>0` 로 읽는 코드가 있는지 세라. 한 용도에서의 비활성이 다른 용도에서의 무해함을 뜻하지 않는다. 증상: 무관해 보이는 기능이 삭제 직후 조용히 꺼지고, 아무 에러도 안 난다.
# 지워도 되는 줄 알았던 설정 행이 다른 기능의 스위치였다
## Problem
설정 테이블의 한 행이 **두 가지 목적**을 겸한다:
1. **내용**이 쓰이는 목적 — 그 행이 *무엇을 말하는가* (라우팅 대상, 임계값, 매핑)
2. **존재**가 쓰이는 목적 — 그 행이 *있는가 없는가* (= 암묵적 피처 플래그)
2번은 대개 **어디에도 적혀 있지 않다.** 그래서 1번 기준으로 "이 행은 죽었다"를
정확히 측정하고도, 지우면 2번이 같이 꺼진다.
- **증상**: 설정 하나를 정리한 다음날, **무관해 보이는 기능**이 동작을 멈춘다.
에러 없음 · 로그 없음 · 테스트 green. 그 기능은 그냥 **조용히 no-op** 이 된다.
- **근본 원인**: `if not await repo.has_any(workspace_id): return` 같은
**존재만 묻는 호출**이 그 테이블을 피처 게이트로 쓰고 있다.
- **흔한 오해**: *"이 룰은 한 번도 발화한 적 없다 → 지워도 아무 일도 안 일어난다."*
전제는 참인데 결론이 거짓이다. **발화 = 내용 사용**이고, 게이트는 **존재**를 읽는다.
둘은 서로 다른 관측량이다.
### 실제 사례 (BSVibe, 2026-08-18)
`design (plan) → opus` 라우팅 룰이 프로덕션에서 한 번도 resolve 되지 않음을
**정식으로 확인**하고(별도 PR 로 근본 원인까지 규명) 중복으로 판단해 삭제했다.
그런데 `agent_runner._maybe_spawn_impl_run` 이:
```python
if not await self._workspace_has_routing_rules(design_run.workspace_id):
return # ← 룰이 하나라도 있어야 design→impl 체이닝이 발화
```
그 룰이 **해당 워크스페이스의 마지막 룰**이었다(전체 런의 73%가 도는 곳).
결과가 최악의 모양이다 — 프레이머는 여전히 런을 `design_then_impl` 로 표시하므로
루프는 **빌드가 아니라 SPEC** 을 하라고 지시받고, 명세가 떨어지고, **아무도 구현하지
않고**, 런은 `verified` 로 **완료 보고**된다. 사용자는 코드를 요청하고 명세를 받으면서
"완료" 를 듣는다.
체이닝된 하위 런: 삭제 전날 1건 → 삭제 당일 **0건**.
## Solution
### 1. 삭제 전 — 테이블/리포지토리 이름으로 전수 grep
행의 *내용* 필드가 아니라 **테이블·리포지토리·모델 이름**으로 찾아야 한다.
게이트는 내용을 안 읽으므로 필드명으로는 안 걸린다.
```bash
# 내용 기준 (이것만 하면 놓친다)
grep -rn "artifact_type_hint\|target_model" --include="*.py" .
# 존재 기준 — 이쪽이 진짜 위험한 곳
grep -rnE "has_any|exists\(|\.count\(|is not None|len\(.*rules" --include="*.py" . \
| grep -i "<table_or_repo_name>"
```
**`has_any` · `exists` · `count() > 0` · `if rows:` 는 전부 "존재를 읽는다"는 신호**이고,
곧 **피처 플래그**라는 뜻이다.
### 2. 삭제 전 — "이 테이블이 비면 무엇이 달라지나"를 한 문장으로 답하라
답할 수 없으면 아직 지울 준비가 안 된 것이다.
### 3. 삭제 후 — 무관해 보이는 기능 하나를 실제로 돌려라
유닛 테스트는 이걸 못 잡는다. 게이트가 꺼지면 코드 경로가 **일찍 return** 할 뿐이라
아무것도 실패하지 않는다. **프로덕션에서 한 번 돌려 산출물을 세는 것**만이 증거다.
```sql
-- 삭제 전/후로 같은 축을 세라 (날짜별 파생 산출물 수)
select created_at::date, count(*) from <downstream_rows>
where <chained_marker> is not null group by 1 order by 1;
```
### 4. 수정 — 존재를 플래그로 쓰지 말고 게이트를 자기 근거로 세워라
```python
# Before — 룰 테이블이 파이프라인 스위치를 겸한다
if not await self._repo.has_any(workspace_id):
return
# After — 파이프라인 결정은 파이프라인 신호가 한다
if frame.get("pipeline") != "design_then_impl":
return
```
⚠️ 게이트를 지울 때 **그 결함 동작을 명세로 박아둔 기존 테스트**가 거의 항상 있다
(`test_no_chaining_without_routing_rules` 같은 이름). 지우지 말고 **뒤집어서**
새 계약을 고정하라 — 그 테스트가 왜 존재했는지가 커밋 기록에 남는다.
## Key Insights
- **"발화한 적 없다" ≠ "지워도 무해하다".** 한 값이 두 목적을 겸하면, 한 목적에서의
비활성은 다른 목적에서의 무해함을 **전혀** 함의하지 않는다. 측정이 정확해도
결론은 틀릴 수 있다 — 측정한 관측량이 결론이 필요로 하는 관측량과 다르면.
- **존재를 읽는 호출이 가장 위험하다.** 내용을 읽는 코드는 필드명으로 grep 에 걸리지만,
`has_any`/`exists` 는 **아무 필드도 언급하지 않아서** 안 걸린다. 삭제 영향 조사는
반드시 **테이블/리포지토리 이름**으로 한 번 더 돌려라.
- **가장 나쁜 실패는 에러가 아니라 조용한 조기 완료다.** 게이트가 꺼지면 예외가 아니라
`return` 이 나고, 상류는 아무것도 모른 채 "성공" 을 보고한다. 사용자는 요청한 것과
다른 산출물을 받으면서 **완료 알림**을 듣는다. 실패보다 나쁘다.
- **에이전트가 게으른 것처럼 보이면 다음 단계를 의심하라.** 다단계 파이프라인에서
"요청은 코드였는데 명세만 나왔다"는 대개 에이전트의 회피가 아니라 **이어졌어야 할
하류 단계가 안 돈 것**이다. 상류 단계는 제 일을 정확히 했다.
→ [[agent-degraded-output-suspect-execution-environment]]
- **삭제의 사후 검증은 프로덕션에서만 된다.** 게이트 삭제/추가는 유닛 테스트가
구조적으로 못 잡는 변경이다(경로가 실패하는 게 아니라 사라진다).
같은 축으로 **삭제 전후 산출물 수**를 세라.
## Related
- `config-menu-offers-options-nothing-implements` — 반대 방향: 메뉴에 있는데 주방에 없다.
이 스킬은 **주방에서 쓰는데 메뉴에 안 적혀 있다.**
- `absence-measurement-validity-check` — 0을 재기 전에 생산자가 도는지 확인하라.
- `feedback_queue_apply_step_never_wired` — 유닛 통과하나 프로덕션 호출자 0.
View on GitHub