ADR-04: Early-Return 프레임워크 탐지
| 날짜 | 작성자 | 리포지토리 |
|---|---|---|
| 2025-12-23 | @KubrickCode | core |
상태: 승인됨
배경
문제 상황
SpecVital Core는 다양한 언어와 프레임워크에 걸쳐 테스트 파일이 어떤 프레임워크에 속하는지 탐지함. 이 탐지는 다음을 처리해야 함:
- 패턴을 공유하는 유사 프레임워크: Jest와 Vitest 모두
describe/it구문 사용 - 파일당 복수의 유효한 신호: import 문, 설정 파일 존재 여부, 콘텐츠 패턴
- 결정론적 요구사항: 동일 입력은 항상 동일 결과를 생성해야 함
- 모노레포 복잡성: 서로 다른 스코프를 가진 중첩된 설정 파일
탐지 신호
테스트 파일은 여러 신호로 식별 가능함:
| 신호 | 예시 | 신뢰도 |
|---|---|---|
| Import | import { test } from 'vitest' | 최고 |
| Config 스코프 | jest.config.js 스코프 내 파일 | 높음 |
| 콘텐츠 | jest.fn(), vi.mock() 패턴 | 중간 |
| 파일명 | *.test.ts, *_test.go | 낮음 |
전략적 질문
탐지기가 여러 신호를 어떻게 조합하여 단일 프레임워크 결과를 생성해야 하는가?
결정
우선순위 기반 early-return 사용: 신호 신뢰도에 따라 첫 매칭이 승리함.
탐지는 엄격한 우선순위 순서를 따름:
- Import → 프레임워크별 import가 발견되면 즉시 반환
- Config 스코프 → 파일이 설정 스코프 내에 있으면 즉시 반환
- 콘텐츠 패턴 → 프레임워크별 패턴이 발견되면 즉시 반환
- Unknown → 매칭된 신호가 없으면 반환
어떤 우선순위 레벨에서든 첫 번째 성공적인 매칭이 발생하면 하위 우선순위를 확인하지 않고 즉시 반환함.
탐지 소스 추적
각 결과는 프레임워크가 어떻게 탐지되었는지를 포함함:
go
type DetectionSource string
const (
SourceImport DetectionSource = "import"
SourceConfigScope DetectionSource = "config-scope"
SourceContentPattern DetectionSource = "content-pattern"
SourceUnknown DetectionSource = "unknown"
)검토한 옵션
옵션 A: 우선순위 기반 Early-Return (선택됨)
신호 신뢰도 계층에 따라 첫 매칭이 승리함.
장점:
- 빠른 실행: 첫 매칭에서 중단, 불필요한 처리 없음
- 예측 가능한 동작: 동일 입력은 항상 동일 결과 생성
- 디버깅 용이: "어떤 신호가 매칭되었는지" 추적이 명확함
- 단순한 구현: 복잡한 점수 계산 로직 없음
단점:
- 하위 우선순위 신호 무시: 더 강해도 후순위 신호는 평가되지 않음
- Import 추출 민감성: 잘못된 import 파싱은 잘못된 결과로 이어짐
옵션 B: 점수 축적
각 신호에 신뢰도 점수를 부여하고 합산하여 최고 점수 프레임워크를 반환함.
점수 예시:
| 신호 | 점수 |
|---|---|
| 스코프 | 80 |
| Import | 60 |
| 콘텐츠 | 40 |
| 파일명 | 20 |
장점:
- 여러 신호가 서로를 강화 가능
- 더 강한 전체 신호가 약한 초기 매칭을 덮어쓸 수 있음
단점:
- 디버깅 어려움: "왜 이 프레임워크가 이겼는가?"는 모든 점수 분석 필요
- 튜닝 복잡성: 점수 값은 임의적이고 보정하기 어려움
- 비결정론 위험: 점수 동점 시 추가 tie-breaking 규칙 필요
- 성능 오버헤드: 결정 전 모든 신호 평가 필요
옵션 C: 하이브리드 접근법
점수 축적에 early-exit 임계값 적용 (예: 점수가 100을 초과하면 즉시 반환).
장점:
- 속도와 신호 조합의 균형
단점:
- 점수 계산의 복잡성 상속
- 임계값은 임의적임
- 여전히 전체 점수 계산 로직 필요
구현 세부사항
Config 스코프 해석
여러 설정 파일이 파일에 적용될 수 있을 때, 탐지기는 가장 구체적인 것을 선택함:
- 깊이 기반 선택: 더 깊은 설정 경로가 우선 (더 구체적)
- Tie-breaker 1: 더 긴 설정 경로 (더 구체적인 경로)
- Tie-breaker 2: 사전순 정렬 (결정론적)
project/
├── jest.config.js # 깊이 0
├── packages/
│ └── web/
│ └── jest.config.js # 깊이 2 (packages/web/ 내 파일에 대해 승리)언어별 처리
Go 테스트 파일은 import 탐지 대신 네이밍 컨벤션 (*_test.go)을 사용하며, 일반 탐지 흐름 전에 특별 케이스로 처리함.
네거티브 매칭
프레임워크는 "확실히 이 프레임워크가 아님" 신호를 선언 가능함:
go
type MatchResult struct {
Confidence int
Evidence []string
Negative bool // true이면 이 프레임워크 제외
}유사한 프레임워크가 패턴을 공유할 때 false positive를 방지함.
결과
긍정적
성능
- 빠른 실행: 첫 매칭에서 종료
- 하위 우선순위 신호에 대한 불필요한 계산 없음
- 최선 O(1), 최악 O(프레임워크 × 신호)
유지보수성
- 단순한 제어 흐름 (순차적 우선순위 검사)
- 탐지 단계별 명확한 책임
- 새 프레임워크나 신호 추가 용이
디버깅 용이성
- 결과에 탐지 소스 포함
- "왜 이 프레임워크인가" 추적 용이
- 감사할 복잡한 점수 계산 없음
결정론
- 동일 파일은 항상 동일 결과 생성
- Config 스코프 tie-breaking이 완전히 결정론적
- 무작위 또는 타이밍 의존적 동작 없음
부정적
신호 계층이 고정됨
- 프로젝트별 우선순위 순서 조정 불가
- 완화: 우선순위 순서는 실제 신뢰도 분석 기반
Import 파싱 민감성
- 잘못된 import 추출은 잘못된 결과 유발
- 완화: 포괄적인 테스트 커버리지를 갖춘 언어별 추출기
후순위 신호 무시
- import가 매칭되면 콘텐츠 패턴은 검사되지 않음
- 완화: Import가 가장 신뢰할 수 있음. 후순위 신호 무시는 의도적임.
설계 원칙
- 명시적 개발자 의도가 승리: Import 문은 의식적인 프레임워크 선택을 나타냄
- 프로젝트 설정이 권위적: 설정 파일은 프로젝트 레벨 결정을 정의함
- 콘텐츠 패턴은 폴백: 명시적 신호가 없을 때만 사용함
- 파일명 패턴은 신뢰할 수 없음: 탐지에서 제거됨 (false positive가 너무 많음)
관련 ADR
- ADR-03: Tree-sitter AST 파싱 엔진 - import 추출에 사용되는 파싱 인프라
참조
pkg/parser/detection/detector.go- 핵심 탐지 구현pkg/parser/detection/result.go- DetectionSource를 포함한 결과 타입pkg/parser/framework/scope.go- Config 스코프 해석 로직
