# `quber.core` — canonical document model and engine seams

## Canonical type: `DoclingDocument`

`docling_core.types.doc.document.DoclingDocument` is the canonical in-memory
document model. Anything implementing the [`Parser`](parsers/base.py) abstract
base class returns a `ParseResult` that carries one on `.document`.

Why:

- Mature DOM with stable provenance + reading-order conventions.
- Schema lives in `docling_core.types.doc` and is maintained upstream.
- We track the latest stable release as the floor, with no upper bound. The
  pinned versions live in `pyproject.toml` and are resolved in `uv.lock`; this
  file never restates them. We refresh deliberately. We accept that docling
  moves; we do not accept being trapped on a stale version.

Scope of this decision: `DoclingDocument` is the canonical type for the
**document-parser** layer's output. It is **not** a universal type that all
engines must produce. Engines that only extract *parts* of a document — for
example Set-of-Mark table-only extraction — sit behind a separate Protocol
([`TableExtractor`](../core/extractors/base.py)) and produce their own
native shapes (`ExtractedTable` Pydantic model).

## Two engine seams

| Seam             | Module                   | Returns                  | Purpose                                                                                            |
| ---------------- | ------------------------ | ------------------------ | -------------------------------------------------------------------------------------------------- |
| `Parser` (ABC)   | `core.parsers`           | `ParseResult`            | Whole-document parsing; the `DoclingDocument` is on `.document`. `DoclingParser` (abstract) -> `TunedFinancialParser` / `LegacyDoclingParser`; `MockParser`. Pick a preset with `parser_for_preset`. |
| `TableExtractor` | `core.extractors`        | `list[ExtractedTable]`   | Table-only extraction. The active engine is `SetOfMarkExtractor`; `MockTableExtractor` serves tests. See `extractors/__init__.py` for the full list.          |

The two seams are decoupled. Composition lives in `core.fusion`: it matches
the Set-of-Mark tables to the docling tables and grafts the Camelot bodies into
a unified `DoclingDocument`, which `quber fuse` writes as `<base>.unified.json`.

## Header consolidation

The header-consolidation logic that builds `TableMetadata` for each table
in a `DoclingDocument` lives in [`consolidation.py`](consolidation.py) as
`HeaderConsolidator`. It is a post-processor over the parser's output,
not part of the parser itself — so the same logic applies regardless of
which `Parser` implementation produced the document.
