Saltar a contenido

Corpus y persistencia

El corazón del modelo de datos: el Corpus (una tabla canónica Arrow con semántica de valor), su Manifest de procedencia, los snapshots sellados y los backends que lo respaldan —puro en memoria para tests, o DuckDB para la biblioteca viva—.

Corpus

Wrapper de semántica de valor sobre un TabularBackend + un Manifest.

Lo que circula por el pipeline: Source lo siembra, el Forager lo expande, el humano lo cura, el Preprocessor lo normaliza, el Store lo persiste (biblioteca viva), los Projectors lo consumen. NO envuelve una base de datos directamente; delega en el TabularBackend inyectado.

Todos los métodos que modifican el contenido devuelven un Corpus nuevo; la instancia original no muta nunca (semántica de valor).

Methods:

Name Description
from_arrow

Valida la tabla con el schema canónico y construye el Corpus.

to_arrow

Devuelve la tabla Arrow del contenido actual.

seeds

Vista de los papers semilla (is_seed == True).

candidates

Vista de los candidatos (curation_status == 'candidate').

accepted

Vista de los papers aceptados (curation_status == 'accepted').

scoped

Devuelve un Corpus nuevo con el subconjunto de filas según el scope.

add_paper

Agrega un paper validando la fila con PaperRow.

with_manifest

Devuelve un Corpus nuevo con el mismo contenido y un Manifest distinto.

merge

Combina dos Corpus deduplicando por id (idempotente).

accept

Marca los papers con los ids dados como 'accepted'.

reject

Marca los papers con los ids dados como 'rejected'.

materialize

Materializa una vista explodida por entidad (author/keyword/institution).

snapshot

Exporta una foto sellada del estado actual (parquet + manifest.json).

Attributes:

Name Type Description
table Table

Tabla Arrow del contenido actual (delegada al backend).

manifest Manifest

Metadatos del Corpus (solo lectura).

table property

table: Table

Tabla Arrow del contenido actual (delegada al backend).

manifest property

manifest: Manifest

Metadatos del Corpus (solo lectura).

from_arrow classmethod

from_arrow(
    table: Table, *, backend: TabularBackend | None = None
) -> Corpus

Valida la tabla con el schema canónico y construye el Corpus.

Falla ruidoso con SchemaError si el schema no coincide (columna faltante, tipo incorrecto o nulo en columna no-nullable).

Parameters:

Name Type Description Default
table Table

Tabla Arrow a envolver.

required
backend TabularBackend | None

Backend a usar. Si es None (default), usa InMemoryBackend. El Hito 3 pasará un DuckDBBackend.

None

Returns:

Type Description
Corpus

Instancia de Corpus validada.

Raises:

Type Description
SchemaError

Si la tabla no cumple el schema canónico.

to_arrow

to_arrow() -> Table

Devuelve la tabla Arrow del contenido actual.

Returns:

Type Description
Table

Tabla Arrow con el schema canónico.

seeds

seeds() -> Table

Vista de los papers semilla (is_seed == True).

Returns:

Type Description
Table

Tabla Arrow filtrada.

candidates

candidates() -> Table

Vista de los candidatos (curation_status == 'candidate').

Returns:

Type Description
Table

Tabla Arrow filtrada.

accepted

accepted() -> Table

Vista de los papers aceptados (curation_status == 'accepted').

Returns:

Type Description
Table

Tabla Arrow filtrada.

scoped

scoped(scope: str) -> Corpus

Devuelve un Corpus nuevo con el subconjunto de filas según el scope.

Vista pura: no muta el original; dos llamadas con el mismo scope devuelven corpora con el mismo corpus_hash.

Valores de scope: - 'all': corpus completo (sin filtrar). - 'accepted': is_seed == True OR curation_status == 'accepted'. - 'seeds_only': is_seed == True.

Parameters:

Name Type Description Default
scope str

Uno de 'all', 'accepted', 'seeds_only'.

required

Returns:

Type Description
Corpus

Nuevo Corpus con el subconjunto de filas.

Raises:

Type Description
ValueError

Si el scope no es reconocido.

add_paper

add_paper(row: dict[str, object]) -> Corpus

Agrega un paper validando la fila con PaperRow.

Si el id no está presente en el dict, lo calcula automáticamente con la lógica de D1' (doi > source_id > título+año, ADR 0036).

Parameters:

Name Type Description Default
row dict[str, object]

Diccionario con los datos del paper. Debe incluir al menos title, is_seed y curation_status.

required

Returns:

Type Description
Corpus

Nuevo Corpus con el paper agregado.

Raises:

Type Description
SchemaError

Si los datos no pasan la validación de PaperRow.

with_manifest

with_manifest(manifest: Manifest) -> Corpus

Devuelve un Corpus nuevo con el mismo contenido y un Manifest distinto.

Semántica de valor: la instancia original no muta nunca. El corpus_hash del resultado es idéntico al del original porque el hash es sobre el contenido (tabla), no sobre el manifest.

Uso típico: actualizar openalex_version, equations, chaining, filters o enrichers en el manifest sin tocar los datos del backend.

Parameters:

Name Type Description Default
manifest Manifest

Nuevo Manifest a asociar al corpus.

