spec(p9-fb-25): config workspace.include 제거 + 지원 형식 가시성

도그푸딩 피드백 2026-05-05:
1. config 의 include + exclude 동시 존재가 case 4 (둘 다 매치 안 함)
   에서 의미 모호.
2. 어차피 처리 가능 형식 (md / png / jpg / pdf) 이 정해져 있으니
   사용자에게 명시 필요.

설계 핵심:

- `WorkspaceCfg.include: Vec<String>` 제거 (denylist-only). 옛 config
  의 `include = [...]` 은 silently 무시 + Config::load 가 단발
  deprecation warning emit.
- `IngestItem.warnings` 에 skip 사유 채움 (`unsupported media type:
  .docx` / `kb:// URI not yet supported`).
- `IngestReport.skipped_by_extension: BTreeMap<String, u32>` 신규
  (additive wire — release 트리거 안 됨 per CLAUDE.md). key =
  lowercase ext (`docx`, `txt`), no-ext = `<no-ext>` sentinel.
- CLI / TUI summary 에 breakdown 표시 (`90 skipped: 80 docx, 5 txt,
  5 epub`) — 모두, desc 정렬.
- README + `kebab init` config.toml 주석에 지원 형식 명시.

Spec status `planned`. 다음 단계: writing-plans skill 로 implementation
plan 작성.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-05 11:21:42 +00:00
parent a68c47124f
commit 40ca4bf27e

View File

