Classification: research prototype with a validated decision core and a working structural pipeline for supported table shapes. Not a beta engine, not production-ready. See "Honest classification" at the bottom — this is not a hedge, it's a measured conclusion from what's below.
PDF
-> ingestion/pdf_extract.py (pdfplumber bbox-based table grid extraction -> DocumentIR)
-> schema/classify.py (table SHAPE classification + section IDENTITY classification,
the latter with an explicit confidence tier)
-> candidates/build.py (IR Table -> Candidate list: forward-fill, subscript-join,
multiline-cell zipping, leftmost-column parameter fallback)
-> vlm/interface.py (VLMPerceptionProvider contract; a real implementation lives in
integrations/qualcomm/, no live model wired in this environment)
-> decision/core.py (the frozen-then-extended decision core: tri-state negative
selection with confidence tiers, dedup, malformed-value
rejection, AMBIGUOUS_MISSING_CONDITION, explainable trace)
-> api/extract.py (top-level extract(pdf_path, query, vlm_provider=None) ->
structured result dict; two-phase orchestration -- decides on
structural evidence first, escalates to a VLM provider only
for pages with an unresolved table schema, and only if the
structural pass didn't already produce a confident answer)
-> integrations/qualcomm/ (QualcommVLMProvider: evidence schema, prompt, JSON-to-Candidate
parser -- see docs/QUALCOMM.md and examples/qualcomm_alim_colab.ipynb)
This matches the required principle: VLM perceives, deterministic engine decides. The
decision core itself has been changed only when a reproducible test demonstrated a real defect
(see CHANGELOG below) -- never to make a test pass without understanding why it was failing.
The Qualcomm integration adds a real VLMPerceptionProvider implementation and wires the
previously-unused vlm_provider orchestration path in api.extract(), but does not touch
decision/core.py at all.
Repository structure note: the task spec suggesting this integration proposed a
src/alim/ layout; this repo keeps the existing alim/ top-level layout instead, since
renaming it would touch every import and the CI config for no functional benefit, and risk
breaking the 15 previously-passing tests for a cosmetic change. The new pieces
(alim/integrations/qualcomm/, examples/qualcomm_alim_colab.ipynb,
alim/tests/test_qualcomm_integration.py, docs/QUALCOMM.md) follow the spec's intent without
the rename.
- Real bbox-based PDF table extraction (not text flattening), with page number, table bounding box, caption, and preceding-heading preserved per table.
- Table shape classification into 4 concrete categories (
spec_table,single_value_table,device_variant_unsupported,merged_header_unsupported,bitfield_unsupported) plus a catch-all refusal (unknown_unsupported). That is 6 of the 14 shapes named in the original task spec -- see Limitations for the other 8. - Section identity classification with a 3-tier confidence model actually in use
(
VERIFIEDfrom caption,STRONGLY_SUPPORTEDfrom a preceding numbered heading,UNVERIFIEDfrom the table's own header/section text only) plusNONE.SUPPORTEDandAMBIGUOUSexist in the enum for spec compatibility but are not currently produced by any code path -- named, not faked. - Forward-fill for continuation rows, subscript-symbol normalization, and a general multiline-cell-zipping rule for aligned multi-condition cells -- the last of these was only implemented after being independently confirmed on two different real manufacturers' datasheets (DS18B20 and LM35), specifically to avoid a one-off, overfit rule.
- A tri-state decision core (
FOUND/AMBIGUOUS/AMBIGUOUS_MISSING_CONDITION/NOT_FOUND) with dedup, malformed-value rejection, footnote-caveat preservation, and a per-extraction explainable trace. - 15 passing regression tests: 10 decision-layer scenarios (isolated from ingestion, per the "only change the decision core when a test proves a decision-layer defect" rule) and 5 structural-failure-class tests against real PDF fixtures.
- A real end-to-end run across 5 real, distinct-manufacturer datasheets producing correct
VERIFIED/FOUNDresults, correctSCHEMA_UNKNOWNrefusals, and one correctNOT_FOUND-- see benchmark output.
-
Confidence tier on structural_check (VERIFIED/STRONGLY_SUPPORTED/UNVERIFIED/NONE). Found via
test_mislabeled_header_cropped_call_is_ambiguous_not_confidently_wrong: a flat PASS/FAIL/UNKNOWN let a table's own (real, published, wrong) header text produce a confident but incorrect PASS. General because it encodes a document-structure fact true across manufacturers: a caption is more reliable than a table's own header, and a numbered section heading is more reliable than an isolated header cell with no other corroboration. -
AMBIGUOUS_MISSING_CONDITION guard keyed on
conditions_distinguish, notall_verified_pass. Found viatest_split_condition_same_table_flags_missing_condition: the previous guard let two same-table, same-parameter, conflicting-value candidates through as a confidentFOUNDbecause both were caption-verified -- verification of which table is correct is a different question from whether anything explains why the values differ. General because it only depends on condition-text presence/absence, not on any detail of this specific datasheet. -
Leftmost-column parameter fallback in
_build_field_map. Found via the end-to-end run (not a unit test -- a real gap unit tests didn't cover): a table whose header literally doesn't contain the word "Parameter" produced zero candidates, silently, even though it passed schema classification as a legitimate spec table. General because "the leftmost column is the label column" is close to a universal convention in this document class, not specific to the one real datasheet (Nordic nRF24L01+) that exposed it. -
NUM_RE/value_qualityaccepting\u00b1and vulgar-fraction glyphs. Found via the end-to-end run: a real, correct tolerance value (\u00b1\u00bd,\u00b12) was rejected as malformed. General because +/- tolerance notation is standard datasheet convention, not specific to DS18B20. -
Multiline-cell zipping in
candidates/build.py. Implemented only after being seen independently on two different manufacturers (DS18B20's "Input Logic High" dual-condition row, LM35's per-line-aligned condition/value cells) -- the bar set in the task spec for adding a general rule rather than a one-off hack. -
Symbol-based grouping (
_group_key) instead of raw parameter-text grouping, with a same-page guard. Found via the end-to-end run against the real nRF24L01+ "Parameter (condition)"-only table: two rows for the same real parameter (VDD) got differentdetected_parametertext purely because a qualifier ("if input signals >3.6V") is embedded in the one combined column that table has, breaking the multi-condition grouping logic for that table shape. Fix: group by symbol when available. This fix regressed one existing test immediately (test_mislabeled_header_cropped_call_is_ambiguous_not_confidently_wrong) by conflating "same table, missing condition" with "different tables, coincidentally same symbol" -- both now correctly separated by requiring page agreement for the former. General because reference designators are the standard identity signal across this document class, not specific to one manufacturer's column layout. -
unresolved_tablescapped/hidden by default (include_unresolvedflag). Found via the end-to-end run on a 78-page real document: the default successful response was carrying ~150 mostly-irrelevant "unsupported table" entries (pin diagrams, revision history, timing diagrams thatpdfplumberalso detects as grid-shaped). This violated the spec's own requirement that the normal successful case stay simple. Fix: report a count by default, full detail only on request or on an actual refusal. -
VLM fallback orchestration was never actually wired.
api.extract()accepted avlm_providerparameter and never called it -- found while preparing the Qualcomm integration, which is exactly the code path this parameter exists for. Fixed with a two-phase design: decide on structural evidence first; only escalate to the VLM, and only for the specific pages with an unresolved table, if the structural pass didn't already produce a confident answer. The first version of the fix still called the VLM on a page that had already resolved successfully, purely because that same page also contained an unrelated misdetected "table" (a figure) -- caught bytest_vlm_only_called_for_pages_with_unresolved_tables_not_every_pageand fixed by deciding on structural evidence before ever considering escalation. General because it's a sequencing rule (decide first, escalate only on genuine insufficiency), not specific to any one document. -
VLM escalation called EVERY unresolved page, not just relevant ones. Measured on the real nRF24L01+ datasheet: 77 of 78 pages have at least one table-shaped region the schema classifier doesn't recognize (mostly figures/diagrams misdetected as tables, not real spec tables). Escalating to a VLM after a failed structural pass would have called it on up to 77 pages for one query -- directly against "minimum VLM computation necessary." Fixed with
alim/candidates/page_ranking.py: cheap text-overlap scoring (reusing the same symbol/synonym vocabulary the decision core already has) plus a table-of-contents-page bonus when one is detected, capped atmax_vlm_pages(default 5). Verified this actually finds the right page, not just fewer pages: for a real query the structural pass can't answer, the ranked top-5 includes the real page containing that answer, out of 77 candidates.
See docs/QUALCOMM.md for the full record: model selection (Qwen3-VL-4B-Instruct, chosen over
the originally-suggested Qwen2.5-VL-7B-Instruct after checking the current AI Hub catalog
directly), the GenieX/QAIRT runtime path, real published on-device Snapdragon X Elite/X2 Elite
benchmark numbers (cited, not reproduced by this project), and the honest Colab-vs-real-device
distinction. Run examples/qualcomm_alim_colab.ipynb (works immediately in MOCK_MODE = True
with no GPU; set MOCK_MODE = False for a real model run on Colab's GPU). Integration tests:
pytest alim/tests/test_qualcomm_integration.py alim/tests/test_vlm_fallback_orchestration.py -v.
Real-datasheet corpus extended to 22 documents across sensors, an RF transceiver,
regulators, op-amps, a comparator, a timer, an ADC, a DAC, logic ICs, an MCU, memory, and a
power-management IC (fetch_fixtures.py). A structural-only baseline (no VLM,
alim/benchmarks/corpus_baseline.py) was measured across all 22: only 4 resolve with a
confident structural answer, 10 come back SCHEMA_UNKNOWN (the actual target set for VLM
fallback testing), 7 NOT_FOUND, 1 AMBIGUOUS_MISSING_CONDITION. Full numbers and reading in
docs/QUALCOMM.md §6. The notebook's §10 batch cell runs the identical query set through a
configured VLM provider so you can measure how many of the 10 SCHEMA_UNKNOWN cases a real
model resolves.
Research prototype with a validated decision core and a working structural pipeline for supported table shapes.
Why not "beta" or "production-ready":
- The real-datasheet corpus is 5 PDFs across 5 manufacturers, not the 15-20+ across sensors/ regulators/op-amps/ADCs/MCUs/transceivers/memory/logic/power-management the spec calls for. Every number in this repo is honest for n=5, not representative of a larger population.
- No live VLM call has been made anywhere in this project. A real
VLMPerceptionProvider(QualcommVLMProvider) and the orchestration to actually invoke it (api.extract()'s two-phase structural-then-VLM logic) both exist and are tested end-to-end -- but only against a fixture backend returning hand-written JSON, since this environment has no network route to huggingface.co or any model endpoint. The first genuine model call happens when you runexamples/qualcomm_alim_colab.ipynbwithMOCK_MODE = False. Until then, any table the structural pipeline refuses stays refused when no provider is configured. - 8 of the 14 named table schemas are not distinguished; they currently fall into the two generic refusal buckets rather than being correctly identified and extracted.
- No caching across repeated queries against the same document -- every
extract()call re-parses the whole PDF (see benchmark: this roughly doubles latency for 2 queries against one document). Named, not fixed, this session. - Section-identity classification only recognizes the caption-below and numbered-heading-above
conventions. A manufacturer using neither gets
NONEconfidence for every row in that table, meaning the negative-selection safety net provides no protection there -- this is real and currently true for part of DS18B20's own content. - No determinism guarantees have been added beyond "the code has no randomness in it" -- there's no seed control, no explicit tie-breaking rule beyond dict/list iteration order, and no VLM-output validation layer, because there's no VLM output to validate yet.
See LIMITATIONS.md for the full, itemized list against every section of the original task
spec, including what was explicitly out of scope for this session and why.
Apache License 2.0. See LICENSE. Copyright 2026 Rishikesavan a.k.a Youness Yunair.