required

Returns:

Type Description
Corpus

Nuevo Corpus con el mismo backend y el manifest dado.

merge

merge(other: Corpus) -> Corpus

Combina dos Corpus deduplicando por id (idempotente).

Cuando dos filas comparten id, las fusiona campo a campo según D3: - Escalares: el no-nulo gana; si ambos no-nulos, gana other. - Listas: unión de sets, ordenada y estable. - curation_status: decisión humana más reciente (por decided_at en provenance); si empatan → prioridad accepted > rejected > candidate. - provenance: log append-only (unión de eventos únicos).

merge es idempotente: c.merge(c) produce un Corpus equivalente. El orden de filas resultante es determinista: primero las filas de self en su orden original, luego las filas nuevas de other (las que no estaban en self) en el orden en que aparecen en other.

Parameters:

Name Type Description Default
other Corpus

Corpus a fusionar con este.

required

Returns:

Type Description
Corpus

Nuevo Corpus con las filas fusionadas.

accept

accept(
    ids: list[str],
    *,
    by: str = "human",
    decided_at: datetime | None = None,
) -> Corpus

Marca los papers con los ids dados como 'accepted'.

Agrega un evento al log de provenance con action='accepted', decided_by y decided_at (ISO8601 UTC). El Corpus original no muta (semántica de valor).

R2 (ADR 0017 enmendado): decided_at se inyecta desde la frontera (CLI) para que el núcleo no llame al reloj. Si es None, el backend usa datetime.now(UTC) como conveniencia para uso como librería.

Parameters:

Name Type Description Default
ids list[str]

Lista de id a aceptar.

required
by str

Identificador de quien toma la decisión (default 'human').

'human'
decided_at datetime | None

Instante de la decisión. Si es None, el backend usa datetime.now(UTC) como fallback (ergonomía de librería).

None

Returns:

Type Description
Corpus

Nuevo Corpus con los papers aceptados.

reject

reject(
    ids: list[str],
    *,
    by: str = "human",
    decided_at: datetime | None = None,
    source: str | None = None,
) -> Corpus

Marca los papers con los ids dados como 'rejected'.

Agrega un evento al log de provenance con action='rejected', decided_by, source y decided_at (ISO8601 UTC). El Corpus original no muta (semántica de valor).

R2 (ADR 0017 enmendado): decided_at se inyecta desde la frontera (CLI) para que el núcleo no llame al reloj. Si es None, el backend usa datetime.now(UTC) como conveniencia para uso como librería.

Parameters:

Name Type Description Default
ids list[str]

Lista de id a rechazar.

required
by str

Identificador de quien toma la decisión (default 'human').

'human'
decided_at datetime | None

Instante de la decisión. Si es None, el backend usa datetime.now(UTC) como fallback (ergonomía de librería).

None
source str | None

Origen del evento de rechazo (p. ej. criterio de filtro PRISMA como 'filter:language:in:["fr"]'). Si es None, el campo source del evento queda vacío.

None

Returns:

Type Description
Corpus

Nuevo Corpus con los papers rechazados.

materialize

materialize(
    view: Literal["author", "keyword", "institution"],
) -> Table

Materializa una vista explodida por entidad (author/keyword/institution).

Devuelve una tabla Arrow con una fila por (paper_id, entidad), útil para análisis de co-autoría o co-ocurrencia sin salir de Arrow.

Parameters:

Name Type Description Default
view Literal['author', 'keyword', 'institution']

Nombre de la vista a materializar.

required

Returns:

Type Description
Table

Tabla Arrow con columnas paper_id + la columna de la entidad.

Raises:

Type Description
ValueError

Si el nombre de vista no es reconocido.

snapshot

snapshot(path: Path) -> CorpusSnapshot

Exporta una foto sellada del estado actual (parquet + manifest.json).

Crea la carpeta path si no existe, escribe corpus.parquet y manifest.json con el corpus_hash calculado (D2). NO es la persistencia (eso es el Store DuckDB); es un export derivable.

Parameters:

Name Type Description Default
path Path

Directorio donde escribir el snapshot.

required

Returns:

Type Description
CorpusSnapshot

CorpusSnapshot apuntando al directorio recién escrito.

Manifest

Bases: BaseModel

Metadatos sellados del Corpus.

Se serializa a manifest.json junto al corpus.parquet del snapshot. Los campos obligatorios no tienen default; los opcionales sí (D5).

CorpusSnapshot

Carpeta con corpus.parquet + manifest.json: export sellado.

Reproducible y versionable. No es la biblioteca viva, es su foto. El corpus se reconstruye en cada acceso desde el parquet (property lazy).

Methods:

Name Description
load

Carga un snapshot desde un directorio.

Attributes:

Name Type Description
path Path

Directorio del snapshot.

manifest Manifest

Manifest del snapshot.

corpus Corpus

Reconstruye el Corpus desde el parquet del snapshot.

path property

path: Path

Directorio del snapshot.

manifest property

manifest: Manifest

Manifest del snapshot.

corpus property

corpus: Corpus

Reconstruye el Corpus desde el parquet del snapshot.

Returns:

Type Description
Corpus

