feat(fb-27): introspection (kebab schema) + structured error wire #104

Merged
altair823 merged 18 commits from feat/p9-fb-27-introspection-and-error-wire into main 2026-05-07 04:19:43 +00:00
Owner

요약

  • kebab schema [--json] 명령 + schema.v1 wire 도입 — agent 가 binary 의 wire 버전 / 기능 / 모델 / 인덱스 통계를 한 번의 호출로 introspect.
  • error.v1 wire 도입 — --json 모드에서 fatal error 가 stderr ndjson 으로 emit. 비 --json 은 기존 stderr text 동작 보존.
  • agent foundation MVP (fb-30 MCP + fb-29 daemon prerequisite). 0.3.0 release 첫 component.

변경 사항

feat/p9-fb-27-introspection-and-error-wire 17 commits, 28 files (+1258/-29).

  • 새 typed errors: ConfigInvalid (kebab-config, cause prefix read_failed: / parse_failed:), NotIndexed (kebab-store-sqlite). SqliteStore::open_existingOpenFlags::SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_URI 로 silent CREATE 방지.
  • 새 modules: kebab-app::error_signal (typed error re-export), kebab-app::schema (SchemaV1 + schema_with_config facade), 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, mirrors wire_doctor since SchemaV1 carries own schema_version) + wire_error_v1 (simple tag_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 결정.
  • Cascade rule cleanup: kebab_parse_md::PARSER_VERSION + kebab_store_vector::INDEX_VERSION_STR pub const 노출 — kebab-app 의 private duplicate literal 제거 (single source of truth).
  • 새 wire schemas: docs/wire-schema/v1/schema.schema.json + error.schema.json.

테스트

  • 신규 ~20 테스트 (unit + integration) all green.
  • cargo clippy --workspace --all-targets -- -D warnings clean.
  • 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 무관.
  • Manual smoke (/tmp/kebab-fb27-final): ingest → kebab schema (text + JSON) → error wire (config_invalid) → legacy stderr text 모두 검증.

알려진 한계 (deferred)

tasks/HOTFIXES.md2026-05-07 — p9-fb-27 항목이 source of truth.

  • error.v1.details shape per code 가 design spec literal 과 일부 일탈 — 신규 typed signal 도입 전까지 interim. JSON Schema 의 detailsadditionalProperties: 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 / Malformedcode: "generic" fallback — 후속 task 에서 stream_aborted / malformed_response dedicated code 도입 검토.

Spec contract

  • design docs/superpowers/specs/2026-04-27-kebab-final-form-design.md §10.1 capability matrix subsection 추가.
  • task spec tasks/p9/p9-fb-27-introspection-and-error-wire.md status opencompleted, 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 의무 아님).

