spec(p9-fb-01..03): ingest progress events + cancellation in §2.4a / §10
도그푸딩 후 추가된 long-running 작업 진행 표시 + cancel 정책을 frozen design 에 명시. p9-fb-01/02/03 (ingest progress callback / CLI display / TUI background) 의 spec PR — impl PR 들이 이어진다. 변경: - docs/wire-schema/v1/ingest_progress.schema.json (신규): line-delimited streaming event schema. discriminated by `kind` (scan_started → scan_completed → asset_started → asset_finished* → embed_batch_* → completed | aborted). 마지막 줄은 기존 ingest_report.v1 그대로 (외부 wrapper backward-compat). - 2026-04-27-kebab-final-form-design.md §2.4a (신규): IngestProgressEvent 절. 이벤트 ordering / aborted 의 idempotency / CLI 의 stderr vs stdout 분리 / TUI · desktop 의 in-memory 소비. - 2026-04-27-kebab-final-form-design.md §10: long-running 작업 (ingest, future eval run, RAG streaming, embed batch) 의 두 invariant — progress 의 단일 source / cooperative cancel + step boundary. trait (§7.2) 시그니처는 무영향 — facade hidden parameter 로 추가. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -277,6 +277,27 @@ variant 별 해당 키만 채움. `path` 와 `uri` 는 항상 채움 (`uri` 는
|
||||
|
||||
`--summary-only` 시 `items: null`.
|
||||
|
||||
### 2.4a IngestProgressEvent
|
||||
|
||||
`kebab ingest --json` 가 long-running 작업의 진행을 line-delimited JSON 으로 흘려보낸다. 마지막 줄은 기존 `ingest_report.v1` 그대로 유지 (외부 wrapper backward-compat). 그 위로 N 개의 `ingest_progress.v1` 줄이 streaming. discriminated by `kind`:
|
||||
|
||||
```json
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "scan_started", "ts": "2026-05-02T18:30:00Z", "root": "/home/altair/KnowledgeBase" }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "scan_completed", "ts": "...", "total": 142 }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "asset_started", "ts": "...", "idx": 1, "total": 142, "path": "notes/foo.md", "media": "markdown" }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "embed_batch_started", "ts": "...", "n_chunks": 32 }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "embed_batch_finished", "ts": "...", "n_chunks": 32, "ms": 412 }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "asset_finished", "ts": "...", "idx": 1, "total": 142, "kind_result": "new", "chunks": 38 }
|
||||
{ "schema_version": "ingest_progress.v1", "kind": "completed", "ts": "...", "counts": { "scanned": 142, "new": 12, "updated": 3, "skipped": 127, "errors": 0, "chunks_indexed": 421, "embeddings_indexed": 421 } }
|
||||
```
|
||||
|
||||
**계약**:
|
||||
- 모든 ingest 실행은 정확히 한 번의 terminal 이벤트 (`completed` 또는 `aborted`) 로 종료.
|
||||
- 이벤트 ordering: `scan_started < scan_completed < (asset_started < asset_finished)* < (completed | aborted)`. embed batch 는 asset 이벤트 사이 임의 위치.
|
||||
- `aborted` 의 `counts` 는 cancel 시점까지의 부분 집계. SQLite 에 commit 된 doc/chunk 는 그대로 유지 — 다음 `kebab ingest` 가 idempotent 하게 이어받음.
|
||||
- non-`--json` 모드는 stderr 에 spinner + 사람-친화 라인 (구현 detail). `--json` 모드는 stderr 비움 + stdout 전부 line-delimited.
|
||||
- 같은 streaming surface 가 TUI / desktop UI 의 background ingest worker 도 소비 (in-memory mpsc, 와이어로 안 나감).
|
||||
|
||||
### 2.5 DocSummary (`kebab list docs`)
|
||||
|
||||
```json
|
||||
@@ -1299,6 +1320,17 @@ Refusal 은 에러 아님. `kebab ask` 거절은 정상 stdout (Answer with grou
|
||||
|
||||
Logging: `tracing` + `tracing-subscriber` + `tracing-appender` daily roll, `~/.local/state/kebab/logs/`. structured (`trace_id`, `doc_id`, `chunk_id`).
|
||||
|
||||
**Long-running 작업의 진행 표시 + cancel** (도그푸딩 후 추가 — 2026-05-02):
|
||||
|
||||
초 단위 이상 걸리는 모든 명령 (`kebab ingest`, future `kebab eval run`, RAG streaming, embed 배치) 은 다음 두 invariant 를 지킨다:
|
||||
|
||||
1. **진행 표시는 surface 별로 분리되되 source 는 단일.** facade (`kebab-app`) 가 progress event 를 `mpsc::Sender<IngestEvent>` (또는 그에 준하는 channel) 로 흘려보내고, CLI / TUI / desktop 이 각자 방식으로 소비. CLI 의 `--json` 모드는 §2.4a 의 line-delimited dump, 사람-친화 모드는 stderr spinner + 단계 라인. TUI 는 status bar 1 줄. desktop (P9-5) 는 progress widget.
|
||||
2. **cancel 은 cooperative + step boundary 에서 즉시 응답.** facade 가 `Option<Arc<AtomicBool>>` cancel token 받음. asset loop iteration / embed batch / vector upsert 같은 step boundary 마다 check, true 면 in-flight asset 마무리 후 `Aborted` event 발신 + `Ok(IngestReport)` 정상 반환 (Err 아님 — 정상 종료의 한 형태). 부분 commit 된 doc/chunk 는 SQLite 에 살아있어 재실행이 idempotent. CLI 는 SIGINT, TUI 는 `Esc` / `Ctrl-C` 가 cancel 신호.
|
||||
|
||||
`kebab-core` trait (§7.2) 시그니처는 무영향 — progress / cancel 은 `kebab-app` facade 의 hidden parameter 로 추가 (`ingest_with_config_progress(..., progress: Option<Sender<IngestEvent>>, cancel: Option<Arc<AtomicBool>>)`).
|
||||
|
||||
|
||||
|
||||
`kebab doctor` 출력 (사람):
|
||||
|
||||
```text
|
||||
|
||||
51
docs/wire-schema/v1/ingest_progress.schema.json
Normal file
51
docs/wire-schema/v1/ingest_progress.schema.json
Normal file
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||
"$id": "https://kb.local/wire/v1/ingest_progress.schema.json",
|
||||
"title": "IngestProgressEvent v1",
|
||||
"description": "Streaming progress event emitted by `kebab ingest --json`. One event per line (line-delimited JSON). Discriminated by `kind`. The terminal events are `completed` and `aborted` — every ingest run ends with exactly one of them. The final stdout line of a `--json` ingest is still the existing `ingest_report.v1` for backwards compatibility; progress events stream above it.",
|
||||
"type": "object",
|
||||
"required": ["schema_version", "kind", "ts"],
|
||||
"properties": {
|
||||
"schema_version": { "const": "ingest_progress.v1" },
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"scan_started",
|
||||
"scan_completed",
|
||||
"asset_started",
|
||||
"asset_finished",
|
||||
"embed_batch_started",
|
||||
"embed_batch_finished",
|
||||
"completed",
|
||||
"aborted"
|
||||
]
|
||||
},
|
||||
"ts": { "type": "string", "description": "RFC 3339 timestamp at the moment the event was emitted." },
|
||||
"root": { "type": "string", "description": "scan_started: workspace root being walked." },
|
||||
"total": { "type": "integer", "minimum": 0, "description": "scan_completed / asset_started / asset_finished: total assets discovered." },
|
||||
"idx": { "type": "integer", "minimum": 1, "description": "asset_started / asset_finished: 1-based index of the current asset within the scan." },
|
||||
"path": { "type": "string", "description": "asset_started: workspace-relative path of the asset being processed." },
|
||||
"media": { "type": "string", "description": "asset_started: media kind label (e.g. `markdown`, `pdf`, `image`)." },
|
||||
"kind_result": {
|
||||
"type": "string",
|
||||
"enum": ["new", "updated", "skipped", "error"],
|
||||
"description": "asset_finished: per-asset outcome (mirrors `ingest_report.v1.items[].kind`)."
|
||||
},
|
||||
"chunks": { "type": "integer", "minimum": 0, "description": "asset_finished: chunk count produced for this asset." },
|
||||
"n_chunks": { "type": "integer", "minimum": 0, "description": "embed_batch_started / embed_batch_finished: chunks in this embedding batch." },
|
||||
"ms": { "type": "integer", "minimum": 0, "description": "embed_batch_finished: wall-clock duration of the batch." },
|
||||
"counts": {
|
||||
"type": "object",
|
||||
"description": "completed / aborted: aggregate counters at the moment the run ended (mirrors fields on `ingest_report.v1`).",
|
||||
"properties": {
|
||||
"scanned": { "type": "integer", "minimum": 0 },
|
||||
"new": { "type": "integer", "minimum": 0 },
|
||||
"updated": { "type": "integer", "minimum": 0 },
|
||||
"skipped": { "type": "integer", "minimum": 0 },
|
||||
"errors": { "type": "integer", "minimum": 0 },
|
||||
"chunks_indexed": { "type": "integer", "minimum": 0 },
|
||||
"embeddings_indexed": { "type": "integer", "minimum": 0 }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user