feat(fb-27): introspection (kebab schema) + structured error wire #104
Reference in New Issue
Block a user
Delete Branch "feat/p9-fb-27-introspection-and-error-wire"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
요약
kebab schema [--json]명령 +schema.v1wire 도입 — agent 가 binary 의 wire 버전 / 기능 / 모델 / 인덱스 통계를 한 번의 호출로 introspect.error.v1wire 도입 —--json모드에서 fatal error 가 stderr ndjson 으로 emit. 비--json은 기존 stderr text 동작 보존.변경 사항
feat/p9-fb-27-introspection-and-error-wire17 commits, 28 files (+1258/-29).ConfigInvalid(kebab-config,causeprefixread_failed:/parse_failed:),NotIndexed(kebab-store-sqlite).SqliteStore::open_existing가OpenFlags::SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_URI로 silent CREATE 방지.kebab-app::error_signal(typed error re-export),kebab-app::schema(SchemaV1+schema_with_configfacade),kebab-cli::error_classify(classify(anyhow::Error) -> ErrorV1).kebab-app::SchemaV1:kebab_version(env! const) +wire.schemas(11 fully-qualified id) +capabilities(10 bool, 6 true / 4 future placeholder) +models(parser/chunker/embedding/prompt_template/index/corpus_revision 6축) +stats(doc/chunk/asset count + last_ingest_at).kebab-cli::wire:wire_schema(idempotent re-tag, mirrorswire_doctorsince SchemaV1 carries ownschema_version) +wire_error_v1(simpletag_object).kebab-cli::error_classify::classify: 7 code initial set —config_invalid/not_indexed/model_unreachable/model_not_pulled/timeout/io_error/generic. exit code 0/1/2/3 unchanged —RefusalSignal/NoHitSignal/DoctorUnhealthy만 1/1/3 결정.kebab_parse_md::PARSER_VERSION+kebab_store_vector::INDEX_VERSION_STRpub const노출 —kebab-app의 private duplicate literal 제거 (single source of truth).docs/wire-schema/v1/schema.schema.json+error.schema.json.테스트
cargo clippy --workspace --all-targets -- -D warningsclean.cargo test --workspace -j 1— fb-27 신규 테스트 모두 PASS. 2 reset.rs 실패 (enumerate_data_only_excludes_config_dir,enumerate_all_has_four_distinct_paths) 는 main 에서도 동일한 pre-existing env-dependent 실패 — fb-27 무관./tmp/kebab-fb27-final): ingest →kebab schema(text + JSON) → error wire (config_invalid) → legacy stderr text 모두 검증.알려진 한계 (deferred)
tasks/HOTFIXES.md의2026-05-07 — p9-fb-27항목이 source of truth.error.v1.detailsshape per code 가 design spec literal 과 일부 일탈 — 신규 typed signal 도입 전까지 interim. JSON Schema 의details는additionalProperties: true로 permissive. 후속 task:IoFailure/OpTimeout신규 signal 도입.Config::load(Some(/wrong))silent default fallback — agent 가 잘못된 path 로 호출 시 default 동작. fb-28 (--readonly/--quiet) 또는 별 follow-up 에서 strict mode 검토.LlmError::Stream/Malformed가code: "generic"fallback — 후속 task 에서stream_aborted/malformed_responsededicated code 도입 검토.Spec contract
docs/superpowers/specs/2026-04-27-kebab-final-form-design.md§10.1 capability matrix subsection 추가.tasks/p9/p9-fb-27-introspection-and-error-wire.mdstatusopen→completed, banner cross-link to HOTFIXES.Release trigger
0.3.0 minor bump 시점에 묶을 예정 (fb-26 ~ fb-31 0.3.0 agent foundation 그룹의 첫 component — wire additive only 라 본 PR 단독으로 release 의무 아님).
Replace `read failed: {e}` / `parse failed: {e}` with the underscore- slugged `read_failed:` / `parse_failed:` prefixes so kebab-cli's error_classify (Task 8) and the error.v1 JSON Schema (Task 14) can treat the prefix as a stable wire contract while leaving the OS / toml-crate detail in the suffix as free-form context. Also add the symmetric `cause` non-empty assertion to the malformed-TOML test so a regression that empties `cause` on the parse path would be caught. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>New `SqliteStore::open_existing` API + `NotIndexed` signal for the missing-DB case. kebab-app re-exports the type via its `error_signal` module so kebab-cli's `error_classify` can map it to `error.v1 { code: "not_indexed" }`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>Replace `path.exists()` + `Connection::open` (which silently CREATEs on race) with `Connection::open_with_flags` using READ_WRITE|URI but NOT CREATE. SQLite surfaces `SQLITE_CANTOPEN` for missing files; we wrap as NotIndexed { found: None } as before. Adds open_existing_does_not_create_missing_db regression test pinning the no-side-effect invariant. Also documents read-only intent on open_existing, the format contract on NotIndexed.found, and removes scaffolding comments from kebab-app error_signal that are no longer load-bearing. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>- SKILL.md: `streaming_ingest` → `streaming_ask`, `multi_turn` → `rag_multi_turn` (capability name mismatch flagged in final review — agents following the example literal would read non-existent fields). - HOTFIXES.md: add `not_indexed.details` to the interim wire shape deviations list — emit `{ expected, found }` only (spec literal `{ data_dir, expected, found }` not honored because NotIndexed signal carries one full path, not separate data_dir). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>회차 1 — fb-27 전반 직조 깔끔. 17 commit 의 의존 순서 (typed errors → error_signal → schema facade → CLI wire/classify → tests → docs/HOTFIXES) 가 자연스러움. spec/HOTFIXES 사이 cross-link 견고하고, interim wire shape deviation 도 솔직하게 명시. 머지 임박 — 다음 5 nit/follow-up 정리 후 회차 2 가능.
"schema.v1"literal 이 4 곳에 흩어짐 — const 한 군데로 모아 cascade 사고 줄이기.models.parser_version이 markdown 만 — PDF/image 빠진 사실 한 줄 주석.print_schema_text의"kebab v{}\n"+ 빈 println 이 빈 줄 두 개 — 다른 section 패턴과 일관 맞추기.127.0.0.1:150ms timeout) — 잠재 flaky, 가능하면 hermetic 변형 검토.open_existing의 RW flag + 주석 enforcement gap — 후속 PR 에서 RO 변형 분리 검토 (HOTFIXES 한 줄 추가만이라도).테스트 커버리지 양호, clippy 통과, manual smoke 5 시나리오 정상. backwards-compat 완전 (exit code 0/1/2/3 + 비 --json stderr text 보존). 0.3.0 agent foundation MVP 으로 적합.
@@ -0,0 +83,4 @@let stats = collect_stats(&store)?;let models = collect_models(cfg, &store);Ok(SchemaV1 {schema_version: "schema.v1".to_string(),[중복]
"schema.v1"literal 이 본 파일 line 86 +kebab-cli/src/wire.rsline 152 (tag_object) + line 147 (idempotent guard) + JSON Schema literal 4 곳에 동일하게 박혀 있음. 향후error.v1/schema.v2분기 시 모두 손대야 함.pub const SCHEMA_V1_ID: &str = "schema.v1"같은 const 하나 노출하고 wire.rs 가 그 const 를 import 해 비교/태그하면 누락 위험 사라짐. nit 이지만 cascade 규약 ($9) 정신과 같은 결.적어도 본 파일 안에서는
KEBAB_VERSIONconst 패턴 (line 55) 바로 옆에SCHEMA_VERSION도 두는 정도로도 가독성 + 변경점 단일화 효과.@@ -0,0 +131,4 @@fn collect_models(cfg: &Config, store: &kebab_store_sqlite::SqliteStore) -> Models {Models {parser_version: kebab_parse_md::PARSER_VERSION.to_string(),[부분 정보]
parser_version이 markdown 의kebab_parse_md::PARSER_VERSION만 surface — PDF (pdf-page-v1per p7-3 hotfix) 와 image (P6) 의 parser version 은 노출 안 됨. spec literal 도 single string 이라 본 PR 단독 결함은 아니지만, agent 가models.parser_version == "md-frontmatter-v2"만 보고 "이 binary 가 markdown 만 처리" 라고 오해할 여지가 있음.최소: line 137 위에
// markdown parser only — pdf/image extractors maintain their own versions; surface those when SchemaV1.models becomes a multi-medium map (P+).같은 주석 한 줄. 후속 task 의 spec brainstorm 시 single-string vs map 결정 필요.근거:
tasks/HOTFIXES.md의2026-05-02 — P7-3항목 —config.chunking.chunker_versionsingle 와 같은 모양의 deviation.@@ -0,0 +137,4 @@let client = reqwest::blocking::Client::builder().timeout(std::time::Duration::from_millis(50)).build().unwrap();let err = client.get("http://127.0.0.1:1").send().unwrap_err();[잠재적 flaky]
127.0.0.1:1connect-refused 에 50ms timeout — port 1 자체는 reserved 라 거의 모든 환경에서 즉시 ECONNREFUSED 반환되지만, 일부 macOS / SELinux / network namespace / 컨테이너 (host-network 안 쓰는 CI) 에서 timeout race 가능.LlmError::Unreachable매칭이 source 의 reqwest::Error 종류와 무관하게 enum discriminator 만 보고 분기되니, 다음과 같이 더 hermetic 하게 가능:reqwest::Error 가 private constructor 라 불가능하면 본 댓글 생략 OK. 수렴 실패 신호 발생하면
_common.sh옆에 cfg(test) 전용LlmError::Unreachable_TEST_FIXTUREvariant 도입 검토 가능. 본 PR 머지 차단 조건 아님 — 후속 cleanup 신호.@@ -720,2 +744,4 @@}fn print_schema_text(s: &kebab_app::SchemaV1) {println!("kebab v{}\n", s.kebab_version);[가독성 nit]
println!("kebab v{}\n", s.kebab_version);의\n은 println 이 자체 추가하는 newline 위에 한 줄 더 — 결과적으로 빈 줄 하나 + 첫 section 사이 또 빈 줄 (각 section 끝의println!()때문). 의도가 "버전 줄 + 빈 줄 + section" 이라면 OK 지만, 다른 section 들은 모두println!()한 번으로 끝내고 있어 일관성 깨짐.이 형태가 다른 section 의
"stats\n ...\n ..."+println!()패턴과 시각적으로 통일.@@ -62,0 +88,4 @@pub fn open_existing(path: &std::path::Path) -> anyhow::Result<Self> {let conn = Connection::open_with_flags(path,OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_URI,[footgun risk]
SQLITE_OPEN_READ_WRITEflag 인데 doc (line 81-82) 가 "callers should not issue mutations through this connection" 명시. 컴파일러가 막아주지 않음 — 후속 maintainer 가 schema_with_config 안에서 stats 외 조회를 추가하다가 무심코 mutation 을 끼우면 silent하게 통과.apply_pragmas가journal_mode = WAL호출하기 때문에 RW 가 강제된다고 doc 에 적혀 있는데, 실제로는:open시점에 WAL 로 옮겨짐). 후속 connection 이 다시journal_mode = WAL호출해도 결과는 동일.SQLITE_OPEN_READ_ONLY로 열고apply_pragmas의 WAL 라인 skip 하는 read-only 변형 (apply_read_pragmas?) 으로 분리하면 enforcement 가능.현재 PR 머지 차단 아님 —
schema_with_config가 read-only 라 동작은 정확. follow-up task 로 read-only 변형 검토 권장. 본 PR 의open_existing는 그대로 두고,apply_pragmas분리만 추후 PR.HOTFIXES 의 "Known limitation" 절 끝에 한 줄 추가도 가능.
회차 2 — 회차 1 의 5 nit 모두 정확히 반영. 추가 actionable 없음.
확인:
SCHEMA_V1_IDconst 가 schema.rs 에 신설 + lib.rs 에 re-export + wire.rs 의 두 literal 모두 import 사용 — single source of truth 확보.collect_models::parser_version위에 markdown-only 한계 주석 3 줄 (P+ 의 multi-medium map 발전 경로 명시).print_schema_text헤더 줄이 다른 section 패턴 (println!()별도) 과 일관.llm_unreachable_classifiestimeout 50ms → 500ms (10x headroom) + 5 줄 접근 방식 / 한계 설명 주석.open_existingRW flag enforcement 갭 항목 한 줄 추가.테스트 모두 통과 (kebab-app schema_report 2/2, kebab-cli wire 8/8 + error_classify 7/7), clippy clean. 머지 가능.