검색 hit / RAG citation 에 indexed_at + stale 두 wire 필드 추가. documents.updated_at 재활용 (V006 incremental ingest 가 자연 source-of-truth). config [search] stale_threshold_days = 30 default. additive minor wire. TUI Warning role / CLI plain [stale] tag / agent --json 동시 surface. 자동 재 ingest 는 out of scope. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8.6 KiB
title, phase, component, task_id, status, target_version, contract_source, contract_sections, date
| title | phase | component | task_id | status | target_version | contract_source | contract_sections | date | ||
|---|---|---|---|---|---|---|---|---|---|---|
| p9-fb-32 — Stale doc indicator design | P9 | kebab-app + kebab-store-sqlite + kebab-search + kebab-cli + kebab-tui | p9-fb-32 | design | 0.4.0 | ../../docs/superpowers/specs/2026-04-27-kebab-final-form-design.md |
|
2026-05-08 |
p9-fb-32 — Stale doc indicator
Goal
검색 hit / RAG citation 에 "마지막으로 색인된 시점" 과 "임계 초과 stale 여부" 두 신호를 노출. 사용자 / agent 가 답변 근거의 최신성 약점을 즉시 인지할 수 있게 한다. 자동 재 ingest 는 하지 않는다 — 표시까지가 본 task 범위.
Behavior contract
Stale 정의
stale = (now - documents.updated_at) > stale_threshold_days * 86400s
documents.updated_at은 V001 부터 존재. 마지막 실제 re-process 시점 (RFC3339).- fb-23 incremental ingest 의 skip path 는
put_document를 호출하지 않으므로updated_at이 자연스럽게 stale 의 source-of-truth 역할. stale_threshold_days = 0→ 모든 hitstale = false(기능 비활성).- 음수 threshold → config load error (
error.v1, exit code 2).
Wire schema delta
search_hit.v1 — 두 필드 additive (required):
| 필드 | 타입 | 의미 |
|---|---|---|
indexed_at |
string (RFC3339 date-time) |
hit 의 source doc documents.updated_at |
stale |
boolean |
server-computed now - indexed_at > threshold |
citation.v1 — 동일 두 필드 additive (required). answer.v1.citations[] 가 본 schema 사용.
answer.v1 — 변경 없음. citation 단위로 stale 정보 운반. aggregate flag (any_citation_stale) 는 out of scope — consumer 가 citations[] 순회로 충분.
schema_version 은 search_hit.v1 / citation.v1 그대로. additive minor 라 breaking 아님 — 바이너리 version cascade 의 wire 트리거에 해당하지 않음. 단, frozen wire schema 정책상 필드 추가 자체는 spec 갱신 필요 → CLAUDE.md §wire-schema 의 v1 명세 반영.
Config
config.toml [search] 섹션 — 신규 필드:
[search]
stale_threshold_days = 30 # 0 = 비활성. 양의 정수.
- env override:
KEBAB_SEARCH_STALE_THRESHOLD_DAYS. - default: 30 (코드
Defaultimpl). - validation: 음수 →
Config::loaderror (error.v1.code= config_invalid). - 변경 즉시 반영 (compute 가 query path 에 위치, ingest 불필요).
CLI plain output
--json 미설정 시 (사람 읽기용 plain stderr/stdout) — 각 hit / citation 의 doc_path 옆에 [stale] tag 추가:
1. [stale] notes/dmq-quota.md § 운영 절차
score=0.812 indexed_at=2025-12-03
색상은 terminal capability 있을 때만 노란색. 없으면 plain 텍스트. 비활성 (threshold=0) 또는 fresh hit 은 tag 미표시.
TUI
Theme::Role::Warning재사용 (fb-14 에 정의됨).- search pane / inspect pane / ask citation pane: stale doc 의
doc_path우측에[STALE]배지. - 색상 단독 의미 전달 금지 정책 (fb-14) 준수 — 텍스트
[STALE]+ Warning 색. T토글 (fb-14) 영향 없음 (Warning role 은 dark/light 양쪽 정의됨).
Allowed dependencies
각 crate 기존 deps 유지. 신규 dep 없음. time 크레이트 (이미 kebab-store-sqlite / kebab-app 에서 사용).
Forbidden dependencies
kebab-core는 wire 변환 / threshold 계산 안 함 (도메인 타입만).- UI crate (
kebab-cli/kebab-tui/kebab-mcp) 가 직접 SQL 호출 X —kebab-appfacade 통해서만.
Public surface delta
kebab-core (도메인)
// SearchHit / Citation 도메인 struct 에 indexed_at: time::OffsetDateTime 필드 추가.
// stale 은 도메인이 아니라 wire 계산 결과 — facade 에서만 채움.
kebab-store-sqlite
// 기존 search query JOIN documents 확장 — updated_at SELECT.
// retriever 가 chunk hit 과 함께 indexed_at 받음.
kebab-search (lexical/vector/hybrid)
// 내부 Hit struct 에 indexed_at: OffsetDateTime 운반 필드 추가.
// fusion / scoring 로직 영향 없음.
kebab-app (facade)
pub fn compute_stale(indexed_at: OffsetDateTime, now: OffsetDateTime, threshold_days: u32) -> bool {
if threshold_days == 0 { return false; }
let delta = now - indexed_at;
delta.whole_seconds() as u64 > u64::from(threshold_days) * 86400
}
// search / ask wire DTO 변환 시 indexed_at + stale 두 필드 채움.
// now 는 Clock 추상화로 주입 (테스트 결정성 위함).
kebab-config
#[derive(Deserialize, ...)]
pub struct SearchConfig {
#[serde(default = "default_stale_threshold_days")]
pub stale_threshold_days: u32,
// ... 기존 필드
}
fn default_stale_threshold_days() -> u32 { 30 }
env: KEBAB_SEARCH_STALE_THRESHOLD_DAYS.
kebab-cli
render_hit_plain / render_citation_plain 에 [stale] tag 추가. 색상 헬퍼는 기존 is_tty 검사 패턴 재사용.
kebab-tui
search/inspect/ask pane 의 doc_path 라인 렌더에 Theme::style(Role::Warning) 으로 [STALE] Span 추가. snapshot 테스트 갱신.
Test plan
| kind | description |
|---|---|
| unit (kebab-config) | default().search.stale_threshold_days == 30 |
| unit (kebab-config) | 음수 threshold load → error.v1 (config_invalid) |
| unit (kebab-config) | KEBAB_SEARCH_STALE_THRESHOLD_DAYS=7 env override |
| unit (kebab-app) | compute_stale(now-31d, now, 30) == true |
| unit (kebab-app) | compute_stale(now-29d, now, 30) == false |
| unit (kebab-app) | compute_stale(_, _, 0) == false (모든 입력) |
| unit (kebab-app) | compute_stale(now-30d, now, 30) == false (boundary, 정확히 동일 = false) |
| 통합 (kebab-app/cli) | search wire JSON 의 각 hit 에 indexed_at (RFC3339) + stale (bool) 존재 |
| 통합 (kebab-app/cli) | ask wire JSON citations[] 각 항목에 동일 두 필드 |
| 통합 (kebab-tui) | stale doc 포함 search pane snapshot 에 [STALE] 배지 (insta [time] redaction 적용) |
| 통합 (kebab-cli) | plain output 에 [stale] tag 정확한 위치 |
| 통합 (wire-schema) | search_hit.schema.json / citation.schema.json 갱신 + JSON Schema validation 통과 |
| 통합 (smoke) | docs/SMOKE.md 시나리오에 stale 시나리오 추가 — 30일 전 ingest 시뮬레이션 (Clock 주입) |
snapshot 갱신: 기존 search / ask 관련 fixture (crates/kebab-cli/tests/fixtures/, crates/kebab-tui/tests/snapshots/ 등) — indexed_at 은 time-dependent 라 insta filter 로 [indexed_at] 마스킹.
Implementation steps (high-level)
- wire schema 파일 갱신 (
docs/wire-schema/v1/search_hit.schema.json,citation.schema.json) — required 두 필드 추가. kebab-configSearchConfig.stale_threshold_days신설 + env + 검증.kebab-store-sqliteretriever query 의 documents JOIN 확장 —updated_atSELECT.kebab-search내부 Hit struct 에indexed_at필드 운반.kebab-appfacade 의 wire DTO 변환에compute_stale호출. Clock 주입 인프라 신설 (없을 시).kebab-cliplain renderer +[stale]tag.kebab-tuiWarning Span 추가 + snapshot 갱신.- 모든 단위/통합 테스트 추가 + 기존 snapshot redaction 갱신.
- README 의 Configuration 섹션에
stale_threshold_days명시.docs/SMOKE.md시나리오 추가. - HOTFIXES.md 영향 없음 (frozen spec 변경 X).
Risks / notes
- Snapshot churn: 기존 search / ask snapshot 약 ~5개 재 record.
indexed_at마스킹 필터 표준화 필요. - Clock 주입: 코드베이스에
Clocktrait 가 이미 있는지 확인 — 없으면 facade-local trait 신설 (테스트 결정성 위함). Production path 는OffsetDateTime::now_utc단순 wrapper. - Off-by-one: boundary 가
>(초과) 인지>=(이상) 인지 —>채택 (정확히 30일째는 fresh). - citation.v1 stub: 현재 schema 가 stub 상태 (variant-discriminated validation 미구현). 두 필드 추가는 stub 수준에서도 required 명시 가능.
- Scope discipline: 자동 재 ingest / TUI 'r' 키 / file mtime 기반 stale 모두 후속 task. 본 spec 은 표시까지.
Documentation updates (implementation PR 동시)
README.md— Configuration 섹션의 config 예시 +stale_threshold_days한줄.docs/SMOKE.md— config 예시 갱신 + stale 시나리오 walkthrough 한 단락.tasks/p9/p9-fb-32-stale-doc-indicator.md—status: open → completed, design/plan 링크 추가.tasks/INDEX.md— fb-32 행 ✅ 표시 + 0.4.0 release 트리거 노트.integrations/claude-code/kebab/SKILL.md— wire 필드 추가 멘션 (parsing tip 한 줄).