File size: 23,021 Bytes
b68816f
 
 
 
 
 
 
 
 
 
 
 
 
f07443e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b68816f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f07443e
b68816f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
f07443e
 
 
b68816f
 
 
 
f07443e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
b68816f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
# 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.

> ## ⚠️ schema_version **0.4.0** β€” asset pointers (2026-09-10)
>
> **A figure is no longer its own chunk.** Its markdown reference is folded into the prose chunk at
> its reading-order position, and the figure travels as an `Asset` on that chunk. This is a **MINOR**
> bump, not a patch: chunk boundaries change, so a 0.3.x consumer would silently read a different
> document shape. `adapter.check_schema_version` rejects a newer minor by design β€” parsing and
> extraction ship together.
>
> **Why.** Before 0.4.0 an image became its own chunk with an **empty `text`**, which made it
> unreachable by construction: no text β†’ no term mentions β†’ joins no cluster β†’ never selected as
> evidence. Measured across three documents: **13 figures, every one described by a vision model we
> pay for, and ZERO appearing in any cluster's evidence.** Meanwhile the prose that said
> "Figure 1.1" had its `![](…)` reference stripped, so nothing connected the sentence to the picture.
>
> **What changed**
>
> | | 0.3.3 | 0.4.0 |
> |---|---|---|
> | A figure | its own chunk, `text=""`, unreachable | folded into the prose chunk as `![](asset://<id>)`, carried in `Chunk.assets` |
> | A table | its own chunk | **unchanged** β€” its linearised text earns its evidence slot (74.6% of the Komatsu manual's top-3 evidence is tabular). Gains an `Asset` + two-way pointers |
> | Asset bytes | returned by the API, **written nowhere**; `images[]` pointed at files that did not exist | content-addressed and persisted; `Asset.storage_key` |
> | Asset identity | the parser's page-local filename | **`sha256(bytes)[:16]`** |
> | A caption | landed in a *neighbouring* prose chunk, unlinked | attached to the `Asset` **and** left in the prose text (it is verbatim document content) |
> | A description | stored raw, JSON braces included | unwrapped; `description_source` records what produced it |
>
> **Measured effect** (same three documents, no re-parse β€” the cache fingerprint is unchanged):
> chunks 32β†’28, 85β†’80, 59β†’44 Β· **image chunks 2/3/8 β†’ 0/0/0** Β· empty-text chunks β†’ 0 Β·
> 13 assets persisted Β· 0 dangling references Β· **0 descriptions leaked into `text`** Β·
> captions still 17/17 in text Β· recall unmoved.
>
> **The invariant that must not break:** a model-written `description` never enters `Chunk.text`.
> `text` is the haystack the span check searches, so generated prose in there would let a
> fabrication pass the control built to catch it.

> **Current state (2026-09-02) β€” additive changes since the reference run below.** The default
> parser is now **Mistral OCR** (`parser_name: "mistral-ocr"`, `parser_backend: "mistral-ocr"`),
> API-based and needing **no GPU**. The artifact shape is **identical whichever parser produced it** β€”
> extraction never learns which ran, which is what made the swap possible at all.
>
> **2026-09-07:** **MinerU was DROPPED 2026-09-07** (Rifqi): commented out of the `knowledge-parsing` extra and installed nowhere β€” not the Space, not the dev venv (verified). It is **not a fallback**; there is one parser, Mistral OCR. Its code paths in `src/knowledge_parsing/` are untouched (comment-out-don't-delete) and every MinerU measurement in these docs stands as record. Dropping it was also forced: `mineru==3.4.4` pinned against this project's `python-docx` and `pypdf`, which made `uv lock` fail for the whole project. The `parser_name` / `parser_backend` fields stay in the contract:
> committed artifacts record `mineru`, and a reader must keep distinguishing them.
> `schema_version` is now **`0.3.3`**, all changes additive (null/derived on older artifacts):
> `source_title` (0.3.2) and `normaliser_version` (0.3.1) at the document level, and a computed
> 1-based **`page_no`** (0.3.3) on each Chunk. The reference JSON below is still the literal
> 2026-08-24 **MinerU** run, so it predates these fields.

---

## 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 |
| `source_title` | string \| null | β€” | **(0.3.2)** The document's human-readable name. Equals `doc_id` today; a separate field so the two can diverge once `doc_id` becomes a content hash |
| `normaliser_version` | string \| null | β€” | **(0.3.1)** Fingerprint of the normalisation code that produced these chunks. `parser_config` covers the parser's settings; this covers our half |
| `version` | int | βœ… | Version of **this artifact for this document**; bumped on re-parse |
| `schema_version` | string | βœ… | Versions **the contract**. `0.3.0` on this reference run; **current `0.4.0`** (see the banner above) |
| `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 `[]` |
| `page_no` | int | βœ… | **(0.3.3) 1-based**, computed `page_idx + 1`. The number a human / review UI reads; derived, never stored, so it cannot drift from `page_idx` |
| `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[] | βœ… | ⚠️ **DEPRECATED 0.4.0** β€” superseded by `assets`. Still populated for one version so a 0.3.x reader degrades rather than breaking. Holds the parser's own filenames, which are **not reliable**: Mistral reported `image_id: img-2.jpeg` on a block whose markdown said `img-1.jpeg`, so two figures collided and one image's bytes were lost. Read `assets` |
| `assets` | Asset[] | βœ… | **New 0.4.0.** Figures and tables this chunk contains or points at. Default `[]` |
| `referenced_by` | string[] | βœ… | **New 0.4.0.** `chunk_id`s pointing AT this chunk. Populated on table chunks. Default `[]` |
| `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 |

## 2b Β· `Asset` *(new at 0.4.0)*

```json
{
  "asset_id": "d2e7b4ffe31006ec",
  "kind": "figure",
  "storage_key": "assets/d2e7b4ffe31006ec.jpeg",
  "page_idx": 2,
  "page_no": 3,
  "bbox": [93, 562, 434, 907],
  "caption": "Figure 1.1. The relationship between exploration information, resources and reserves (SME, 1991).",
  "description": "The diagram illustrates the classification of mineral resources and reserves based on increasing levels of geological knowledge…",
  "description_source": "mistral-ocr/bbox_annotation"
}
```

| Field | Type | Req | Notes |
|---|---|---|---|
| `asset_id` | string | βœ… | **`sha256(bytes)[:16]`.** Not the parser's id β€” see the `images` row. Content addressing makes a re-parse idempotent and de-duplicates a figure that appears twice (Open Pit has one such pair) |
| `kind` | enum | βœ… | `figure` Β· `chart` Β· `diagram` Β· `table` β€” one vocabulary for both cases |
| `storage_key` | string \| null | β€” | Where the bytes live. `null` for a table (no bytes of its own) and whenever persistence was skipped or failed β€” writing assets is a **never-throw** seam |
| `page_idx` | int | βœ… | 0-based, as the parser reports |
| `page_no` | int | βœ… | **Computed** `page_idx + 1`. The number a human reads; derived so it cannot drift |
| `bbox` | int[] \| null | β€” | `[x0,y0,x1,y1]` of the **asset**, so a UI can highlight the figure. Distinct from the host chunk's `bbox`, which stays where the text is |
| `caption` | string \| null | β€” | **VERBATIM** from the document's own caption block. Quotable as evidence, span-checkable. Also left in the prose `text`, where it already landed β€” the asset gets a copy, not a move |
| `description` | string \| null | β€” | ⚠️ **MODEL-WRITTEN.** Never span-checkable, never evidence, and **never in `text`** |
| `description_source` | string \| null | β€” | e.g. `mistral-ocr/bbox_annotation`, so a later model swap is traceable rather than silently changing what the field means |

**A figure's reference appears inline in its host chunk's `text`**, at the reading-order position the
document put it:

```text
…so closely and the geological character is so well defined that size, shape, depth
and mineral content of the resource are well established.

![](asset://d2e7b4ffe31006ec)

Figure 1.1. The relationship between exploration information, resources and reserves (SME, 1991).

– Indicated. Quantity and grade…
```

Two consumer obligations:

- **Resolve `asset://<id>` against that chunk's own `assets`.** Every reference is guaranteed to
  resolve locally (0 dangling across the three test documents).
- **Switch on the caption/description distinction.** A caption may be quoted and used as a provenance
  span; a description may not, and a span taken from one is rejected by the validator.

---

### 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 `…`.