## 요약 - `kebab schema [--json]` 명령 + `schema.v1` wire 도입 — agent 가 binary 의 wire 버전 / 기능 / 모델 / 인덱스 통계를 한 번의 호출로 introspect. - `error.v1` wire 도입 — `--json` 모드에서 fatal error 가 stderr ndjson 으로 emit. 비 `--json` 은 기존 stderr text 동작 보존. - agent foundation MVP (fb-30 MCP + fb-29 daemon prerequisite). 0.3.0 release 첫 component. ## 변경 사항 `feat/p9-fb-27-introspection-and-error-wire` 17 commits, 28 files (+1258/-29). - **새 typed errors**: `ConfigInvalid` (kebab-config, `cause` prefix `read_failed:` / `parse_failed:`), `NotIndexed` (kebab-store-sqlite). `SqliteStore::open_existing` 가 `OpenFlags::SQLITE_OPEN_READ_WRITE | SQLITE_OPEN_URI` 로 silent CREATE 방지. - **새 modules**: `kebab-app::error_signal` (typed error re-export), `kebab-app::schema` (`SchemaV1` + `schema_with_config` facade), `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, mirrors `wire_doctor` since SchemaV1 carries own `schema_version`) + `wire_error_v1` (simple `tag_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 결정. - **Cascade rule cleanup**: `kebab_parse_md::PARSER_VERSION` + `kebab_store_vector::INDEX_VERSION_STR` `pub const` 노출 — `kebab-app` 의 private duplicate literal 제거 (single source of truth). - **새 wire schemas**: `docs/wire-schema/v1/schema.schema.json` + `error.schema.json`. ## 테스트 - 신규 ~20 테스트 (unit + integration) all green. - `cargo clippy --workspace --all-targets -- -D warnings` clean. - `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 무관. - Manual smoke (`/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.details` shape 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_response` dedicated code 도입 검토. ## Spec contract - design `docs/superpowers/specs/2026-04-27-kebab-final-form-design.md` §10.1 capability matrix subsection 추가. - task spec `tasks/p9/p9-fb-27-introspection-and-error-wire.md` status `open` → `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 의무 아님).
altair823 added 17 commits 2026-05-07 04:10:48 +00:00
Re-exports existing doctor_signal entries (RefusalSignal / NoHitSignal /
DoctorUnhealthy) + LlmError from kebab-llm-local. ConfigInvalid /
NotIndexed re-exports added in subsequent tasks once the source crates
define them.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps every error path in `Config::from_file` (read failure, TOML parse,
validation) so downstream callers can `downcast_ref::<ConfigInvalid>()`
to build the `error.v1` wire record. kebab-app re-exports the type via
its `error_signal` module.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
New `SchemaV1` struct + `schema_with_config(&Config)` builder. Surfaces
wire schemas list, capabilities (current + future placeholders), model
versions (parser/chunker/embedding/prompt_template/index/corpus_revision),
and stats (doc/chunk/asset counts + last ingest). kebab-store-sqlite
gains `count_summary()` to back the stats block.

Deviations from plan:
- `cfg.models.embedding.id` → `cfg.models.embedding.model` (actual field name)
- No `Config::expand_path` method → free fn `kebab_config::expand_path(&cfg.storage.data_dir, "")`
- `PARSER_VERSION` added to `kebab-parse-md/src/lib.rs` (was absent; synced with `KEBAB_PARSE_MD_VERSION` literal in kebab-app)
- `INDEX_VERSION_STR` added to `kebab-store-vector/src/store.rs` + re-exported from `lib.rs` (was a private `const`)
- `corpus_revision()` returns `u64` directly (not `Result<u64>`) — no `?` in collect_models
- `SchemaV1` carries `schema_version: "schema.v1"` field (wire schema convention)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace kebab-app's private `KEBAB_PARSE_MD_VERSION` literal with a
direct reference to `kebab_parse_md::PARSER_VERSION` so the parser
version cascade has a single source of truth (design §9 invariant).

Add maintenance comment on schema.rs WIRE_SCHEMAS const pointing to
docs/wire-schema/v1/ + kebab-cli wire helpers as the authoritative
sources to keep in sync.

Tighten open_existing doc comment to match the actual SQLITE_OPEN_READ_WRITE
flag (needed for WAL pragma application) — callers should still avoid
issuing mutations through this connection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two scenarios: freshly-ingested 2-doc KB (stats reflect counts +
last_ingest_at populated) and empty-but-initialized KB (counts zero,
last_ingest_at None). The empty case runs ingest_with_config over an
empty workspace dir to seed kebab.sqlite before calling schema_with_config,
since open_existing (used internally) returns NotIndexed if the DB is absent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plug coverage hole flagged in code review — test 1 was asserting only
doc_count + last_ingest_at, leaving count_summary's chunk_count and
asset_count queries un-pinned.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
`error_classify::classify` maps anyhow::Error → ErrorV1 wire record by
downcasting to known typed errors (LlmError + ConfigInvalid + NotIndexed
re-exported from kebab_app::error_signal, plus std::io::Error chain).
Generic fallback emits `code: "generic"` with the chain in `details` when
verbose.

