Release Notes 2026-08-12 (v0.3.1)
English | 繁體中文
This is a delivery-correctness release. v0.3.0 shipped the screenshot pipeline; running a
corpus of research reports through it surfaced a class of defect the quality gate could not
see — output that is well-formed, passes every check, and is missing or garbling exactly the
text a retrieval query lands on. Every fix below was measured against that corpus rather than
against the few documents that first showed the symptom. (The corpus grew from 100 to 167
documents during the round, which is why the denominators below differ.)
Previous release: Release Notes 2026-07-17 (v0.3.0).
Highlights
Body text, not the page it was printed on
- Page furniture (running heads, folios, volume lines) had a detector and no production
caller: 1,826 lines were delivered as body paragraphs, inside 851 of 1,843 chunks. All three
delivery surfaces now skip them, and short text that recurs in the same margin band on two
or more pages is additionally tagged as furniture. - The supplement pass — which re-reads each page and fills in what no block covers — was
duplicating and reordering the prose it was meant to complete. Coverage is now directional,
spans one page either side, ignores markdown escaping and whitespace, and falls back to bbox
containment for text no comparison can match. Supplements are spliced into the parser's
reading order per page instead of the whole document being re-sorted by vertical position;
one supplement had been enough to interleave the columns of a two-column layout. - Documents whose cover title is set vertically delivered a garbled
H1— OCR of vertical
type drops and swaps characters — while a clean copy of the same title sat unused in the IR
as a horizontal running head. 49 of 167 documents were affected. The running head is now
authoritative for characters but not for which segments a title has, so a cover kicker the
running head omits is kept, and a series label it adds is refused.
Tables
- Multi-row headers. A one-row-header assumption sent a table's sub-column names to a
fallback (欄位4: 22.40, with nothing to say it is a share of imports) and shipped the
sub-column row as the first data record. Header rows are now folded behind four guards, each
with a live counter-example. Placeholder column names: 926 → 8. - Table titles. Where the parser gave no caption (52 of 128 tables), the title came from a
document-level string picked by substring match, truncated to 100 characters. The nearest
caption printed above the table is now used; 46 of the 52 recover their printed name. - Numeric tables were being discarded as OCR noise. Any purely numeric cell counted as
"weak", so a wide statistics table was structurally guaranteed to read as garbage and have
every data row dropped — 7 tables in 4 reports, whose absence then tripped the
empty-output check, the VLM audit, and a reviewer pass of roughly 100k tokens. Well-formed
figures are now read as data; lone digits and mash such as4.64.74.7still count as noise.
11 of 11 tables recovered. - Attribution rows. The 注/資料來源 line printed inside a table's own border looks exactly
like a data row with only its first column filled, so it was emitted as a record whose
heading is a sentence and whose one field states something the table never said. It is now
recognised by its wording — narrowly, against the openings papers actually use — and
rendered as a trailing line after the records. A row that does not announce itself stays
data, because a row whose cell boundaries the parser lost is full-width too and the two are
indistinguishable by shape. - Collapsed cell boundaries are now repaired. New capability; see Compatibility Notes.
- Table titles in
chunks.jsonl. The parser hands captions back as lists of strings; 76
table titles shipped as the Python list repr, andchunks.jsonlis what retrieval reads.
Captions are now flattened once, where the payload is built. The same shape had been making
the figure caption-length gate fire on every properly captioned figure, so the model was
re-captioning figures that already had a caption printed beside them.
Traditional Chinese output
- The simplified-content detector converted with
s2tand compared against the source, but
that mapping treats 台/布/占/干/了 — every one of them standard zh-TW — as simplified. Ordinary
Taiwanese prose therefore read as simplified, and was then rewritten bys2twp's phrase
table (公布→公佈, 關鍵零組件→關鍵零元件, 數據→資料, institution names 台→臺). Ambiguous variant
pairs are folded before detection, conversion usess2tw, and the author's own variant
choice is restored positionally afterwards. - Conversion never saw the parser's own text: its single call site was the VLM render path, so
849 unambiguously simplified characters reached the output across 88 of 100 documents. It
now runs as a normalize post-pass over every text-bearing payload field, whatever produced
the block, with a kana guard applied line by line so a bibliography's Japanese entries are
not "repaired". - Two characters the detector could not see at all (凈, 説 —
s2tmaps them to themselves)
passed byte-identical and never reached conversion: 51 and 25 occurrences.
VLM robustness
- A model the endpoint does not serve now reports unavailable. The availability check
returned true whenever the HTTP endpoint answered — including the branch that had just
reported the configured model missing — so a model that was never pulled, or a typo in the
name, did not stop the stage once with something a human could act on; it let every
enrichment request go out and fail one at a time. An endpoint that enumerates no models
stays permissive (some gateways return an empty list), and Ollama's implicit:latesttag
is matched so a working bare-name configuration is not called broken. - Crops below the vision model's 32px patch factor are never sent. Four crops of 32x31,
34x31 and 37x31px caused 8 model-runner panics and 8 lost enrichments; the server reports
this to the client as a generic resource-limit error that reads like an out-of-memory
condition and is not one.
API, operations, security
- Named-host access is now opt-in. The frontend dev server ran with all hosts allowed on
0.0.0.0, so any website could DNS-rebind to it and reach the unauthenticated backend
through the/apiproxy. SeeSEMARK_FRONTEND_ALLOWED_HOSTSbelow;localhostand direct
IP access are unchanged. - The privacy scrub now runs at the write boundary. It had been applied at scattered call
sites, leaving four delivered surfaces unmasked:dataset.md, the structured-repair
main-document re-export,chunks.jsonlmetadata, and the VLM-audit excerpts in
quality_gate.json/llm_vlm_outputs.md. - Delivered writes are atomic. No delivered artifact was written atomically, and split
documents were deleted before their replacements were written, so a crash mid-export left
deleted files still referenced by the old index. Stale split documents are now removed only
after the new set lands. - Download lists past 500 runs. With the default filter, the outputs-summary endpoint only
ever inspected the newest 500 runs and reported the count within that window as the total,
so installs past that point silently lost older documents from download lists. List queries
also gained a stable tiebreaker —created_athas second resolution, so batch-created rows
tied and could skip or duplicate rows across pages. - Enrich cache identity is parse-aware. Keys folded in neither the parse configuration nor
the parser version, so re-parsing under different settings could serve a caption belonging
to a different figure, and switching the output language served captions in the old
language. Previously cached rows re-enrich once. - Pipeline concurrency. One module-level semaphore is now shared by the queue workers and
the directbackground=falsepath, which previously ran unbounded concurrent parse+VLM
pipelines. The startup sweep also cancels PENDING runs orphaned by a restart (the queue is
in-memory), and the task queue is stopped before the database disconnects so cancellation
handlers can still write run status during shutdown. - Viewer. The bbox overlay was vertically compressed on every page — on A4 portrait a
page-bottom block drew at 71% height, so clicking a region selected the wrong block. Tables
that the package stage deliberately keeps as raw HTML now render in the main-text pane
instead of being silently dropped, throughrehype-rawpaired withrehype-sanitize
(scripts, event handlers and inline styles stay stripped). Relative image paths resolve
through the asset endpoint instead of 404ing against the SPA route. - The health endpoint reports the real application version instead of a hardcoded
0.1.0.
Content that is not content
- A promotional insert — advertisements with prices, order lines and QR codes — occupies the
right column of the last page in 32 documents while the left column still carries the
reference list, so this had to be a column-level judgement: dropping the page would have
deleted the references. The column boundary is derived from the page's own gutter rather
than a hard-coded coordinate. It also keeps 105 covers and QR codes out of the model. - A twenty-page report's bibliography was being scored with thresholds calibrated for a
one-page form:DOI:andhttps:entries plus colon-terminated citation titles read as
field labels, and 申請單 matched inside 申請單位 ("applicant unit"). 5 of 100 research reports
were flagged as fillable forms, each raising an empty-structured-output issue and its
downstream audit. - Figure sections written by the model shipped as
##headings at the same level as the
article's own, naming no figure. They are now bold subsections under a single container
heading. 311 spurious headings removed.
Documentation and demos
examples/demos/README.mdgained the two screenshot demos added in v0.3.0 and the
raw-parse.md/figure-example.mdartifacts it never listed, and now has a Traditional
Chinese counterpart.- Both READMEs document collapsed-table repair, and three details that existed in only one
language were synced. BACKLOG.mdrecords the known limitations the architecture review surfaced and this round
deliberately did not change.
Compatibility Notes
- New capability, on by default:
vlm_repair_collapsed_tables(aPipelineConfigfield,
no environment variable) re-reads tables whose cell boundaries the parser collapsed into a
single merged cell — 26 of 128 tables in the reference corpus — and rebuilds the grid from
the table image. It is independent ofvlm_enrich_tables, which buys summaries for tables
that parsed cleanly. The original parse is kept alongside astable_body_mineru; a ragged
grid, or a transcription holding less than 60% of the printed characters, is rejected and
the original stands. Set it tofalseto keep the previous behavior. - New variable:
SEMARK_FRONTEND_ALLOWED_HOSTS(comma-separated). Reaching the frontend
through a named host now requires listing it. Access bylocalhostor by IP is unaffected. - Enrich cache keys changed. Rows cached before this release re-enrich once.
- Caption shape in the stored IR. Documents parsed before this release keep list-shaped
captions in their saveddocument_ir.json. A read-side guard covers them, so titles come
out right either way; only a re-parse puts flat captions in the file. - The health endpoint's version string changes from
0.1.0to the real application
version. template_section_labelsadded todefault.jsondoes not take effect in deployments that
supply their own corpus ruleset, because corpus rules replace rather than merge.- Backend test suite: 437 → 718 tests green.
Verification and Caveats
- The pipeline fixes were verified by replaying all 167 stored document IRs offline, plus
end-to-end runs on three documents. A full live re-run of the whole corpus on the rebuilt
image has not been done. - Where a threshold appears above — 0.60 coverage, 40-character header cells, the 60%
short-read floor, the 106-character whole-table floor — it is a measurement of this corpus,
not a principle. A corpus whose tables are annotated differently needs them re-measured. - The supplement-coverage calibration is deliberately asymmetric: it prefers a repeated phrase
over a line that exists in one copy and is gone. Two whole-table duplicates that fall below
the length floor come back as a result of that choice. - Because collapsed-table repair treats the image as the authority, a table the model misreads
is replaced by a wrong grid rather than a garbled one. The fail-closed guards are what stands
between those two outcomes. - Known limitations that remain: image footnotes are still outside the supplement coverage
text (the same bug class, not yet measured); drop caps still lose their first character at
the parser level; and a Chinese paragraph citing Japanese inline keeps its simplified glyph
(measured at one character corpus-wide — character-level splitting risks corrupting the
Japanese).