Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .reviewbot/config.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 이 파일은 여러 리뷰 봇이 나눠 쓴다.
# - 최상위 키 : 이 파일을 읽는 모든 봇에 적용되는 공통 설정
# - `it-play:` : 이 봇에만 적용 (겹치는 최상위 키를 덮어쓴다)
# - `sandrone:` : sandrone-code-review-bot 에만 적용 — 이 봇은 읽지 않는다
# 모르는 키는 그냥 무시한다. 상대 봇 전용 키(tone, summaryPlacement 등)를 최상위에 둬도 안전하다.

model: darwin-35b-q4_k_m.gguf
language: ko
temperature: 0.2
Expand All @@ -20,4 +26,10 @@ triggerPrefix: "/review"
# 부르는 이름은 코드에 고정되어 있다(src/config.ts 의 BOT_MENTION).
threadReply: true

# 이 봇에만 적용할 값이 있으면 아래처럼 네임스페이스 블록에 둔다.
# 지금은 이 리포를 이 봇만 리뷰하므로 최상위 키만으로 충분하다.
#
# it-play:
# maxInlineComments: 40

# 리뷰 지침은 여기 두지 않는다 — 리포지토리 루트의 AGENTS.md 또는 CLAUDE.md 를 그대로 읽는다.
45 changes: 43 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,16 @@ export const DEFAULT_CONFIG: BotConfig = {
/** 리포지토리 루트의 설정 파일 후보 (먼저 발견된 것 하나만 사용) */
export const CONFIG_FILES = ['.reviewbot/config.yml', '.reviewbot/config.yaml', '.reviewbot.yml', '.reviewbot.yaml']

/**
* 이 봇의 설정 네임스페이스 키. 앞에 있는 것부터 순서대로 적용한다(뒤가 이긴다).
*
* 설정 파일 하나를 여러 리뷰 봇이 나눠 쓴다 — 최상위 키는 모든 봇이 읽는 공통 설정이고,
* 봇 이름을 키로 둔 블록은 그 봇에만 적용된다. 다른 봇(`sandrone` 등)의 블록은 읽지 않는다.
*
* 짝이 되는 sandrone-code-review-bot 도 같은 규칙으로 `sandrone:` 블록을 읽는다.
*/
export const BOT_NAMESPACES = ['it-play', 'itplay'] as const

/**
* 리포지토리 루트의 코딩 지침 문서 후보 (먼저 발견된 것 하나만 사용).
*
Expand All @@ -124,6 +134,10 @@ export const CONFIG_FILES = ['.reviewbot/config.yml', '.reviewbot/config.yaml',
*/
export const INSTRUCTION_FILES = ['AGENTS.md', 'CLAUDE.md']

function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null && !Array.isArray(value)
}

function coerceStringArray(value: unknown): string[] | undefined {
if (!Array.isArray(value)) return undefined
return value.filter((v): v is string => typeof v === 'string')
Expand Down Expand Up @@ -169,6 +183,33 @@ function pickFileConfig(raw: unknown): Partial<BotConfig> {
return out
}

/**
* 설정 파일 한 장에서 이 봇에 적용할 값을 뽑는다: 최상위 공통 설정 → 이 봇의 네임스페이스 블록.
*
* 네임스페이스 블록은 최상위와 같은 키를 그대로 쓴다 — 겹치는 키는 블록 쪽이 이긴다.
* `exclude` 도 마찬가지로 덮어쓴다(기본 제외 목록 + 그 레이어의 목록). 누적하지 않는 것은
* sandrone 쪽 동작과 맞추기 위해서다 — 같은 파일을 읽은 두 봇이 서로 다른 파일을 리뷰하면 안 된다.
*/
function pickRepoConfig(raw: unknown): Partial<BotConfig> {
let merged = pickFileConfig(raw)
if (!isRecord(raw)) return merged

for (const namespace of BOT_NAMESPACES) {
const block = raw[namespace]
if (!isRecord(block)) continue
log.info(`설정 네임스페이스 적용: ${namespace}`)
merged = { ...merged, ...pickFileConfig(block) }
}

// 다른 봇의 블록은 건너뛴다. 키 이름은 외부 입력이라 개수만 남긴다.
const skipped = Object.keys(raw).filter(
(key) => isRecord(raw[key]) && !(BOT_NAMESPACES as readonly string[]).includes(key),
).length
if (skipped > 0) log.info(`다른 봇의 설정 네임스페이스 ${skipped}개를 건너뛰었다`)

return merged
}

function envOverrides(): Partial<BotConfig> {
const env = process.env
const out: Partial<BotConfig> = {}
Expand All @@ -194,15 +235,15 @@ function envOverrides(): Partial<BotConfig> {
}

/**
* 설정 병합 순서: 기본값 → 리포지토리 설정 파일 → 환경변수
* 설정 병합 순서: 기본값 → 설정 파일 최상위 → 설정 파일의 이 봇 네임스페이스 → 환경변수
*
* 설정 파일 내용은 호출부가 GitHub API로 읽어 넘긴다 — 리포지토리를 체크아웃하지 않는다.
*/
export function loadConfig(fileContent?: string): BotConfig {
let fromFile: Partial<BotConfig> = {}
if (fileContent !== undefined) {
try {
fromFile = pickFileConfig(parseYaml(fileContent))
fromFile = pickRepoConfig(parseYaml(fileContent))
} catch (error) {
log.warn(`설정 파일 파싱 실패: ${(error as Error).message} — 기본값을 사용한다`)
}
Expand Down