wire.rs adds wire_schema (idempotent re-tag, mirrors wire_doctor pattern
since SchemaV1 carries its own schema_version field) and wire_error_v1
(simple tag_object). Tests pin both wrappers + 7 classify code paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Text mode: doctor-style key/value layout. JSON mode: schema.v1 wire
record. Honors `--config <path>` via the established
`kebab_app::schema_with_config(&cfg)` facade pattern (per the P3-5 /
P4-3 regression conventions).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps the existing `Err(e)` arm with a `cli.json` branch:
- `--json`: stderr ndjson `error.v1` via wire_error_v1
- non-`--json`: legacy `error: <msg>` text path (unchanged)

exit_code() unchanged — RefusalSignal/NoHitSignal/DoctorUnhealthy
still drive 1/1/3.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
cli_schema: exercises `kebab schema` (text + --json) on a fresh-but-init'd
KB. Pins schema_version, kebab_version non-empty, capabilities.json_mode
true, capabilities.mcp_server false (future placeholder).

cli_error_wire: spawns `kebab --json --config <malformed.toml> ingest`
and verifies stderr emits a single error.v1 ndjson line with
code == "config_invalid". Non-JSON mode regression-pinned to keep the
legacy `error:` prefix. Note: --config /nonexistent silently falls back
to defaults (by design); a file that exists but fails TOML parsing is
the reliable trigger for config_invalid.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
schema.v1: full introspection report shape with required fields for
wire / capabilities / models / stats. capabilities object enumerates
all 10 flag names (current 6 true + future 4 false) as required keys.

