Commit History
-
[NOTICKET] feat(knowledge-parsing): commit the reference artifact, and stop the stamp churning
Three changes, all aimed at one problem: the two halves of the pipeline could not reliably agree on which artifact they were measuring. **Metadata now precedes `chunks` in the artifact.** Pydantic serialises in declaration order, so it decides what a person sees on opening the file. With `chunks` first, checking which parser or normaliser produced an artifact meant scrolling past thousands of lines to the end — and that is a routine check now, not a rare one. JSON objects are unordered by specification and every consumer reads by key, so this changes nothing for code and `schema_version` is unchanged: no field, type or meaning moved. **`contracts.py` is out of the normaliser hash.** The artifact's SHAPE is already versioned by `schema_version`, so hashing it again counted the same change twice. The reordering above proved the cost: the stamp moved while the chunks came out byte-identical, which is a false "your artifact is stale". A signal that cries wolf gets ignored, and this one e…a0e59e4 -
[NOTICKET] feat(knowledge-parsing): stamp the artifact with the code that built it
A chunks.json is a frozen snapshot of the normalisation code at the moment it ran. When that code is fixed, existing artifacts do not update themselves — and nothing in their metadata said so. `parser_config` fingerprints MinerU's settings; `schema_version` versions the artifact's SHAPE, which a normalisation bugfix does not change. So a stale artifact and a correct one declared identical metadata, and the only observable difference was the character count. That cost a day. A pre-fix artifact reached the extraction half, term recall came back 0.7073 against a 0.8049 baseline, and it read as a regression in extraction's own work. It was neither side's setup: same filter code, different input. Verified here — scoring that artifact reproduces 169 mentions and 0.7073 exactly, against 186 and 0.8293 for the rebuilt one. `normaliser_version` is a hash of the modules that decide what a chunk CONTAINS: normalize.py, render.py, checks.py, contracts.py. A source hash rather than a hand-mainta…c64a0cb
-
[NOTICKET] fix(knowledge-parsing): normalize.py silently dropped every list block
MinerU keeps list content in `list_items` and gives such items no `text` field. They therefore fell through the generic text path, which reads `item["text"]`, came back empty, and were skipped — no error, no warning, no trace. This is the same shape of bug the chart/image branch was written to prevent, and that branch's comment describes it exactly: "pictures have no such field — so BOTH its caption and its description vanish in silence". Lists have no such field either, and nobody noticed. Measured cost on the BUMA standard: two dropped blocks took five gold terms with them — Joint survey, Truck count, EWH, Controllable, Hierarki Level Analysis. None of the five were "missed by the model"; none were in the artifact at all. Term-filter recall 0.7073 -> 0.8293, +0.1220. List content joins the running text chunk rather than opening its own, because it is prose inside its section. It deliberately skips heading detection: a short bullet can look like a heading, and letting one open a se…6edfbe7
-
[NOTICKET] refactor(knowledge-parsing): English throughout the module
The parsing half was written in Indonesian — identifiers, docstrings, comments, manifest keys and console output. Everything else in the repo is English, and this module is the newest work, so it is the one to bring into line rather than the older code around it. Renamed across the module: parse_dokumen -> parse_document, normalisasi -> normalise, pulihkan_batas_kata -> restore_word_boundaries, kosakata_layak -> trustworthy_vocabulary, periksa_items -> check_items, CatatanDokumen -> DocumentRecord, and the locals with them. Docstrings and comments were REWRITTEN rather than translated word for word. Several carry the reasoning behind a decision — why `\frac` is unwrapped before the braced wrappers, why a heading never enters `text`, why the digit threshold is 3 — and a mechanical translation would have flattened exactly the part worth keeping. Manifest keys move too: detik_per_halaman -> seconds_per_page, peringatan -> warnings, jenis -> kind, galat -> error. That is a data change, …7f153d6
-
[NOTICKET] feat(knowledge-parsing): carry image descriptions, apart from the text
MinerU describes images and charts, but only on some backends. Read from its own source (is_image_analysis_option_visible): never on `pipeline`, always on `vlm`, and on `hybrid` only at effort=high — medium disables it outright. Two faults kept that description from ever arriving. `normalize.py` had no branch for `image` items at all. Different backends label the same picture differently: what `pipeline` calls a chart, `vlm` calls an image. Image items fell through to the generic text path, which reads only item["text"] — a field pictures do not have — so caption AND description were dropped in silence. That is most of what looked like "vlm loses chart detection" in yesterday's comparison: vlm found them, we discarded them. The second is worse and was waiting to fire. `_teks_chart` appended `content` straight into Chunk.text. Harmless today because `pipeline` always leaves it empty — but the moment the backend flips to vlm, model-written prose starts flowing into the field the extra…c113bf8 -
[NOTICKET] fix(knowledge-parsing): let the source prove itself before trusting it
Word-boundary recovery treats the PDF text layer as the arbiter. That is safe when the layer is intact and dangerous when it is HALF there: a legitimate word looks unknown, and gets split. An empty text layer was already safe — no vocabulary, no recovery. The gap was the middle ground, and nothing measured it. So the page now has to prove itself: how many of MinerU's own prose words on that page are actually found in its text layer. A low score means the two read the same page differently, and a source that disagrees with the parser is not fit to arbitrate it. Pages scoring below 90%, or carrying too little prose to judge at all, lose their vocabulary and skip recovery — left damaged but honest, rather than repaired into something the document never said. This replaces a constant tuned against one document with a measure that assesses each page on its own evidence. Measured across three documents, two of them a McGraw-Hill textbook in English with nothing in common with the BUMA st…22c6ad1 -
[NOTICKET] fix(knowledge-parsing): read word boundaries back from the source PDF
MinerU writes formulas one character at a time ("T o t a l H o u r s"), and once those are rejoined the word boundaries are gone: "Total Hours" and "TotalHours" are no longer distinguishable from the LaTeX alone. Everything fuses into a single token. The multiplication written as a letter "x" is not a separate bug. It is the same one — an "x" between terms is just another spaced character, so "MOHH x Qty x PA x UA x Pty" collapses to "MOHHxQtyxPAxUAxPty". Two obvious fixes were tried and rejected: - Rewriting a lone "x" to a multiplication sign guesses, and guesses wrong on any document with a variable named x, silently. It also leaves "TotalHours" untouched, which is the larger half of the damage. - Emitting a warning fires on 7 of 11 equations. A warning that lights up on 64% of items is not actionable, and recovers nothing. The source PDF still holds the boundaries: its text layer reads "MOHH x Qty x PA x UA x Pty", properly spaced. So the answer is read from t…e047f7a
-
[NOTICKET] fix(knowledge-parsing): render markup to prose in text; only numbered headings split sections
Both defects were measured by the extraction side on the same document and gold set, changing only the parse: PyMuPDF (plain text) recall 0.8537 35/41 MinerU, raw markup in text recall 0.7561 31/41 MinerU, formula + table rendered recall 0.8293 34/41 1. Chunk.text now carries readable prose, not markup. The term filter is an NER model reading prose. MinerU writes formulas with every character spaced out — "{ \mathrm { P u r c h a s i n g ~ c o s t s } }" — and tables as HTML; neither yields a single mention for terms the same model finds easily in plain text. render.py rejoins spelled-out words and numbers, folds subscripts, maps operators to non-letter glyphs (a literal "x" for \times would merge into the token beside it), and flattens tables to " | "-separated rows. The markup is kept, not discarded: Chunk.latex and Chunk.table_html hold it verbatim, because the formula branch needs exactly that form. Found while verifying: table cells carr…2cb1336 -
[NOTICKET] fix(knowledge-parsing): keep the heading line out of Chunk.text
The heading was briefly copied into `text` as its first segment, to stop the chunk that defines a term from containing no mention of that term — the BUMA standard names the term in the heading and opens the body with "Adalah ..." without repeating it. The extraction half already solves this, and better, because it measured it on the document: `validate/span_check.py` builds its haystack as heading + text, and `rank/evidence.py` treats a heading that names the term as a mention at offset 0. So copying it into `text` only causes harm: the heading appears twice in that haystack, and its occurrence becomes a real mention, inflating mention_count. That count is what the frozen prototype baseline is compared against (169 mentions -> 66 clusters), so v2 numbers would drift for a reason that has nothing to do with extraction quality. `heading` stays verbatim, which is what both halves actually needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>15028da -
[NOTICKET] feat(knowledge-parsing): document parsing half of the knowledge pipeline
Implements §4 B2/B5/B6 and closes the four open seam items (S2, S3, S4, S6b/c/d) against the contracts.py draft circulated 2026-08-19. Additive and flag-gated: src/knowledge/ (Tesseract -> pgvector) is untouched. Seam — src/knowledge_parsing/contracts.py, pydantic per S6e: - ParsedDocument envelope: content_hash, n_pages, version, parser_name/version/ backend/config, schema_version, raw_output_dir. parser_backend is read from MinerU's _middle.json, so it records what actually ran, not what was configured — a MinerU change and a prompt change stay distinguishable (S2). - Heading structure is READ, not derived. Verified in MinerU's source: pipeline and vlm run identical text_level logic, and bbox is emitted by both. Heading availability is document-dependent, not backend-dependent — the earlier claim came from sampling the McGraw-Hill handbook, which has no numbered headings (S3). - source_wording on TermRecord; heading carried verbatim (S4). - page_idx/page_idxs, 0-based, n…5840d20