리뷰 두 건에서 나온 지적을 반영한다.
1) sweep 이 끝나도 asset 진행바가 복구되지 않았다 (HIGH)
sweep 은 asset 진행바를 빌려 쓰면서 자기 라벨과 자기(더 작은) 총계를
씌운다. 그런데 `AssetStarted` 는 위치와 메시지만 세팅하고 길이·스타일은
건드리지 않는다. 그래서 sweep 이 한 번 돌면 그 뒤 색인 구간 전체가
`sweep [====] 4213/21` 로 그려졌다 — 라벨도 분모도 틀린다.
더 나쁜 건 스타일 교체가 v0.26.1 의 커스텀 키 `{asset_elapsed}` 를 같이
날린다는 점이다. 느린 asset 에서 `(Ns)` 가 도는 게 "멈춘 게 아님"의 유일한
신호인데 sweep 이 그걸 없앤다. 이 PR 이 sweep 구간에서 없앤 "hang 처럼
보임" 을 asset 구간에 새로 만드는 셈이었다.
바 세팅을 `dress_bar_for_assets` 로 빼고 `ScanCompleted` 와
`SweepCompleted` 양쪽에서 부른다. 스타일을 길이보다 먼저 세팅하는데,
indicatif 가 두 호출 사이에 다시 그릴 수 있어 과도기 프레임이 최소한
올바른 라벨을 달게 하기 위해서다.
TTY 전용이라 비-TTY 실측만 보고 있어서 놓쳤다. pty 로 재현해 고친 뒤
`ingest [====] 16/17 doc9.md` 로 나오는 것을 확인했다.
2) docs/DOGFOOD.md §1.8 이 갱신되지 않았다 (MEDIUM)
`--json` 이벤트 순서 목록에 sweep 3종이 빠져 있었다. verify 항목에
sweep 분모·연속성과 "sweep 뒤 진행바가 asset 분모로 돌아오는가" 를 넣었다
— 1번이 정확히 그 항목이 있었으면 잡혔을 결함이다.
3) ingest_progress.rs 의 ordering invariant 주석이 stale 했다 (MEDIUM)
§2.4a 순서 블록에 sweep 이 없었다. 설계 문서 자체는 frozen baseline 이고
HOTFIXES dated entry 가 있으니 규약상 문제없지만 코드 주석은 living 이다.
4) 잔가지 (LOW)
- purge 실패가 "디스크에 남아 있어 그냥 뒀다" 와 구분되지 않았다. 둘 다
`removed: false` 이고 ndjson 에는 아무것도 안 남아,
`sweep_summary` 의 `checked - purged` 차이로도 못 가른다. 사후 기록이
로그뿐이라는 게 이 PR 의 전제이므로 `purge_failed` 를 추가했다.
- `SweepProgress` 가 purge 할 때만 바 메시지를 세팅하고 비우지 않아,
마지막 purge 경로가 이후 후보를 훑는 내내 남았다. 안 지울 때는 비운다.
- 주석과 스키마가 신규 이벤트를 `v0.33.0` 이라고 적었는데, CLAUDE.md 의
bump 규칙상 이 변경은 patch 다 (additive-only wire + 관측성 개선, 새
명령·플래그·config 없음, 검색·색인 결과 불변 — 선례가 asset_phase).
`v0.32.1` 로 고쳤다.
미반영: `SweepCompleted` 가 TTY 에서 `bar.println` 대신 stderr 에 직접
쓰는 것 — 기존 `AssetTimings`/`PdfOcr*` 이 같은 패턴이라 신규 회귀가 아니고,
바꾸려면 그 셋을 같이 옮겨야 해서 별 건이다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
227 lines
8.0 KiB
JSON
227 lines
8.0 KiB
JSON
{
|
|
"$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. `sweep_*` events (v0.32.1) cover the deleted-file sweep that runs between the scan and the asset loop; before them that phase emitted nothing and a long sweep was indistinguishable from a hang (issue #228).",
|
|
"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",
|
|
"asset_chunked",
|
|
"asset_phase",
|
|
"asset_timings",
|
|
"embed_batch_started",
|
|
"embed_batch_finished",
|
|
"pdf_ocr_started",
|
|
"pdf_ocr_finished",
|
|
"sweep_started",
|
|
"sweep_progress",
|
|
"sweep_completed",
|
|
"completed",
|
|
"aborted"
|
|
]
|
|
},
|
|
"ts": {
|
|
"type": "string",
|
|
"format": "date-time",
|
|
"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. sweep_started / sweep_progress: total stored paths the deleted-file sweep will examine (the store's paths minus the ones this scan covered)."
|
|
},
|
|
"idx": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "asset_started / asset_finished: 1-based index of the current asset within the scan. sweep_progress: 1-based index of the current sweep candidate."
|
|
},
|
|
"path": {
|
|
"type": "string",
|
|
"description": "asset_started: workspace-relative path of the asset being processed. sweep_progress: workspace path of the candidate examined."
|
|
},
|
|
"media": {
|
|
"type": "string",
|
|
"description": "asset_started: media kind label (e.g. `markdown`, `pdf`, `image`)."
|
|
},
|
|
"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 / asset_chunked (v0.24.0): chunk count produced for this asset."
|
|
},
|
|
"phase": {
|
|
"type": "string",
|
|
"enum": [
|
|
"ocr",
|
|
"caption",
|
|
"embed"
|
|
],
|
|
"description": "asset_phase (v0.26.1): the slow internal phase the asset just entered. Short phases (parse/chunk/store) are not emitted."
|
|
},
|
|
"model": {
|
|
"type": [
|
|
"string",
|
|
"null"
|
|
],
|
|
"description": "asset_phase (v0.26.1): model performing the phase — vision LLM id for ocr/caption, embedder model_id for embed. null when the phase runs without a configured model."
|
|
},
|
|
"parse_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.24.0, additive): parse phase wall-clock (ms). Emitted by markdown / image / PDF paths."
|
|
},
|
|
"chunk_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.24.0, additive): chunk phase wall-clock (ms). Emitted by markdown / image / PDF paths."
|
|
},
|
|
"expansion_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.24.0, additive): retained for wire compatibility but always 0 — doc-side expansion was removed (HOTFIXES 2026-06-03)."
|
|
},
|
|
"embed_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.24.0, additive): embed + vector phase wall-clock (ms) — embedding, vector upsert, and stale-vector purge."
|
|
},
|
|
"store_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.24.0, additive): SQLite persist phase wall-clock (ms) — put_asset/document/blocks/chunks only."
|
|
},
|
|
"ocr_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.26.1, additive, default 0): image/PDF OCR phase wall-clock (ms). 0 on the markdown path (no OCR)."
|
|
},
|
|
"caption_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings (v0.26.1, additive, default 0): image caption phase wall-clock (ms). 0 on markdown / PDF paths."
|
|
},
|
|
"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 / pdf_ocr_finished: wall-clock duration (ms). pdf_ocr_finished skip path 의 의미는 mixed (DCTDecode 부재 시 0, engine 실패 시 latency-before-bail). sweep_completed: wall-clock of the whole sweep phase."
|
|
},
|
|
"chars": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "pdf_ocr_finished: char count of OCR result. Skip 시 0."
|
|
},
|
|
"page": {
|
|
"type": "integer",
|
|
"minimum": 1,
|
|
"description": "pdf_ocr_started / pdf_ocr_finished: 1-based PDF page number under OCR."
|
|
},
|
|
"ocr_engine": {
|
|
"type": "string",
|
|
"description": "pdf_ocr_finished: engine_name (e.g. 'ollama-vision')."
|
|
},
|
|
"skipped": {
|
|
"type": "boolean",
|
|
"description": "pdf_ocr_finished: true 일 시 OCR 미수행 (DCTDecode 부재 또는 engine 실패). chars=0 만으로는 skip 과 0-char result 구분 불가."
|
|
},
|
|
"image_byte_size": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "pdf_ocr_finished (optional, v0.20.x): raster image byte size."
|
|
},
|
|
"image_width": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "pdf_ocr_finished (optional, v0.20.x): raster image width px."
|
|
},
|
|
"image_height": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "pdf_ocr_finished (optional, v0.20.x): raster image height px."
|
|
},
|
|
"failure_reason": {
|
|
"type": "string",
|
|
"description": "pdf_ocr_finished (optional, v0.20.x): OCR failure reason. Present iff skipped=true due to engine error. Values: timeout | ocr_error | network_error | other."
|
|
},
|
|
"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
|
|
}
|
|
}
|
|
},
|
|
"removed": {
|
|
"type": "boolean",
|
|
"description": "sweep_progress: true when the file was truly absent and its document was purged; false when it is still on disk (left untouched) or the purge failed."
|
|
},
|
|
"checked": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "sweep_completed: sweep candidates examined."
|
|
},
|
|
"purged": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "sweep_completed: documents removed because their source file is gone."
|
|
}
|
|
}
|
|
}
|