Corpus reconstruido desde corpus.parquet.

load classmethod

load(path: Path) -> CorpusSnapshot

Carga un snapshot desde un directorio.

Lee manifest.json y verifica que corpus.parquet exista.

Parameters:

Name Type Description Default
path Path

Directorio del snapshot.

required

Returns:

Type Description
CorpusSnapshot

CorpusSnapshot apuntando al directorio dado.

Raises:

Type Description
FileNotFoundError

Si faltan archivos requeridos en el directorio.

SchemaError

Si el manifest.json no es válido.

SchemaError

Bases: Exception

Violación del schema canónico Arrow del Corpus.

Se lanza con el nombre de la columna y la descripción del problema para que el mensaje sea accionable (lección 7 de v0: fallar fuerte y ruidoso).

TabularBackend

Bases: Protocol

Contrato de almacenamiento tabular para el Corpus.

Cualquier implementación que satisfaga este Protocol puede respaldar un Corpus. Las reglas de identidad/hash/merge del ADR 0013 (D1/D2/D3) son contrato de este Protocol: cada implementación debe cumplirlas.

Implementaciones actuales: - InMemoryBackend (núcleo puro, sin I/O — Hito 1.5). - DuckDBBackend (biblioteca viva persistida — Hito 3).

Invariantes que toda implementación debe garantizar

D1 id estable y determinista (_compute_id de schemas). D2 corpus_hash order-independent: mismas filas en distinto orden → mismo hash. D3 merge idempotente: orden por primera aparición, dedup por id, resolución de curation_status por decisión humana más reciente.

Methods:

Name Description
to_arrow

Exporta el contenido completo como tabla Arrow canónica.

add_paper

Agrega una fila ya validada y devuelve un backend nuevo.

merge

Fusiona other_table en este backend (D3).

apply_curation

Aplica accept/reject a los papers indicados y devuelve backend nuevo.

filter_view

Devuelve una tabla Arrow filtrada por la vista pedida.

corpus_hash

Computa el hash order-independent del contenido (D2).

add_referenced_refs

