1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
//! Cache-key version tag folded into every on-disk cache key.
//!
//! A cached extraction result is only valid for the crate version, cache
//! schema, and *build* that produced it. Neither the content hash (bytes of
//! the input) nor the config hash (the `ExtractionConfig`) changes when
//! *extraction behaviour* changes — a fixed extractor, a reordered pipeline
//! stage, a new post-processor — so without this tag an entry written before
//! the fix is served forever, and the bug it encodes outlives its own fix.
//!
//! The tag mixes in four things:
//! - `CARGO_PKG_VERSION` — the crate version.
//! - [`CACHE_SCHEMA_VERSION`] — bumped by hand when a behaviour change is
//! deliberately recognised as cache-affecting (see its own doc comment).
//! - `XBERG_BUILD_ID` — a best-effort per-build identifier baked in by
//! `build.rs` (see that file's `build_id` for the full derivation and its
//! git-SHA / dirty-tree / env-override precedence, and for the cases —
//! Docker builds, crates.io/docs.rs tarball builds — where it is `""`).
//! This is the safety net *under* [`CACHE_SCHEMA_VERSION`], not a
//! replacement for it: most behaviour changes are never recognised as
//! cache-affecting and so never get a manual schema bump, but they usually
//! do land as a distinct commit, which `XBERG_BUILD_ID` catches without
//! anyone having to remember.
//! - The debug/release profile bit (`cfg!(debug_assertions)`) — a debug and
//! a release binary built from the identical commit must not silently
//! share entries if they ever diverge in output; the bit costs nothing to
//! include even when they don't.
//!
//! When `XBERG_BUILD_ID` is `""` (no git metadata and no override reached
//! the build), that component stops distinguishing anything: the tag's only
//! remaining discriminating power is crate version + schema version, plus
//! the profile bit above (new here regardless of `XBERG_BUILD_ID`) — the
//! same discriminating power the tag had before `XBERG_BUILD_ID` existed.
//! Note this changes the tag's *value*, not just what it discriminates on:
//! deploying this change invalidates every entry already on disk, the same
//! one-time effect as a [`CACHE_SCHEMA_VERSION`] bump.
//!
//! Every cache key is therefore prefixed with this tag. Changing any of the
//! four inputs makes all previously written entries unreachable; they age
//! out through the normal cleanup pass.
/// Generation counter for cached extraction results.
///
/// Bump this whenever extraction output can change for an unchanged input and
/// an unchanged `ExtractionConfig` — that is, whenever a fix or a behaviour
/// change would otherwise be masked by an entry written before it landed.
/// Bumping it invalidates every existing cache entry process-wide.
/// Bumped for #687: the OCR cache key did not cover the Tesseract engine variables
/// `apply_tesseract_variables` applies (`crates/xberg/src/ocr/processor/config.rs`), so
/// entries written before `hocr_font_info` was enabled in 57e414a6db kept being served
/// after it landed, serving stale font-size data. `hash_config` now folds those variables
/// into its own hash, but this bump is still needed to invalidate the entries that were
/// already on disk before that fix.
///
/// Bumped for #783: `hocr_parser::parse_hocr_to_internal_document_with_dictionary_filter`
/// now drops per-line dictionary-invalid noise from the `InternalDocument`/`content` a
/// Tesseract "markdown"-format OCR call produces, for an unchanged image hash and config
/// hash (`hash_config` never covered this -- it is not a Tesseract engine variable, just a
/// call-site decision in `ocr::processor::execution::perform_ocr`). A prior attempt at
/// this exact fix (reverted as `29738a1f29`) was measured byte-identical with its new
/// threshold on vs. off. Its confirmed cause was representation drift -- it stripped only
/// `page_texts` while rendering reads paragraphs -- not this constant. But that commit
/// also failed to bump it, so a stale entry would have hidden a working fix just as
/// effectively, and the two are indistinguishable after the fact. Any A/B measuring this
/// fix MUST run against a cold cache -- see `ocr::cache::OcrCache`.
///
/// Bumped for the PDF OCR source-DPI fix: `extractors::pdf::ocr` now tells the Tesseract
/// backend the true resolution of each rendered page (`SOURCE_DPI_BACKEND_OPTION`), so
/// `image::preprocessing::normalize_image_dpi_owned` stops assuming 72 DPI for rasters
/// rendered at 150. Every PDF OCR page therefore produces a differently sized raster, a
/// different Tesseract `scan_res`, and different output for an unchanged input and config.
/// `hash_config` now folds `TesseractConfig::source_dpi` in, but that only separates future
/// entries from each other; the entries already on disk were written under the 72 assumption
/// with a key that cannot distinguish them, and this bump is what makes them unreachable. Any
/// A/B measuring this fix MUST run against a cold cache.
///
/// Bumped for the OCR page-number fix: `perform_ocr` (`ocr::processor::execution`) stamped
/// every returned element, table, and `OcrElement` with `1` regardless of which page of the
/// source document the image actually was, because Tesseract numbers every single-image call's
/// hOCR/TSV/iterator page as `0`/`1` internally (see `TesseractConfig::page_number`'s doc
/// comment). `hash_config` now folds `page_number` in, but that only separates future entries
/// from each other; entries already on disk were written with the always-`1` page baked into
/// their cached content and a key that cannot distinguish them, and this bump is what makes
/// them unreachable.
///
/// Bumped for built-in extractor provenance: successful built-in extraction now defaults a
/// missing or unrecognized `extraction_method` to `native`. Cached `ExtractedDocument` values
/// are returned before extractor dispatch, so schema-6 entries could otherwise retain `None`.
pub const CACHE_SCHEMA_VERSION: u32 = 7;
/// Number of hex characters in the cache version tag.
const VERSION_TAG_HEX_LEN: usize = 8;
/// Return the process-wide cache-key version tag as 8 hex characters.
///
/// Stable for the lifetime of a process: the same binary always produces the
/// same tag, because every input is a compile-time constant (`env!` values
/// and `cfg!(debug_assertions)` are baked in at compilation, not read at
/// runtime). Two builds that differ in crate version, [`CACHE_SCHEMA_VERSION`],
/// debug/release profile, or `XBERG_BUILD_ID` (see `build.rs`'s `build_id`)
/// produce different tags. When `XBERG_BUILD_ID` is `""` on both builds
/// (neither reached git metadata nor an override — e.g. two crates.io/docs.rs
/// tarball builds of the same version), those two builds still collide on
/// that input; see the module doc for why that particular case is considered
/// correct rather than a gap.
pub
/// Prefix `cache_key` with the cache-key version tag (see [`cache_version_tag`]).
///
/// The result stays a single safe filename component: the tag is hex, and the
/// separator is `-`, so `Path::file_stem` on `<tag>-<key>.msgpack` round-trips
/// back to the versioned key.
pub