@@ -0,0 +1,166 @@
# p9-fb-25 — Config `workspace.include` 제거 + 지원 형식 가시성
**Date**: 2026-05-05
**Status**: planned
**Audience**: kebab-config / kebab-app / kebab-cli / kebab-tui implementer.
**Source feedback**: 사용자 도그푸딩 2026-05-05 — config 의 `workspace.include` + `workspace.exclude` 가 동시에 있으면 case 4 (둘 다 매치 안 함) 의미 모호 + 어차피 처리 가능 형식이 정해져 있으니 사용자에게 명시 필요.
## Goal
- `WorkspaceCfg.include` 필드 제거. dead config field 제거 + denylist-only 모델 정착.
- 사용자가 ingest 결과에서 \*\*어떤 파일이 왜 skip 됐는지\*\* 즉시 파악.
- 지원 형식 (md / png / jpg / pdf) 을 README + `kebab init` config 주석에 명시.
## Non-goals
- include 의 enforce 로직 추가 (반대 방향).
- 새 extractor (txt / docx / epub 등) 도입 — 별 spec.
- `kebab doctor` 가 unsupported 파일 카운트 분석 — 별 task (간단 follow-up 가능).
## Allowed dependencies
- 기존 crate 만. 신규 crate 없음. 신규 SQLite migration 없음.
## Storage 변경
없음.
## API / Wire 변경
### `kebab-config::WorkspaceCfg`
`include: Vec<String>` 필드 제거. `exclude: Vec<String>` 만 유지.
backward-compat: serde default `deny_unknown_fields` 미사용이라 옛 config 의 `include = [...]` 은 silently deserialize 통과 + 무시. `Config::load` 가 옛 키 발견 시 `tracing::warn!` 로 deprecation 경고 emit (단발 — 같은 process 안에서 한 번만):
```
deprecated config: `workspace.include` 필드는 더 이상 사용되지 않습니다 (p9-fb-25). 처리 가능한 형식 (md / png / jpg / pdf) 은 extractor 가 자동 결정. 다음 버전부터 config 갱신 권장.
```
검출 방법: `Config::load` 가 raw TOML 파싱 후 `workspace` 테이블의 키 이름을 살펴 `include` 존재 여부 확인. `serde_ignored` crate 미도입 (YAGNI) — `toml::Value` 로 raw lookup 한 번.
### `kebab-core::IngestItem.warnings`
Skipped path 가 빈 `Vec` 대신 사유 한 줄 채움. 두 case:
- media-type filter (extractor 미지원): `format!("unsupported media type: .{ext}")` (e.g. `"unsupported media type: .docx"`). extension 이 없으면 `"unsupported media type: <no-ext>"`.
- `kb://` URI: `"kb:// URI not yet supported"`.
### `kebab-core::IngestReport.skipped_by_extension`
신규 필드:
```rust
pub skipped_by_extension: std::collections::BTreeMap<String, u32>,
```
key = lowercase extension without leading dot (`"docx"`, `"txt"`, `"epub"`). 확장자 없는 파일 = `"<no-ext>"` sentinel (꺾쇠로 일반 ext 와 시각 구분).
`BTreeMap` 사용 — wire JSON 안에서 key 정렬 안정. `HashMap` 은 매 직렬화마다 순서 바뀌어 diff / snapshot 테스트 noisy.
`AggregateCounts` 도 동일 필드 추가 — TUI / CLI 가 in-flight 와 final 모두에서 일관 표시.
### Wire schema `ingest_report.v1`
`skipped_by_extension` 필드 additive 추가:
```json
"skipped_by_extension": {
"type": "object",
"additionalProperties": {
"type": "integer",
"minimum": 0
},
"description": "p9-fb-25: per-extension skip count. Key = lowercase extension without leading dot (e.g. 'docx'). Files without extension key under '<no-ext>'."
}
```
CLAUDE.md 의 release 규약 (additive minor) 에 따라 release bump 트리거 안 됨.
## TUI / CLI 노출
### CLI summary
기존:
```
✓ ingest: 100 docs (5 new, 3 updated, 2 unchanged, 90 skipped), 142 chunks indexed in 12s
```
변경 (skipped > 0 + breakdown 있을 때만 괄호 안):
```
✓ ingest: 100 docs (5 new, 3 updated, 2 unchanged, 90 skipped: 80 docx, 5 txt, 5 epub), 142 chunks indexed in 12s
```
extension 카운트 desc 정렬 (큰 거 먼저). 모두 표시 (top-3 제한 없음). Line 길어질 우려가 있으나 사용자 원함 — line wrap 은 terminal 책임.
### TUI
`kebab-tui::ingest_progress::status_line` 의 final / aborted 라인 동일 포맷. in-flight 진행 중에는 breakdown 표시 안 함 (idx 진행 중 계속 변동, 불필요 noise).
## 사용자 안내 (docs)
### README
`kebab ingest` row 의 cell 끝에 추가:
```
**지원 형식** (extractor 자동 결정): Markdown (`.md`) / 이미지 (`.png`, `.jpg`, `.jpeg`, OCR + caption) / PDF (`.pdf`). 다른 확장자는 자동 skip — `--json` / TUI 의 `IngestItem.warnings` 에 사유 (`unsupported media type: .docx` 등). 카운트 분류는 `IngestReport.skipped_by_extension`.
```
### `kebab init` config.toml 주석
`[workspace]` section 위에 주석 추가:
```toml
# [workspace] — 색인 대상 디렉토리 + denylist.
#
# 지원 형식 (extractor 가 자동 결정 — config 에 명시할 수 없음):
# - Markdown: .md
# - 이미지: .png .jpg .jpeg (OCR + caption)
# - PDF: .pdf
#
# 다른 확장자는 ingest 시 자동 skip + warning. 처리 대상 폴더의
# 일부만 ingest 하고 싶으면 `kebab ingest <path>` 로 root 명시
# 또는 `.kebabignore` 파일 / 본 `exclude` 로 denylist.
[workspace]
root = "..."
exclude = [...]
```
## Tests
### 신규 단위
- `kebab-config`: `Config::load` 가 옛 `include = [...]` 발견 시 warning emit + 정상 deserialize. snapshot test (in-memory string TOML).
- `kebab-core`: `IngestItem` JSON serde — `warnings``["unsupported media type: .docx"]` round-trip.
- `kebab-core`: `IngestReport.skipped_by_extension` JSON serde — `BTreeMap` 정렬 stable.
### 신규 통합
- `kebab-app`: 다양한 확장자 mix (`.md`, `.docx`, `.txt`, no-ext 파일) workspace 에서 ingest → `report.skipped_by_extension == {"docx": 1, "txt": 1, "<no-ext>": 1}` + 각 skipped 의 `warnings` 채워짐.
- `kebab-tui`: `status_line``90 skipped: 80 docx, 5 txt, 5 epub` 형식.
- `kebab-cli`: `kebab ingest --json` 출력에 `skipped_by_extension` 필드.
### 기존 영향
- 기존 `IngestReport` 구성 site (테스트 fixture 등) 가 새 필드 default 로 채움 (`BTreeMap::new()`).
- `WorkspaceCfg``include` 필드 제거로 컴파일 에러 → 매 site 정리 (기존 default 가 `vec!["**/*.md"]` 였으니 모두 제거).
## Spec contract impact
- design §6.2 의 `workspace.include` 항목 invalidate. frozen spec 그대로 두고 본 spec + HOTFIXES `2026-05-05 — p9-fb-25` 가 source of truth.
- design §3.x `IngestReport``skipped_by_extension` 필드 추가 (additive).
- design §2.4a `IngestEvent::AssetFinished` 에 새로 emit 되는 warnings 의미 추가 (variant 변경 없음, content 풍부화).
## Risks / notes
- **옛 config 가 `include = ["**/*.md"]` 같은 narrow 한 allowlist** 면 본 변경 후 그 이상의 확장자 (예: 파일 추가된 `.png`) 가 자동 ingest 시작. 사용자 의도와 어긋날 수 있음. 완화: deprecation warning 의 문구가 \"처리 가능 형식 자동 결정\" 명시 → 사용자가 alarm 받음. + README 변경. 경계 case 라 design accepted.
- **`skipped_by_extension` 용량**: workspace 가 1만 파일이면 dict size 작음 (extension 종류는 보통 < 50). wire 영향 무시.
- **deprecation warning 단발 vs every-load**: `Config::load` 가 매 CLI 호출마다 발생. 단발 (`std::sync::Once`) 이 깔끔. 본 spec 은 단발 채택.
- **release 트리거**: wire schema additive + serde backward-compat → CLAUDE.md release 규약 의 minor 트리거에 해당 안 됨 (additive 만으로 release 안 찍음). 사용자 explicit 도그푸딩 요청 시 bump.
## Live deviations
추후 발견되는 deviation 은 `tasks/HOTFIXES.md` `2026-05-05 — p9-fb-25` 항목에 기록.