2회차 리뷰가 1회차 지적 6건 모두 실질 해결을 확인하고(마이크로초 전환에
누락된 누적 지점 없음, 단위 1000배 오차 없음) 머지 가능으로 결론냈다.
남은 셋을 반영한다.
1) HOTFIXES 두 표의 모수가 정확히 2배 어긋났다 (MEDIUM)
A/B 표는 "문서 792건 / 16,379 청크", 내역 표는 "1,584건 / 32,758" 이었다.
원인을 추적해 보니 **측정 설정의 artifact** 였다. 시험용 config 를 dogfood
config 에서 sed 로 만들면서 `[[workspace.sources]]` 두 개(wiki / jira)의
root 가 같은 디렉토리를 가리키게 됐고, walker 가 파일 1,157개를 두 번
스캔해 run 하나가 자산 처리 1,584건을 낸다.
A/B 두 run 이 완전히 같은 설정을 쓰므로 비교 자체는 유효하지만, 이 PR 의
산출물이 "실측 근거" 이므로 무엇을 몇 건 쟀는지 정확히 적어야 한다.
코퍼스 / 저장 결과 / run 당 조회 수를 나눠 적고 artifact 를 명시했다.
상한 2.2초의 유도(0.6 + 1,584 × 1 ms)도 이제 모수와 맞는다.
2) emit 시점 ms 절삭이 계통적 하한으로 남아 있었다 (MEDIUM)
내부 누적만 마이크로초가 됐고 wire 필드는 여전히 내림이라, 저자 자신의
데이터대로면 90% 자산이 계속 0 으로 찍힌다. 소비자가 합산하면 자산 수 ×
최대 1 ms 만큼 계통적으로 과소 계상된다.
`div_ceil` 로 올림했다. 같은 run 을 다시 재니 0 으로 찍히는 자산이 하나도
없고 합이 2.1초다 — 내림 0.6초가 하한, 올림 2.1초가 상한이므로 앞서
산술로 낸 0.6~2.2초 구간이 실측으로 확인됐다.
3) cache_ms 라벨이 blake3 키 해싱을 빠뜨렸다 (MEDIUM)
`t_cache` 타이머는 `derivation_cache_key` 계산부터 시작한다. 청크 본문
전체를 해싱하는 순수 CPU 비용이라, "lookup" 만 적힌 라벨은 미스 위주
run 에서 실제로 오해를 만든다. 구조체 주석·필드 주석·스키마 셋 다 고쳤다.
4) 잔가지 (LOW)
- `t_decode` 주석이 "디코드" 라고만 해서 실제로는 히트/미스 분류 루프
전체를 감싼다는 점이 안 드러났다.
- `CacheStats` 의 hit / miss 필드에만 주석이 없었다.
- DOGFOOD 의 "warm 재색인이면 cache_miss == 0" 은 `--force-reingest`
일 때만 성립한다. 그냥 재색인하면 변경 없는 문서가 통째로 skip 되어
`asset_timings` 자체가 안 나온다.
미반영: `get_many` 의 `prepare_cached` 가 배치 크기마다 SQL 문자열이 달라져
사실상 캐시 미스라는 지적 — 정확하지만 누수도 정확성 문제도 없고, 버킷
패딩은 1% 짜리에 낼 복잡도가 아니다. tracing 로그가 us 라 자릿수가 길다는
점도 단위 표기와 값이 맞으므로 그대로 둔다.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017c9JwQq8ZkGvYjpKXMiDhF
242 lines
9.0 KiB
JSON
242 lines
9.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."
|
|
},
|
|
"cache_hit": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings: chunks whose embedding came from the derivation cache. Embeddings only — the OCR and caption derivations share the table but are not counted. Emitted on the markdown / image / PDF paths; the code path does not emit asset_timings at all."
|
|
},
|
|
"cache_miss": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings: chunks whose embedding had to be computed. Same scope as cache_hit."
|
|
},
|
|
"cache_ms": {
|
|
"type": "integer",
|
|
"minimum": 0,
|
|
"description": "asset_timings: what the derivation-cache path cost — cache-key hashing, lookup, payload decode, insert, and the last_used_at touch — excluding the embedder call the misses trigger. Rounded UP to the millisecond, because an asset's cache work is routinely sub-millisecond and truncating would make any sum a systematic undercount. INCLUDED IN embed_ms rather than additional to it: the embed timer spans the whole vector phase, so summing the phase fields double-counts this one."
|
|
}
|
|
}
|
|
}
|