File size: 15,186 Bytes
cb2fd31 f1342ce cb2fd31 7f153d6 cb2fd31 f0e54e9 cb2fd31 f0e54e9 cb2fd31 f0e54e9 cb2fd31 a0df6d3 cb2fd31 f0e54e9 cb2fd31 a0df6d3 5fed261 cb2fd31 f0e54e9 cb2fd31 | 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 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 | # Knowledge Pipeline β Parsing Output Contract
<!-- doc-status -->
> π **LIVE β the seam.** Output contract of the parsing half, consumed by `src/knowledge_extraction/adapter.py`. Source of truth is `src/knowledge_parsing/contracts.py`; this doc describes it.
**Source of truth:** `src/knowledge_parsing/contracts.py`
**Producer:** `python -m src.knowledge_parsing.run --input data/knowledge_docs/`
**Reference run:** `20260824-100720` Β· BUMA `STD_2026_006_MNO` Β· 9 pages Β· MinerU `3.4.4`, backend `pipeline`
**Status:** offline artifacts only β no endpoint, no table. Values below are **literal from that run**, not illustrative.
Companion to `KNOWLEDGE_OUTPUT_CONTRACT.md` (extraction). This is the half **upstream** of it:
parsing emits `ParsedDocument`, extraction consumes it and never touches the parser.
---
## Transport reality β read this first
There is **no endpoint and no table yet.** The CLI writes one directory per run:
```
data/knowledge_runs/<run_id>/
βββ manifest.json run record: config, timings, quality warnings
βββ failures.jsonl documents that failed, with tracebacks
βββ <doc_id>/
βββ chunks.json β the artifact: one ParsedDocument
βββ mineru-source.txt pointer to the untouched MinerU output
```
**Unlike the extraction payloads, this artifact is not a bare array.** `chunks.json` is a single
self-describing object that carries its own provenance β parser name, version, backend, config
fingerprint, source hash. The envelope proposed at the end of the extraction contract already exists
here, and is worth reusing rather than inventing a second shape.
Three conventions hold:
- **`page_idx` is 0-based**, exactly as MinerU reports it, with no conversion anywhere. Converting to
1-based is the UI's job, done once at display, so the artifact always matches the raw MinerU output
kept beside it in the cache.
- **`text` is VERBATIM.** Extraction's span check locates quoted spans literally inside it. Reflowing
or normalising it makes the lookup fail and the field goes silently null β which presents as a bad
model, not as a parser bug.
- **Markup never goes in `text`.** Formulas and tables are rendered to readable prose for `text`, and
preserved in their original form in `latex` / `table_html`.
---
## 1 Β· `chunks.json` β `ParsedDocument` *(single object)*
```json
{
"doc_id": "STD_2026_006_MNO",
"chunks": [ "β¦ Chunk[] β¦" ],
"source_path": "data\\knowledge_docs\\STD_2026_006_MNO.pdf",
"content_hash": "f10807178b20531e86237a2640ff4125d259aa68e5e9d9f910c0c2365cd92833",
"n_pages": 9,
"parser_name": "mineru",
"parser_version": "3.4.4",
"parser_backend": "pipeline",
"parser_config": "b367868551b9",
"version": 1,
"schema_version": "0.3.0",
"created_at": "2026-08-24T10:07:21",
"raw_output_dir": "β¦/data/knowledge_cache/parse/f10807178b20531e-b367868551b9-3.4.4"
}
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `doc_id` | string | β
| Source filename stem |
| `chunks` | Chunk[] | β
| 31 on the reference run |
| `source_path` | string | β
| |
| `content_hash` | string | β
| sha256 of the **source file** β a changed document forces a new artifact |
| `n_pages` | int | β
| |
| `parser_name` | string | β
| Default `"mineru"`. The field exists so a Tesseract / Azure DI path is distinguishable |
| `parser_version` | string \| null | β | e.g. `"3.4.4"` |
| `parser_backend` | string \| null | β | `pipeline` Β· `vlm` Β· `hybrid`. **Read back from MinerU's `_middle.json`** β what actually ran, not what was requested |
| `parser_config` | string \| null | β | 12-char fingerprint of the settings that affect output |
| `version` | int | β
| Version of **this artifact for this document**; bumped on re-parse |
| `schema_version` | string | β
| Versions **the contract**. Currently `0.3.0` |
| `created_at` | string \| null | β | ISO 8601, seconds precision |
| `raw_output_dir` | string \| null | β | Untouched MinerU output, kept content-addressed and never copied |
> **Why provenance is mandatory rather than nice-to-have:** without
> `parser_version` / `parser_backend`, a MinerU upgrade and a prompt change are
> indistinguishable when extraction results shift.
---
## 2 Β· `Chunk`
```json
{
"chunk_id": "STD_2026_006_MNO::0003",
"doc_id": "STD_2026_006_MNO",
"kind": "text",
"text": "Adalah serangkaian kegiatan yang dilakukan untuk menciptakan nilai tambah terhadap sumber daya β¦",
"page_idx": 1,
"page_idxs": [1],
"section_no": "2.1.1",
"heading": "Production",
"chapters": [],
"heading_path": ["2.1.1. Production"],
"has_formula": true,
"is_tabular": false,
"source_items": [6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
"bbox": [122, 293, 275, 307],
"images": ["images/76503982720c45fc9d387accb74d5f5c5083f4f3109ed7fbf6a4da345bc20e0f.jpg"],
"latex": ["$$\nP r o d u c t i o n = M O H H x Q t y x P A x U A x P t y\n$$"],
"table_html": null
}
```
| Field | Type | Req | Notes |
|---|---|---|---|
| `chunk_id` | string | β
| **Format `<doc_id>::<seq>`**, `seq` zero-padded to 4. Note the `::` separator β see *Known gaps* |
| `doc_id` | string | β
| |
| `kind` | enum | β
| `text` Β· `table` Β· `chart` Β· `equation` Β· `image`. **Backends disagree on the label** β a figure `pipeline` calls a chart, `vlm` calls an image. Branch on both, never on one |
| `text` | string | β
| **VERBATIM.** Excludes the section heading line β that lives in `heading`. Consumers needing both compose them |
| `page_idx` | int | β
| **0-based.** First page of the chunk |
| `page_idxs` | int[] | β
| Every page the chunk spans. Default `[]` |
| `section_no` | string \| null | β | e.g. `"2.1.3"`; null when the document is unnumbered |
| `heading` | string \| null | β | This chunk's own section title, **verbatim as written** β `"Physical of Availability (PA)"`, not the tidied `"Physical Availability"` |
| `chapters` | string[] | β
| Running header per page spanned. Default `[]` |
| `heading_path` | string[] | β
| Breadcrumb of enclosing headings, outermost first |
| `has_formula` | bool | β
| Cheap downstream filter |
| `is_tabular` | bool | β
| |
| `source_items` | int[] | β
| Indices into MinerU's `content_list.json` β the trace back |
| `bbox` | int[] \| null | β | `[x0, y0, x1, y1]` of the first source item. Carried for a curation UI; **nothing in the pipeline reasons about it** β ordering uses item sequence, never coordinates |
| `images` | string[] | β
| Paths relative to `raw_output_dir`. Formula images, table/chart crops |
| `latex` | string[] | β
| Source markup for formulas, kept beside the rendered prose. β οΈ **LOAD-BEARING** β extraction's formula guard validates against this field. Reflowing it or leaving it empty weakens the check with no error. See below |
| `table_html` | string \| null | β | Source markup for tables |
| `generated_description` | string \| null | β | β οΈ **MODEL-WRITTEN, NOT FROM THE DOCUMENT.** MinerU's description of an image or chart. **Never span-checkable** β treat as a model claim about a picture, never as evidence. See below |
### Why `text` is rendered but markup is kept
The term filter downstream is an NER model reading prose. MinerU writes formulas character-spaced
(`P u r c h a s i n g ~ c o s t s`) and tables as HTML, and neither yields a single mention.
Measured on the same document and gold set, changing only the parse:
| `text` contains | Recall |
|---|---|
| raw markup | 0.7561 |
| plain text | 0.8537 |
| markup rendered back to prose | 0.8293 |
The markup is not discarded, because the formula branch needs exactly that form.
### `latex` is load-bearing β changing it weakens a guardrail silently
**As of 2026-08-26 this field carries weight it did not carry before.** Extraction's
formula guard validates a model's `formula_latex` claim against `Chunk.latex` β
**not** against `text`, which never contains markup. Until that landed, `latex`
was carried but unread; a parser change touching it would have broken nothing.
That is no longer true.
The failure mode is the dangerous kind. If this field is reflowed, "tidied", or
left unpopulated, the guard does not error β it quietly has less to check against,
and formula claims it can no longer verify are recorded as unverifiable rather
than caught. Fewer things fail; nothing looks wrong.
Two obligations follow, and they run **opposite** to the rule for `text`:
- **Do not clean it.** MinerU's character-spacing (`P r o d u c t i o n`), its
`\mathrm{}` wrappers and its `~` spacing are all expected β extraction
canonicalises before comparing, so cosmetic differences are already tolerated.
Pre-cleaning here removes information the canonicaliser was built to absorb.
- **Do not drop it.** An artifact with no `latex` leaves every formula claim
unverifiable. That is handled gracefully, but it is a guard switched off.
What extraction tolerates is **notation**; what it will not tolerate is **structure**.
Presentational wrappers, `\cdot` versus `\times`, and brace style are all erased
before comparison. A missing `\frac` is not β that is a different formula, and
catching it is the point of the guard.
**Worked evidence.** The `pipeline` backend writes multiplication as a bare letter
`x`, with no `\times` anywhere. Extraction cannot then tell a multiplication sign
from a variable, so every product formula from such an artifact comes back
`latex_verification: "unverified_operator"` β correctly, since the source genuinely
cannot express the operator. This is a real state, not a hypothetical: every
artifact produced without a GPU is a `pipeline` artifact. It is one more reason the
production backend is `hybrid/high` (see `KNOWLEDGE_PIPELINE_CALIBRATION.md` Β§13).
**Cheap health check, no paid run needed.** `latex_verification` is set during
validation, not by a model call, so counting `verified` against
`unverified_operator` in `formulas.json` measures directly whether an artifact's
markup is rich enough to support the guard.
### What decides a chunk boundary β **not the page**
A page break is a printing artifact, not a meaning boundary. Cutting there would sever a definition
that happens to straddle one, and the evidence ranker downstream reasons about sections. So chunking
follows the document's own structure, in this priority:
| | Opens a new chunk | |
|---|---|---|
| 1 | **A numbered section heading** | the primary boundary |
| 2 | **A table or a chart** | one MinerU item β one chunk |
| 3 | **6 000 characters (~1 500 tokens)** | safety net **only** for documents where no heading is detected; without it one chunk could swallow a whole document, blunting evidence ranking and inflating the summary branch's token share. Splits at an item boundary, so `text` stays verbatim |
Measured on the reference run β the spread is content-driven, not fixed:
| | |
|---|---|
| Chunks per page | **1 to 7** (page 0 β 1, page 5 β 7) |
| Chunks spanning >1 page | 0 of 31 here, though `page_idxs` is a list precisely because it can happen |
| `text` length | median **48 words**, max 444, one empty (the caption-less chart) |
Rule 3 therefore almost never fires on a well-structured standard β the headings do the work. It
matters for the mid-chapter textbook pages, which carry no headings at all.
---
## 3 Β· Reference run β what 9 pages actually produced
| | |
|---|---|
| MinerU items | 118 β 90 text, 14 table, 11 equation, 3 chart |
| Chunks emitted | **31** β 15 `text`, 13 `table`, 3 `chart`, **0 `equation`** |
| Images | 27 unique (**3.0 per page**) |
| Quality warnings | 3 |
| Parse time | 385.5 s (**42.8 s/page**, CPU) |
> The chunk count differs from the `13` quoted in the extraction contract's reference run
> (`20260813-145132`). That run predates the section-aware chunker built 2026-08-19; **31 is
> current.** Anything calibrated against 13 chunks should be re-checked.
### Quality warnings β `manifest.json`
Warnings never fail a document; they are recorded so quality cannot drop silently across hundreds of
documents. The reference run's three:
| `jenis` | Page | Detail |
|---|---|---|
| `latex_tanpa_angka` | 1 | `P r o d u c t i o n = M O H H x Q t y x P A x U A x P t y` β symbolic, so no digits |
| `tabel_kosong` | 6 | 1 of 14 tables detected but empty |
| `chart_tanpa_konteks` | 7 | Chart with neither content nor caption |
The check that matters most β **every number in a formula cross-checked against the source PDF's own
text layer** β returned clean. It exists because a CPU-pipeline parse once wrote `5600` as `55600`
with no error and a perfectly valid-looking formula.
---
### `generated_description` β why it is not in `text`
`text` is the haystack the extraction guardrail searches to prove a quote is verbatim. Generated
prose placed there would let a hallucinated sentence pass the very check built to catch
hallucination β silently, and with no code change to blame. A caption **printed in the document** is
verbatim and stays in `text`; a description **invented about the picture** does not.
Whether it is populated at all depends on the backend, read from MinerU's own source
(`is_image_analysis_option_visible`):
| Backend | Describes images |
|---|---|
| `pipeline` | never β `content` is always empty |
| `hybrid` + `effort=medium` | **disabled outright** |
| `hybrid` + `effort=high` | yes |
| `vlm` | yes, always |
So an artifact from `pipeline` carries `null` here throughout. That is the backend's behaviour, not
a parsing failure β and one more reason `parser_backend` has to travel with the data.
---
## Known gaps β do not design around these without asking
**`chunk_id` separator disagrees with the extraction contract.** This artifact emits
`STD_2026_006_MNO::0003`. `KNOWLEDGE_OUTPUT_CONTRACT.md` shows `STD_2026_006_MNO#0007`. Provenance
back-references break if a consumer parses on the wrong separator. **Needs one call, then both docs
updated.**
**`kind="equation"` is declared but never emitted.** All 11 equations arrive as `latex` entries on
`text` chunks with `has_formula=true`. Either the enum member goes, or the chunker should emit them β
but consumers must not branch on `kind == "equation"` today.
**`heading_path` carries page-number pollution.** On the reference run its root is frequently
`"1. 7"` β page 7's number caught as a numbered heading β appearing in 7 of 14 distinct paths. And
`"2. PENJELASAN PARAMETER"` never appears as a root although it should. `heading_path` feeds the
evidence ranker, so this is not cosmetic. Open against B5.
**`chapters` is empty on every chunk** of the reference run. The field is populated only when MinerU
reports running headers; this document has none detected.
---
## Caveat on the sample values
Field shapes, types and defaults are read from `src/knowledge_parsing/contracts.py`. Unlike the
extraction contract, **the values here are literal** β taken from
`data/knowledge_runs/20260824-100720/STD_2026_006_MNO/chunks.json`, with long strings truncated and
marked `β¦`.
|