error.v1: 7-code enum + permissive details object. Real emitted
details shapes documented in description (per-code context varies and
some fields are interim until IoFailure / OpTimeout typed signals
land in follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- README 명령 표 에 `kebab schema` 추가
- HANDOFF post-도그푸딩 항목 한 줄
- CLAUDE.md wire schema 절 schema.v1 / error.v1 추가
- integrations skill — schema 활용 안내 (additive)
- design §10.1 capability matrix subsection 신설

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
HOTFIXES 항목이 fb-27 의 live binding 변경 + interim wire shape
deviation 의 source of truth (error.v1.details 가 신규 typed signal
도입 전까지 spec literal 과 일부 일탈).

spec 상단 banner 와 frontmatter status 가 frozen 상태 + post-merge
HOTFIXES cross-link 으로 갱신.

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>
claude-reviewer-01 requested changes 2026-05-07 04:14:16 +00:00
Dismissed
claude-reviewer-01 left a comment
Member

회차 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 가능.

  1. "schema.v1" literal 이 4 곳에 흩어짐 — const 한 군데로 모아 cascade 사고 줄이기.
  2. models.parser_version 이 markdown 만 — PDF/image 빠진 사실 한 줄 주석.
  3. print_schema_text"kebab v{}\n" + 빈 println 이 빈 줄 두 개 — 다른 section 패턴과 일관 맞추기.
  4. 네트워크 의존 unit test (127.0.0.1:1 50ms timeout) — 잠재 flaky, 가능하면 hermetic 변형 검토.
  5. 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 으로 적합.

회차 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 가능. 1. `"schema.v1"` literal 이 4 곳에 흩어짐 — const 한 군데로 모아 cascade 사고 줄이기. 2. `models.parser_version` 이 markdown 만 — PDF/image 빠진 사실 한 줄 주석. 3. `print_schema_text` 의 `"kebab v{}\n"` + 빈 println 이 빈 줄 두 개 — 다른 section 패턴과 일관 맞추기. 4. 네트워크 의존 unit test (`127.0.0.1:1` 50ms timeout) — 잠재 flaky, 가능하면 hermetic 변형 검토. 5. `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.rs line 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_VERSION const 패턴 (line 55) 바로 옆에 SCHEMA_VERSION 도 두는 정도로도 가독성 + 변경점 단일화 효과.

**[중복]** `"schema.v1"` literal 이 본 파일 line 86 + `kebab-cli/src/wire.rs` line 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_VERSION` const 패턴 (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-v1 per 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.md2026-05-02 — P7-3 항목 — config.chunking.chunker_version single 와 같은 모양의 deviation.

**[부분 정보]** `parser_version` 이 markdown 의 `kebab_parse_md::PARSER_VERSION` 만 surface — PDF (`pdf-page-v1` per 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_version` single 와 같은 모양의 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:1 connect-refused 에 50ms timeout — port 1 자체는 reserved 라 거의 모든 환경에서 즉시 ECONNREFUSED 반환되지만, 일부 macOS / SELinux / network namespace / 컨테이너 (host-network 안 쓰는 CI) 에서 timeout race 가능. LlmError::Unreachable 매칭이 source 의 reqwest::Error 종류와 무관하게 enum discriminator 만 보고 분기되니, 다음과 같이 더 hermetic 하게 가능:

// LlmError::Stream 은 String body 만 받아서 reqwest::Error 불필요
let llm = LlmError::Unreachable {
    endpoint: "http://test.invalid:0".to_string(),
    source: <offline-constructible reqwest::Error>,
};

reqwest::Error 가 private constructor 라 불가능하면 본 댓글 생략 OK. 수렴 실패 신호 발생하면 _common.sh 옆에 cfg(test) 전용 LlmError::Unreachable_TEST_FIXTURE variant 도입 검토 가능. 본 PR 머지 차단 조건 아님 — 후속 cleanup 신호.

**[잠재적 flaky]** `127.0.0.1:1` connect-refused 에 50ms timeout — port 1 자체는 reserved 라 거의 모든 환경에서 즉시 ECONNREFUSED 반환되지만, 일부 macOS / SELinux / network namespace / 컨테이너 (host-network 안 쓰는 CI) 에서 timeout race 가능. `LlmError::Unreachable` 매칭이 source 의 reqwest::Error 종류와 무관하게 enum discriminator 만 보고 분기되니, 다음과 같이 더 hermetic 하게 가능: ```rust // LlmError::Stream 은 String body 만 받아서 reqwest::Error 불필요 let llm = LlmError::Unreachable { endpoint: "http://test.invalid:0".to_string(), source: <offline-constructible reqwest::Error>, }; ``` reqwest::Error 가 private constructor 라 불가능하면 본 댓글 생략 OK. 수렴 실패 신호 발생하면 `_common.sh` 옆에 cfg(test) 전용 `LlmError::Unreachable_TEST_FIXTURE` variant 도입 검토 가능. 본 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!() 한 번으로 끝내고 있어 일관성 깨짐.

println!("kebab v{}", s.kebab_version);
println!();

이 형태가 다른 section 의 "stats\n ...\n ..." + println!() 패턴과 시각적으로 통일.

**[가독성 nit]** `println!("kebab v{}\n", s.kebab_version);` 의 `\n` 은 println 이 자체 추가하는 newline 위에 한 줄 더 — 결과적으로 빈 줄 하나 + 첫 section 사이 또 빈 줄 (각 section 끝의 `println!()` 때문). 의도가 "버전 줄 + 빈 줄 + section" 이라면 OK 지만, 다른 section 들은 모두 `println!()` 한 번으로 끝내고 있어 일관성 깨짐. ```rust println!("kebab v{}", s.kebab_version); 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_WRITE flag 인데 doc (line 81-82) 가 "callers should not issue mutations through this connection" 명시. 컴파일러가 막아주지 않음 — 후속 maintainer 가 schema_with_config 안에서 stats 외 조회를 추가하다가 무심코 mutation 을 끼우면 silent하게 통과.

apply_pragmasjournal_mode = WAL 호출하기 때문에 RW 가 강제된다고 doc 에 적혀 있는데, 실제로는:

  • WAL mode 는 DB 파일 헤더에 영속 (이미 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" 절 끝에 한 줄 추가도 가능.

**[footgun risk]** `SQLITE_OPEN_READ_WRITE` flag 인데 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 에 적혀 있는데, 실제로는: - WAL mode 는 DB 파일 헤더에 영속 (이미 `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" 절 끝에 한 줄 추가도 가능.
altair823 added 1 commit 2026-05-07 04:17:58 +00:00
- schema.rs: extract `SCHEMA_V1_ID` const + re-export via kebab-app::lib.rs.
  wire.rs::wire_schema 의 2 literal 도 import 해서 single source of truth.
- schema.rs::collect_models: parser_version 가 markdown 만 surface 함을
  주석으로 명시 (PDF/image extractor 의 자체 version 은 SchemaV1.models 가
  multi-medium map 으로 진화 시 surface).
- main.rs::print_schema_text: 헤더 줄 끝의 `\n` 제거 + `println!()` 추가 —
  다른 section 들과 패턴 일관.
- error_classify.rs::llm_unreachable_classifies: timeout 50ms → 500ms (10x
  headroom) + 접근 방식 + 한계 주석 추가.
- HOTFIXES: open_existing 의 RW flag + 주석-only enforcement 갭을
  Known-limitation 에 명시.

Round 1 review summary: #104 (comment)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
claude-reviewer-01 approved these changes 2026-05-07 04:18:59 +00:00
claude-reviewer-01 left a comment
Member

회차 2 — 회차 1 의 5 nit 모두 정확히 반영. 추가 actionable 없음.

확인:

  1. SCHEMA_V1_ID const 가 schema.rs 에 신설 + lib.rs 에 re-export + wire.rs 의 두 literal 모두 import 사용 — single source of truth 확보.
  2. collect_models::parser_version 위에 markdown-only 한계 주석 3 줄 (P+ 의 multi-medium map 발전 경로 명시).
  3. print_schema_text 헤더 줄이 다른 section 패턴 (println!() 별도) 과 일관.
  4. llm_unreachable_classifies timeout 50ms → 500ms (10x headroom) + 5 줄 접근 방식 / 한계 설명 주석.
  5. HOTFIXES 의 fb-27 known-limitation 절 마지막에 open_existing RW flag enforcement 갭 항목 한 줄 추가.

테스트 모두 통과 (kebab-app schema_report 2/2, kebab-cli wire 8/8 + error_classify 7/7), clippy clean. 머지 가능.

회차 2 — 회차 1 의 5 nit 모두 정확히 반영. 추가 actionable 없음. 확인: 1. `SCHEMA_V1_ID` const 가 schema.rs 에 신설 + lib.rs 에 re-export + wire.rs 의 두 literal 모두 import 사용 — single source of truth 확보. 2. `collect_models::parser_version` 위에 markdown-only 한계 주석 3 줄 (P+ 의 multi-medium map 발전 경로 명시). 3. `print_schema_text` 헤더 줄이 다른 section 패턴 (`println!()` 별도) 과 일관. 4. `llm_unreachable_classifies` timeout 50ms → 500ms (10x headroom) + 5 줄 접근 방식 / 한계 설명 주석. 5. HOTFIXES 의 fb-27 known-limitation 절 마지막에 `open_existing` RW flag enforcement 갭 항목 한 줄 추가. 테스트 모두 통과 (kebab-app schema_report 2/2, kebab-cli wire 8/8 + error_classify 7/7), clippy clean. 머지 가능.
altair823 merged commit c732189eb3 into main 2026-05-07 04:19:43 +00:00
altair823 deleted branch feat/p9-fb-27-introspection-and-error-wire 2026-05-07 04:19:44 +00:00
Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: altair823-org/kebab#104