Appendea IDs backward observados a referenced_but_not_fetched (#54).

referenced_refs_count

Número de IDs en referenced_but_not_fetched.

referenced_refs

Lista de IDs en referenced_but_not_fetched, en orden de inserción.

add_external_id

Registra un ID externo para un paper dado un motor (ADR 0036 opción C).

external_ids_for

Devuelve todos los IDs externos registrados para un paper.

all_external_ids

Devuelve todas las entradas de la tabla external_ids.

to_arrow

to_arrow() -> Table

Exporta el contenido completo como tabla Arrow canónica.

Es el puente estable hacia los proyectores y analizadores puros.

Returns:

Type Description
Table

Tabla Arrow con el schema canónico del Corpus.

add_paper

add_paper(row: dict[str, object]) -> TabularBackend

Agrega una fila ya validada y devuelve un backend nuevo.

El id debe estar calculado antes de llamar este método; la validación PaperRow corre en Corpus.add_paper.

Parameters:

Name Type Description Default
row dict[str, object]

Diccionario con todos los campos del schema canónico.

required

Returns:

Type Description
TabularBackend

Nueva instancia del backend con el paper agregado.

merge

merge(other_table: Table) -> TabularBackend

Fusiona other_table en este backend (D3).

Devuelve un backend nuevo con el resultado de la fusión. El orden de filas resultante es determinista: primero las filas de self en su orden original, luego las filas nuevas de other_table.

Parameters:

Name Type Description Default
other_table Table

Tabla Arrow a fusionar.

required

Returns:

Type Description
TabularBackend

Nueva instancia del backend con las filas fusionadas.

apply_curation

apply_curation(
    ids: list[str],
    *,
    action: str,
    by: str,
    decided_at: str | None = None,
    source: str | None = None,
) -> TabularBackend

Aplica accept/reject a los papers indicados y devuelve backend nuevo.

Agrega un evento al log provenance con action, decided_by, source y decided_at (ISO8601 UTC). La instancia original no muta.

R2 (ADR 0017 enmendado): decided_at se inyecta desde la frontera (CLI) para que el núcleo no llame al reloj. Si es None, la implementación usa datetime.now(UTC) como fallback (ergonomía para uso como librería sin frontera CLI).

Parameters:

Name Type Description Default
ids list[str]

Lista de id a actualizar.

required
action str

'accepted' o 'rejected'.

required
by str

Identificador de quien toma la decisión.

required
decided_at str | None

Timestamp ISO8601 UTC de la decisión. Si es None, la implementación usa datetime.now(UTC) como fallback.

None
source str | None

Origen del evento (p. ej. criterio de filtro PRISMA). Si es None, el campo source del evento queda vacío.

None

Returns:

Type Description
TabularBackend

Nueva instancia del backend con la curación aplicada.

filter_view

filter_view(
    view: Literal["seeds", "candidates", "accepted"],
) -> Table

Devuelve una tabla Arrow filtrada por la vista pedida.

Parameters:

Name Type Description Default
view Literal['seeds', 'candidates', 'accepted']

'seeds' (is_seed == True), 'candidates' (curation_status == 'candidate'), o 'accepted' (curation_status == 'accepted').

required

Returns:

Type Description
Table

Tabla Arrow filtrada.

corpus_hash

corpus_hash() -> str

Computa el hash order-independent del contenido (D2).

Returns:

Type Description
str

Hexdigest SHA-256 del contenido de la tabla.

add_referenced_refs

add_referenced_refs(
    ref_ids: list[str], *, cycle_round: int
) -> int

Appendea IDs backward observados a referenced_but_not_fetched (#54).

Solo inserta los IDs que aún no están en la tabla (idempotente).

Parameters:

Name Type Description Default
ref_ids list[str]

IDs de OpenAlex observados en backward chaining.

required
cycle_round int

Número de ronda del ciclo en curso.

required

Returns:

Type Description
int

Número de IDs nuevos insertados.

referenced_refs_count

referenced_refs_count() -> int

Número de IDs en referenced_but_not_fetched.

Returns:

Type Description
int

Conteo total de filas en la tabla auxiliar.

referenced_refs

referenced_refs() -> list[str]

Lista de IDs en referenced_but_not_fetched, en orden de inserción.

Returns:

Type Description
list[str]

Lista de ref_id.

add_external_id

add_external_id(
    paper_id: str, engine: str, id: str
) -> None

Registra un ID externo para un paper dado un motor (ADR 0036 opción C).

Idempotente: si ya existe una entrada (paper_id, engine), el valor se reemplaza (un ID por motor por paper).

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required
engine str

Nombre del motor / fuente del ID (p. ej. 'openalex', 'semanticscholar', 'doi').

required
id str

El ID externo correspondiente a ese motor.

required

external_ids_for

external_ids_for(paper_id: str) -> dict[str, str]

Devuelve todos los IDs externos registrados para un paper.

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required

Returns:

Type Description
dict[str, str]

Diccionario {engine: id} con todos los IDs registrados para

dict[str, str]

ese paper. Vacío si el paper no tiene IDs externos registrados.

all_external_ids

all_external_ids() -> list[tuple[str, str, str]]

Devuelve todas las entradas de la tabla external_ids.

Returns:

Type Description
list[tuple[str, str, str]]

Lista de tuplas (paper_id, engine, id) en orden no definido.

InMemoryBackend

Backend puro en Python: almacena el corpus como pa.Table en memoria.

Preserva la lógica de mutación del Corpus del Hito 1 (to_pylist() → mutar en Python → reconstruir la tabla Arrow). No tiene I/O ni dependencias externas; es el backend de referencia para los tests.

Semántica de valor: todas las operaciones mutantes devuelven una nueva instancia; la original no cambia nunca.

Ref #54: almacena también los IDs backward observados en _referenced_refs (lista en memoria, equivalente a la tabla referenced_but_not_fetched de DuckDBBackend).

Methods:

Name Description
to_arrow

Devuelve la tabla Arrow interna.

add_paper

Agrega una fila al backend y devuelve una nueva instancia.

merge

Fusiona other_table respetando D3 y devuelve una nueva instancia.

apply_curation

Aplica accept/reject a los ids indicados y devuelve una nueva instancia.

filter_view

Devuelve la tabla filtrada según la vista pedida.

corpus_hash

Computa el hash order-independent del contenido (D2).

add_referenced_refs

Appendea IDs backward observados a la tabla auxiliar en memoria.

referenced_refs_count

Número de IDs en la tabla auxiliar.

referenced_refs

Lista de IDs en la tabla auxiliar, en orden de inserción.

add_external_id

Registra un ID externo para un paper dado un motor.

external_ids_for

Devuelve todos los IDs externos registrados para un paper.

all_external_ids

Devuelve todas las entradas de la tabla external_ids.

to_arrow

to_arrow() -> Table

Devuelve la tabla Arrow interna.

Returns:

Type Description
Table

Tabla Arrow con el schema canónico.

add_paper

add_paper(row: dict[str, object]) -> InMemoryBackend

Agrega una fila al backend y devuelve una nueva instancia.

Parameters:

Name Type Description Default
row dict[str, object]

Fila ya validada con todos los campos del schema.

required

Returns:

Type Description
InMemoryBackend

Nueva instancia con el paper agregado.

merge

merge(other_table: Table) -> InMemoryBackend

Fusiona other_table respetando D3 y devuelve una nueva instancia.

Orden: filas de self primero, luego filas nuevas de other_table.

Parameters:

Name Type Description Default
other_table Table

Tabla Arrow a fusionar.

required

Returns:

Type Description
InMemoryBackend

Nueva instancia con las filas fusionadas.

apply_curation

apply_curation(
    ids: list[str],
    *,
    action: str,
    by: str,
    decided_at: str | None = None,
    source: str | None = None,
) -> InMemoryBackend

Aplica accept/reject a los ids indicados y devuelve una nueva instancia.

R2: decided_at se inyecta desde la frontera (CLI). Si es None, _apply_curation_to_rows usa datetime.now(UTC) como fallback.

Parameters:

Name Type Description Default
ids list[str]

Lista de id a actualizar.

required
action str

'accepted' o 'rejected'.

required
by str

Identificador de quien decide.

required
decided_at str | None

Timestamp ISO8601 UTC de la decisión (inyectado desde la frontera CLI; None = fallback a datetime.now(UTC)).

None
source str | None

Origen del evento (p. ej. criterio de filtro PRISMA). Si es None, el campo source del evento queda vacío.

None

Returns:

Type Description
InMemoryBackend

Nueva instancia con la curación aplicada.

filter_view

filter_view(
    view: Literal["seeds", "candidates", "accepted"],
) -> Table

Devuelve la tabla filtrada según la vista pedida.

Parameters:

Name Type Description Default
view Literal['seeds', 'candidates', 'accepted']

'seeds', 'candidates' o 'accepted'.

required

Returns:

Type Description
Table

Tabla Arrow filtrada.

Raises:

Type Description
ValueError

Si el nombre de vista no es reconocido.

corpus_hash

corpus_hash() -> str

Computa el hash order-independent del contenido (D2).

Returns:

Type Description
str

Hexdigest SHA-256 del contenido de la tabla.

add_referenced_refs

add_referenced_refs(
    ref_ids: list[str], *, cycle_round: int
) -> int

Appendea IDs backward observados a la tabla auxiliar en memoria.

Solo inserta los que aún no están (idempotente). cycle_round se acepta por compatibilidad con el protocolo pero no se persiste en memoria (el backend en memoria no necesita auditoría de ronda).

Parameters:

Name Type Description Default
ref_ids list[str]

IDs de OpenAlex observados en backward chaining.

required
cycle_round int

Número de ronda del ciclo en curso (ignorado en memoria).

required

Returns:

Type Description
int

Número de IDs nuevos insertados.

referenced_refs_count

referenced_refs_count() -> int

Número de IDs en la tabla auxiliar.

Returns:

Type Description
int

Conteo total.

referenced_refs

referenced_refs() -> list[str]

Lista de IDs en la tabla auxiliar, en orden de inserción.

Returns:

Type Description
list[str]

Lista de ref_id.

add_external_id

add_external_id(
    paper_id: str, engine: str, id: str
) -> None

Registra un ID externo para un paper dado un motor.

Idempotente: si ya existe una entrada (paper_id, engine), el valor se reemplaza (un ID por motor por paper).

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required
engine str

Nombre del motor / fuente del ID (p. ej. 'openalex', 'semanticscholar', 'doi').

required
id str

El ID externo correspondiente a ese motor.

required

external_ids_for

external_ids_for(paper_id: str) -> dict[str, str]

Devuelve todos los IDs externos registrados para un paper.

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required

Returns:

Type Description
dict[str, str]

Diccionario {engine: id} con todos los IDs registrados para

dict[str, str]

ese paper. Vacío si el paper no tiene IDs externos registrados.

all_external_ids

all_external_ids() -> list[tuple[str, str, str]]

Devuelve todas las entradas de la tabla external_ids.

Usado internamente para tests de paridad con DuckDBBackend.

Returns:

Type Description
list[tuple[str, str, str]]

Lista de tuplas (paper_id, engine, id) en orden no definido.

DuckDBStore

Fachada de persistencia sobre DuckDBBackend (ADR 0009, 0015).

Implementa el Protocol Store: persist / load.

Parameters:

Name Type Description Default
path str | Path

Ruta al archivo .duckdb de la biblioteca viva.

required

Methods:

Name Description
persist

Persiste el corpus en la biblioteca viva (idempotente).

persist_replace

Reemplaza toda la tabla corpus con el contenido de corpus.

load

Carga el corpus acumulado desde la biblioteca viva.

close

Cierra la conexión DuckDB y libera el lock de archivo.

Attributes:

Name Type Description
backend DuckDBBackend

Acceso directo al DuckDBBackend subyacente.

backend property

backend: DuckDBBackend

Acceso directo al DuckDBBackend subyacente.

Permite usar extensiones propias como loop_state(), set_loop_state() y query(sql).

Returns:

Type Description
DuckDBBackend

El DuckDBBackend de esta biblioteca viva.

persist

persist(corpus: Corpus) -> None

Persiste el corpus en la biblioteca viva (idempotente).

Hace un merge del corpus entrante en el backend persistido. Idempotente: persistir el mismo corpus dos veces no duplica filas (el upsert por id garantiza la idempotencia, D1/D3).

Parameters:

Name Type Description Default
corpus Corpus

El Corpus a persistir.

required

persist_replace

persist_replace(corpus: Corpus) -> None

Reemplaza toda la tabla corpus con el contenido de corpus.

Equivale a TRUNCATE + INSERT: el estado en disco queda siendo exactamente el corpus dado, sin residuos de variantes previas. Preserva las tablas hermanas (loop_state_log, referenced_but_not_fetched).

Úsalo en la ruta de ingesta (seed, restore, chain, thesaurus) donde ya tenés el corpus completo y deduplcado en memoria. Para el caso «acumular papers de una nueva fuente sin dedup cross-biblioteca», seguí usando persist (upsert-concat D3).

Parameters:

Name Type Description Default
corpus Corpus

El Corpus completo y final a persistir.

required

load

load() -> Corpus

Carga el corpus acumulado desde la biblioteca viva.

Devuelve un Corpus respaldado por el DuckDBBackend del archivo; las operaciones subsecuentes (accept, reject, merge) mutarán el archivo en disco.

Ref #126: reconstruye manifest.filters desde filter_log para que los pasos PRISMA persistan entre sesiones. Ref #141: reconstruye manifest.enrichers desde enricher_log para que los EnricherRef persistan entre sesiones.

Returns:

Type Description
Corpus

El Corpus acumulado en el store.

close

close() -> None

Cierra la conexión DuckDB y libera el lock de archivo.

Delega en DuckDBBackend.close(). Idempotente: llamarlo varias veces no lanza error. Debe llamarse explícitamente en comandos que abren el store y terminan (run_seed_from_bib, run_seed, etc.) para garantizar que el lock se libera antes de la siguiente apertura en el mismo proceso, especialmente en Linux donde DuckDB no libera el lock al hacer GC del objeto.

DuckDBBackend

Backend de biblioteca viva persistida en DuckDB (ADR 0009, 0015).

Implementa el Protocol TabularBackend con mutaciones por SQL puro (INSERT … ON CONFLICT DO UPDATE por id), cumpliendo D1/D2/D3 (ADR 0013).

Semántica de valor: todas las operaciones mutantes (add_paper, merge, apply_curation) devuelven una nueva instancia que comparte la misma ruta de archivo pero refleja el estado actualizado. Internamente cada instancia tiene su propia conexión; la semántica de valor se mantiene porque cada operación crea una nueva instancia.

CycleState (ADR 0016): extensión propia con loop_state() y set_loop_state().

ADR 0024: el orden D3 se garantiza mediante la columna interna _seq (número de secuencia de primera aparición). to_arrow() devuelve exactamente CORPUS_SCHEMA (sin _seq) ordenado por _seq.

Parameters:

Name Type Description Default
table Table | None

Tabla Arrow inicial. Si se pasa, se hace upsert de sus filas en la base de datos. Permite inicializar desde pa.Table (patrón que usa la suite de contrato).

None
path str | Path | None

Ruta al archivo .duckdb. Si es None (default), se usa :memory:.

None

Methods:

Name Description
to_arrow

Exporta el contenido completo como tabla Arrow canónica.

add_paper

Agrega (upsert) una fila al backend y devuelve una nueva instancia.

merge

Fusiona other_table respetando D3 y devuelve una nueva instancia.

apply_curation

Aplica accept/reject a los papers indicados y devuelve backend nuevo.

filter_view

Devuelve la tabla filtrada según la vista pedida.

corpus_hash

Computa el hash order-independent del contenido (D2).

loop_state

Estado actual del lazo de investigación.

loop_round

Número de ronda actual del lazo.

set_loop_state

Registra una transición de CycleState (transición permisiva).

add_referenced_refs

Appendea IDs backward observados a referenced_but_not_fetched.

referenced_refs_count

Número de IDs en referenced_but_not_fetched.

referenced_refs

Lista de IDs en referenced_but_not_fetched, ordenados por observed_at.

add_external_id

Registra un ID externo para un paper dado un motor.

external_ids_for

Devuelve todos los IDs externos registrados para un paper.

all_external_ids

Devuelve todas las entradas de la tabla external_ids.

persist_filter_steps

Persiste los pasos de filtro PRISMA en filter_log.

load_filter_steps

Carga los pasos de filtro PRISMA desde filter_log.

persist_enricher_refs

Persiste las referencias de enriquecedor en enricher_log.

load_enricher_refs

Carga las referencias de enriquecedor desde enricher_log.

close

Cierra la conexión DuckDB subyacente y libera el lock de archivo.

overwrite_corpus

Reemplaza TODA la tabla corpus con el contenido de table.

query

Ejecuta una consulta SQL sobre el backend y devuelve tabla Arrow.

to_arrow

to_arrow() -> Table

Exporta el contenido completo como tabla Arrow canónica.

Impone el schema exacto de CORPUS_SCHEMA (orden y tipos). Valida con validate_table antes de devolver.

Returns:

Type Description
Table

Tabla Arrow con el schema canónico del Corpus (sin _seq).

add_paper

add_paper(row: dict[str, object]) -> DuckDBBackend

Agrega (upsert) una fila al backend y devuelve una nueva instancia.

ADR 0024: asigna _seq = COALESCE(MAX(_seq), 0) + 1 para que la fila nueva quede al final del orden de primera aparición (D3).

Parameters:

Name Type Description Default
row dict[str, object]

Fila ya validada con todos los campos del schema.

required

Returns:

Type Description
DuckDBBackend

Nueva instancia con el paper agregado.

merge

merge(other_table: Table) -> DuckDBBackend

Fusiona other_table respetando D3 y devuelve una nueva instancia.

ADR 0024: el orden D3 (filas de self primero en su orden original, luego las nuevas filas de other_table en su orden de aparición) se garantiza por la columna interna _seq: - Filas de self ya tienen _seq asignado (se preservan en el UPDATE, que no actualiza _seq). - Filas nuevas de other_table reciben _seq mayor, calculado con ROW_NUMBER() OVER (ORDER BY _row_idx) desde MAX(_seq) en _upsert_table; la columna auxiliar _row_idx (índice 0-based del lote Arrow) garantiza que el orden de primera aparición se preserve de forma determinista, sin depender del scan de DuckDB. - _arrow_table_from_con lee con ORDER BY _seq. No se requiere DELETE+reinsert.

Parameters:

Name Type Description Default
other_table Table

Tabla Arrow a fusionar.

required

Returns:

Type Description
DuckDBBackend

Nueva instancia con las filas fusionadas, en orden D3.

apply_curation

apply_curation(
    ids: list[str],
    *,
    action: str,
    by: str,
    decided_at: str | None = None,
    source: str | None = None,
) -> DuckDBBackend

Aplica accept/reject a los papers indicados y devuelve backend nuevo.

Reutiliza _apply_curation_to_rows de backends.memory para garantizar equivalencia exacta con InMemoryBackend.

R2: decided_at se inyecta desde la frontera (CLI). Si es None, _apply_curation_to_rows usa datetime.now(UTC) como fallback.

Parameters:

Name Type Description Default
ids list[str]

Lista de id a actualizar.

required
action str

'accepted' o 'rejected'.

required
by str

Identificador de quien decide.

required
decided_at str | None

Timestamp ISO8601 UTC de la decisión (inyectado desde la frontera CLI; None = fallback a datetime.now(UTC)).

None
source str | None

Origen del evento (p. ej. criterio de filtro PRISMA). Si es None, el campo source del evento queda vacío.

None

Returns:

Type Description
DuckDBBackend

Nueva instancia con la curación aplicada.

filter_view

filter_view(
    view: Literal["seeds", "candidates", "accepted"],
) -> Table

Devuelve la tabla filtrada según la vista pedida.

ADR 0024: usa SELECT * EXCLUDE (_seq) … ORDER BY _seq para mantener el orden D3 y excluir la columna interna del resultado.

Parameters:

Name Type Description Default
view Literal['seeds', 'candidates', 'accepted']

'seeds', 'candidates' o 'accepted'.

required

Returns:

Type Description
Table

Tabla Arrow filtrada.

Raises:

Type Description
ValueError

Si la vista no es reconocida.

corpus_hash

corpus_hash() -> str

Computa el hash order-independent del contenido (D2).

Se computa siempre sobre to_arrow() usando la misma función que InMemoryBackend (ADR 0015, ADR 0013 D2).

Returns:

Type Description
str

Hexdigest SHA-256 del contenido de la tabla.

loop_state

loop_state() -> CycleState | None

Estado actual del lazo de investigación.

Lee la última fila de loop_state_log (log append-only).

Returns:

Type Description
CycleState | None

El CycleState actual, o None si no hay transiciones aún.

loop_round

loop_round() -> int

Número de ronda actual del lazo.

Lee la columna round de la última fila de loop_state_log. Devuelve 0 cuando no hay transiciones (sin estado previo) o cuando la columna es NULL (bases migradas desde antes de R3).

Returns:

Type Description
int

Entero >= 0. 0 = sin estado; 1 = primera ronda; 2+ = re-sembrados.

set_loop_state

set_loop_state(
    state: CycleState, *, cycle_round: int | None = None
) -> None

Registra una transición de CycleState (transición permisiva).

Agrega una fila al log append-only loop_state_log. No bloquea ningún salto (ADR 0016: transiciones permisivas).

R3: persiste también el número de ronda. Si cycle_round es None, conserva la ronda actual (útil para transiciones dentro de la misma ronda, p. ej. chain/filter/build).

La curación (accept/reject) es TRANSVERSAL y NO llama set_loop_state: no transiciona el lazo.

Parameters:

Name Type Description Default
state CycleState

El nuevo estado del lazo.

required
cycle_round int | None

Número de ronda a persistir. Si es None, usa la ronda actual del log.

None

add_referenced_refs

add_referenced_refs(
    ref_ids: list[str], *, cycle_round: int
) -> int

Appendea IDs backward observados a referenced_but_not_fetched.

No duplica IDs ya presentes (idempotencia basada en existencia): solo inserta los que aún no están en la tabla, independientemente del cycle_round. El observed_at lo provee now() de DuckDB.

Parameters:

Name Type Description Default
ref_ids list[str]

IDs de OpenAlex observados en backward chaining.

required
cycle_round int

Número de ronda del ciclo en curso.

required

Returns:

Type Description
int

Número de IDs nuevos insertados (0 si todos ya existían).

referenced_refs_count

referenced_refs_count() -> int

Número de IDs en referenced_but_not_fetched.

Returns:

Type Description
int

Conteo total de filas en la tabla auxiliar.

referenced_refs

referenced_refs() -> list[str]

Lista de IDs en referenced_but_not_fetched, ordenados por observed_at.

Returns:

Type Description
list[str]

Lista de ref_id en orden de inserción.

add_external_id

add_external_id(
    paper_id: str, engine: str, id: str
) -> None

Registra un ID externo para un paper dado un motor.

Idempotente: si ya existe una entrada (paper_id, engine), el valor se reemplaza (un ID por motor por paper). La PK lógica (paper_id, engine) garantiza que no haya duplicados.

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required
engine str

Nombre del motor / fuente del ID (p. ej. 'openalex', 'semanticscholar', 'doi').

required
id str

El ID externo correspondiente a ese motor.

required

external_ids_for

external_ids_for(paper_id: str) -> dict[str, str]

Devuelve todos los IDs externos registrados para un paper.

Parameters:

Name Type Description Default
paper_id str

ID interno del paper en el corpus.

required

Returns:

Type Description
dict[str, str]

Diccionario {engine: id} con todos los IDs registrados para

dict[str, str]

ese paper. Vacío si el paper no tiene IDs externos registrados.

all_external_ids

all_external_ids() -> list[tuple[str, str, str]]

Devuelve todas las entradas de la tabla external_ids.

Usado internamente para _clone() y tests de paridad.

Returns:

Type Description
list[tuple[str, str, str]]

Lista de tuplas (paper_id, engine, id) en orden no definido.

persist_filter_steps

persist_filter_steps(
    steps: list[object], *, replace: bool = True
) -> None

Persiste los pasos de filtro PRISMA en filter_log.

Ref #126 — trazabilidad PRISMA: los pasos aplicados en b2g filter se guardan para que manifest.filters sobreviva entre cargas del store.

Por defecto (replace=True) limpia los pasos anteriores antes de insertar los nuevos: cada invocación de b2g filter reemplaza el registro previo completo (idempotencia de run_filter).

Parameters:

Name Type Description Default
steps list[object]

Lista de FilterStep (se accede a sus atributos name, criteria, count_before, count_after).

required
replace bool

Si es True (default), trunca filter_log antes de insertar. Si es False, appendea (útil para tests que verifican acumulación).

True

load_filter_steps

load_filter_steps() -> list[dict[str, object]]

Carga los pasos de filtro PRISMA desde filter_log.

Ref #126 — trazabilidad PRISMA: permite que DuckDBStore.load() reconstruya manifest.filters con los pasos persistidos.

Returns:

Type Description
list[dict[str, object]]

Lista de dicts con las claves name, criteria,

list[dict[str, object]]

count_before y count_after en el orden de inserción.

persist_enricher_refs

persist_enricher_refs(
    refs: list[object], *, replace: bool = True
) -> None

Persiste las referencias de enriquecedor en enricher_log.

Ref #141 — trazabilidad de enriquecimiento: los EnricherRef aplicados en b2g enrich / b2g chain / b2g build se guardan para que manifest.enrichers sobreviva entre cargas del store.

Por defecto (replace=True) limpia las entradas anteriores antes de insertar las nuevas: cada invocación reemplaza el registro completo (idempotencia; el EnricherRef se identifica por nombre, ADR 0025).

Parameters:

Name Type Description Default
refs list[object]

Lista de EnricherRef (se accede a sus atributos name y params).

required
replace bool

Si es True (default), trunca enricher_log antes de insertar. Si es False, appendea.

True

load_enricher_refs

load_enricher_refs() -> list[dict[str, Any]]

Carga las referencias de enriquecedor desde enricher_log.

Ref #141 — trazabilidad de enriquecimiento: permite que DuckDBStore.load() reconstruya manifest.enrichers con los EnricherRef persistidos.

Returns:

Type Description
list[dict[str, Any]]

Lista de dicts con las claves name y params (dict

list[dict[str, Any]]

str→str) en el orden de inserción.

close

close() -> None

Cierra la conexión DuckDB subyacente y libera el lock de archivo.

Idempotente: llamarlo varias veces o sobre una conexión ya cerrada no lanza error. Necesario en contextos donde se abren múltiples instancias sobre el mismo archivo en el mismo proceso (p. ej. dos llamadas consecutivas a run_seed_from_bib sobre el mismo path): Linux/DuckDB mantiene el lock de archivo hasta que la conexión se cierra explícitamente; depender del GC causa segfault.

overwrite_corpus

overwrite_corpus(table: Table) -> None

Reemplaza TODA la tabla corpus con el contenido de table.

Hace TRUNCATE + INSERT masivo (no upsert fila-a-fila) para que el estado en disco sea exactamente table, sin residuos de filas previas. Preserva las tablas hermanas (loop_state_log, referenced_but_not_fetched).

Úsalo solo en la ruta de ingesta (seed, restore, chain, thesaurus) donde ya tenés el corpus completo y correcto en memoria. El upsert normal (_upsert_table) sigue siendo correcto para el caso «mismo paper desde dos fuentes» (D3); este método NO lo reemplaza en ese contexto.

ADR 0024: reasigna _seq desde 0 sobre la tabla limpia, manteniendo el orden de filas que viene en table (que ya salió de to_arrow() con ORDER BY _seq original).

Parameters:

Name Type Description Default
table Table

Tabla Arrow con exactamente el contenido final a persistir. Debe cumplir CORPUS_SCHEMA.

required

query

query(sql: str) -> Table

Ejecuta una consulta SQL sobre el backend y devuelve tabla Arrow.

Solo para lectura (no muta el estado).

Parameters:

Name Type Description Default
sql str

Sentencia SQL SELECT a ejecutar.

required

Returns:

Type Description
Table

Resultado como tabla Arrow.