Saltar a contenido

API de Python

Referencia de la superficie pública de la librería bib2graph, agrupada por tema. Se autogenera desde los docstrings del código (mkdocstrings): si cambia el código, cambia esta página.

Para usar la herramienta desde la terminal, mira la referencia del CLI b2g.

Corpus y persistencia

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).

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() -> pa.Table

Devuelve la tabla Arrow del contenido actual.

Returns:

Type Description
Table

Tabla Arrow con el schema canónico.

seeds

seeds() -> pa.Table

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

Returns:

Type Description
Table

Tabla Arrow filtrada.

candidates

candidates() -> pa.Table

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

Returns:

Type Description
Table

Tabla Arrow filtrada.

accepted

accepted() -> pa.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"],
) -> pa.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).

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.

to_arrow

to_arrow() -> pa.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"],
) -> pa.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.

54: almacena también los IDs backward observados en _referenced_refs

(lista en memoria, equivalente a la tabla referenced_but_not_fetched de DuckDBBackend).

to_arrow

to_arrow() -> pa.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"],
) -> pa.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

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.

126: reconstruye manifest.filters desde filter_log para que

los pasos PRISMA persistan entre sesiones.

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

to_arrow

to_arrow() -> pa.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"],
) -> pa.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.

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.

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.

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.

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) -> pa.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.

Fuentes (siembra)

Source

Bases: Protocol

Convierte una entrada externa en un Corpus.

El acceso a campos es DEFENSIVO (sin KeyError). Debe entregar al menos el MÍNIMO UNIVERSAL (id, title, year, authors_raw, keywords_raw); el enriquecimiento (refs/citantes/afiliaciones/ instituciones) es OPCIONAL (ADR 0018).

seed

seed(query: str) -> SeedResult

Siembra desde una ecuación de búsqueda.

Parameters:

Name Type Description Default
query str

Ecuación de búsqueda (WoS-style o nativa OpenAlex).

required

Returns:

Type Description
SeedResult

SeedResult con el corpus, la query ejecutada y el reporte

SeedResult

de traducción.

load

load(path: str) -> Corpus

Siembra desde un archivo (export/pearls).

Parameters:

Name Type Description Default
path str

Ruta al archivo de entrada.

required

Returns:

Type Description
Corpus

Corpus con is_seed=True en todas las filas.

OpenAlexSource

Siembra un Corpus desde la API de OpenAlex.

Ejemplo::

source = OpenAlexSource(email="yo@example.com")
result = source.seed('"unequal exchange" AND trade')
print(result.executed_query)
print(result.translation_report)

Credenciales (ADR 0012): email y api_key se inyectan; nunca embebidos en el código. Resolución: argumento > OPENALEX_API_KEY > ausencia → polite pool.

El transport permite pasar un httpx.MockTransport en tests (sin red en CI).

seed

seed(
    query: str,
    *,
    native: bool = False,
    exclude: list[str] | None = None,
    min_year: int | None = None,
    max_year: int | None = None,
) -> SeedResult

Siembra un Corpus desde una ecuación de búsqueda.

cited_by_id queda [] en el corpus sembrado: OpenAlex no lo entrega inline; lo pueblan el Forager/Enricher en Hito 5/8. references_id SÍ se trae inline (referenced_works).

Parameters:

Name Type Description Default
query str

Ecuación de búsqueda (WoS-style o nativa OpenAlex).

required
native bool

Si es True, pasa la query cruda sin traducción.

False
exclude list[str] | None

Lista de términos a excluir de título/abstract (#30). Cada término genera AND NOT "…" dentro del paréntesis de title_and_abstract.search.

None
min_year int | None

Año mínimo de publicación (filtro de rango OpenAlex). Genera from_publication_date:<min_year>-01-01. None = sin límite inferior.

None
max_year int | None

Año máximo de publicación (filtro de rango OpenAlex). Genera to_publication_date:<max_year>-12-31. None = sin límite superior.

None

Returns:

Type Description
SeedResult

SeedResult con el corpus, la query ejecutada y el reporte.

fetch_citing

fetch_citing(openalex_id: str) -> list[dict[str, Any]]

Trae los works que citan al paper con openalex_id dado.

Usa GET /works?filter=cites:{openalex_id} con paginación por cursor, reutilizando el cliente httpx y _work_to_row. Es el mecanismo del forward chaining (Hito 5, ADR 0008).

R5: agrega retry/backoff ante 429/5xx (_fetch_all_with_retry). Sin esto, un corpus mediano falla en el forward chaining con un rate limit de OpenAlex (Nota 06, RAÍZ 3).

Calcula el id canónico (D1) de cada citante antes de devolverlo, de modo que los consumidores de estas filas (Forager, compute_forward_scent) pueden identificar los candidatos.

Parameters:

Name Type Description Default
openalex_id str

ID corto de OpenAlex (p. ej. W12345).

required

Returns:

Type Description
list[dict[str, Any]]

Lista de dicts con el schema canónico de filas del Corpus

list[dict[str, Any]]

(misma estructura que produce _work_to_row + id calculado),

list[dict[str, Any]]

con is_seed=False y provenance chaining_hop=1.

fetch_dois_for

fetch_dois_for(ids: list[str]) -> dict[str, str]

Resuelve una lista de IDs de OpenAlex a sus DOIs.

Batchea la consulta en lotes de hasta 100 IDs por request, usando el filtro openalex_id:W1|W2|... con select=id,doi. Reutiliza el retry/backoff de _fetch_all_with_retry para resiliencia ante 429/5xx.

Diseñado para el OpenAlexEnricher (Hito 8a): mantiene la frontera núcleo/costura — el Source hace la I/O; el Enricher orquesta.

Hito 8b (futuro): el mismo patrón sirve para resolver cited_by_id; el Enricher solo necesita un método distinto o un argumento adicional.

Parameters:

Name Type Description Default
ids list[str]

Lista de IDs cortos de OpenAlex (p. ej. ["W12345", "W67890"]). Se acepta con o sin prefijo URL; se normalizan a ID corto.

required

Returns:

Type Description
dict[str, str]

Dict {openalex_id: doi} con los DOIs encontrados. Los IDs

dict[str, str]

sin DOI en OpenAlex simplemente no aparecen en el resultado.

fetch_dois_to_openalex_ids

fetch_dois_to_openalex_ids(
    dois: list[str],
) -> dict[str, str]

Resuelve una lista de DOIs a sus IDs cortos de OpenAlex (W…).

Batchea la consulta en lotes de hasta 100 DOIs por request, usando el filtro doi:d1|d2|... con select=id,doi. Reutiliza el retry/backoff de _fetch_batch_select para resiliencia ante 429/5xx.

Diseñado para service.resolve (ADR 0035): espeja la dirección INVERSA de fetch_dois_for (ids→dois) pero va en la dirección dois→source_id. Mismo patrón de batching/select/retry/polite-pool.

Normaliza los DOIs de entrada (minúsculas, sin prefijo URL) con _normalize_doi antes de armar el filtro. El dict resultado usa también el DOI normalizado como clave.

DOIs no encontrados en OpenAlex simplemente no aparecen en el resultado (no son error).

Parameters:

Name Type Description Default
dois list[str]

Lista de DOIs (con o sin prefijo URL, mayúsculas/minúsculas).

required

Returns:

Type Description
dict[str, str]

Dict {doi_normalizado: source_id_corto} con los IDs encontrados.

dict[str, str]

Los DOIs sin match en OpenAlex no aparecen en el resultado.

fetch_works_by_ids

fetch_works_by_ids(ids: list[str]) -> Corpus

Trae works completos de OpenAlex a partir de una lista de IDs.

Batchea la consulta en lotes de hasta 100 IDs por request, usando el filtro openalex_id:W1|W2|... con select=_FIELDS (todos los campos del schema canónico). Reutiliza _fetch_batch_select y el retry/backoff de la infraestructura existente.

Los works resultantes se marcan como is_seed=False (son candidatos, no semillas de una ecuación) con curation_status=CANDIDATE y provenance[action="fetched_by_id"].

IDs inexistentes en OpenAlex son simplemente omitidos sin error: el filtro OR devuelve solo los works encontrados.

Las filas del Corpus retornado se ordenan por id canónico (D1) para garantizar determinismo entre corridas (ADR 0017).

Parameters:

Name Type Description Default
ids list[str]

Lista de IDs de OpenAlex (p. ej. ["W12345", "W67890"]). Se acepta con o sin prefijo URL; se normalizan a ID corto.

required

Returns:

Type Description
Corpus

Corpus con los works encontrados. is_seed=False,

Corpus

curation_status=CANDIDATE, provenance[action="fetched_by_id"].

Corpus

Vacío si ningún ID es encontrado.

fetch_citing_batch

fetch_citing_batch(
    ids: list[str],
    *,
    max_per_paper: int | None = None,
    since: date | None = None,
) -> dict[str, list[str]]

Trae en lote los citantes de varios papers usando cites:W1|W2|....

Reemplaza N llamadas individuales a fetch_citing (patrón N+1) por una sola request por lote (≤50 IDs, límite empírico de OpenAlex para OR en cites:). Preserva el retry/backoff de _fetch_all_with_retry.

Presupuesto por semilla (anti-starvation): pagina con cursor sobre el filtro OR del lote y, página a página, atribuye cada citante a las semillas objetivo cruzando references_id del citante con el set de IDs. Lleva un contador por semilla y deja de paginar cuando TODAS las semillas del lote alcanzaron max_per_paper citantes (o se agota la paginación). Así la semilla más citada no consume el presupuesto de las demás.

El filtro cites:W1|W2 es un OR válido en la API de OpenAlex: devuelve todos los works que citan al menos uno de los IDs listados.

Thin wrapper sobre _fetch_citing_pages que descarta el mapa de works para mantener la firma/contrato actual (usado por el OpenAlexEnricher, Hito 8b).

Parameters:

Name Type Description Default
ids list[str]

Lista de IDs cortos de OpenAlex (p. ej. ["W111", "W222"]). Se normalizan a ID corto internamente.

required
max_per_paper int | None

Presupuesto máximo de citantes a recolectar por semilla. None = sin tope (pagina todo). Acota el fetch: cuando todas las semillas del lote alcanzan el tope, se detiene la paginación.

None

Returns:

Type Description
dict[str, list[str]]

Dict {seed_id: [citer_id, ...]}. Los citantes de cada semilla

dict[str, list[str]]

ya están atribuidos (cruzando references_id del citante) y

dict[str, list[str]]

acotados a max_per_paper. Los IDs de citantes son los IDs cortos

dict[str, list[str]]

de OpenAlex. Orden determinista (alfabético).

fetch_citing_batch_with_works

fetch_citing_batch_with_works(
    ids: list[str],
    *,
    max_per_paper: int | None = None,
    since: date | None = None,
) -> tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

Como fetch_citing_batch pero conserva los objetos JSON completos.

Misma lógica de paginación, atribución y presupuesto que fetch_citing_batch, sin red extra: los works ya vienen en las páginas que se traen para la atribución. Reutiliza _fetch_citing_pages (no duplica la lógica).

Diseñado para el forward chaining del Forager (#78, opción A1): permite materializar filas con metadata real (título/año/autores) en vez de placeholders [candidate:W...].

Parameters:

Name Type Description Default
ids list[str]

Lista de IDs cortos de OpenAlex (p. ej. ["W111", "W222"]). Se normalizan a ID corto internamente.

required
max_per_paper int | None

Presupuesto máximo de citantes por semilla. None = sin tope.

None

Returns:

Type Description
dict[str, list[str]]

Tupla (attribution, works_map). attribution es

dict[str, dict[str, Any]]

{seed_id: [citer_id, ...]} en orden alfabético, idéntico al

tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

retorno de fetch_citing_batch con los mismos argumentos.

tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

works_map es {citer_id: work_json} con el objeto JSON

tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

completo (campos _FIELDS: título, año, autores, referencias,

tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

etc.) de cada citante distinto traído en las páginas de la

tuple[dict[str, list[str]], dict[str, dict[str, Any]]]

atribución.

load

load(path: str) -> Corpus

Carga un export JSON de OpenAlex como Corpus.

Cada objeto del array JSON se trata como un Work de OpenAlex y se mapea al schema canónico con is_seed=True.

Parameters:

Name Type Description Default
path str

Ruta al archivo JSON (array de Works).

required

Returns:

Type Description
Corpus

Corpus con los papers cargados.

BibtexSource

Siembra un Corpus desde un archivo BibTeX.

BibTeX es Source secundaria (ADR 0007): entrega el mínimo universal (título, año, autores, keywords) pero típicamente no referencias ni citantes.

El import de bibtexparser es perezoso: se instala con el extra [bibtex] (pip install "bib2graph[bibtex]").

Example::

source = BibtexSource()
corpus = source.load("semillas.bib")

seed

seed(query: str) -> SeedResult

No implementado: BibTeX no siembra por ecuación.

Raises:

Type Description
NotImplementedError

Siempre. Usa load() en su lugar.

load

load(path: str) -> Corpus

Carga un archivo .bib como Corpus.

Todos los papers se marcan con is_seed=True y curation_status='candidate'. Campos faltantes quedan en None (sin KeyError, bugfix T1).

Parameters:

Name Type Description Default
path str

Ruta al archivo .bib.

required

Returns:

Type Description
Corpus

Corpus con los papers del archivo.

Raises:

Type Description
ImportError

Si bibtexparser no está instalado.

SeedResult

Bases: BaseModel

Resultado de Source.seed().

Agrupa el corpus sembrado, la query exacta ejecutada (consciencia de traducción, ADR 0007) y el reporte de mapeo (qué mapeó limpio, qué se aproximó, qué se descartó).

corpus se valida en runtime (arbitrary_types_allowed porque Corpus no es un BaseModel). No hay circularidad: corpus.py no importa sources.

Forrajeo y curación

Forager

Orquesta el chaining sobre un Source, rankeando candidatos por scent.

El scent mide el acoplamiento bibliométrico (ADR 0020/0022, R4): backward = cuántos corpus-papers listan al candidato en sus referencias; forward = cuántos corpus-papers cita el candidato directamente (citación directa al corpus, Wohlin; robusto ante references_id ralas en el corpus).

El forward chaining opera sobre todas las semillas (is_seed=True, sin filtrar por curation_status) y usa batcheo OR (fetch_citing_batch, ≤50 IDs/lote) para eliminar el patrón N+1.

Uso::

forager = Forager(OpenAlexSource(email="yo@example.com"), depth=1)
preview = forager.preview(corpus)
ranked = forager.chain(corpus)
corpus_expandido = corpus.merge(ranked.corpus)

Attributes:

Name Type Description
source

Source inyectado (OpenAlexSource u otro compatible).

depth

Profundidad de chaining; solo 1 está implementado.

max_candidates

Tope de candidatos en el ranking; None = sin límite.

max_citing_per_paper

Presupuesto de citantes por semilla en el forward chaining; None = sin tope.

preview

preview(
    corpus: Corpus, *, direction: Direction = "both"
) -> GrowthPreview

Estima cuántos papers nuevos agregaría un chaining.

Opera solo localmente, sin red. No realiza ninguna llamada a la API en ninguna dirección.

  • Backward: estimación exacta desde references_id local.
  • Forward (dos casos):
  • Si el corpus tiene cited_by_id poblado (pasó por b2g enrich), se cuentan los IDs únicos en cited_by_id que aún no están en el corpus — estimación local exacta sin red.
  • Si cited_by_id está vacío (corpus recién sembrado), el conteo forward no es estimable sin red; forward_requires_fetch=True y by_direction["forward"] vale 0.

NO muta el corpus de entrada.

Parameters:

Name Type Description Default
corpus Corpus

Corpus actual (semillas + curados).

required
direction Direction

'backward', 'forward' o 'both'.

'both'

Returns:

Type Description
GrowthPreview

GrowthPreview con la estimación de crecimiento local.

GrowthPreview

Cuando forward_requires_fetch es True, estimated_new

GrowthPreview

refleja solo el crecimiento estimable localmente (backward).

chain

chain(
    corpus: Corpus,
    *,
    direction: Direction = "both",
    since: date | None = None,
) -> RankedCandidates

Computa candidatos rankeados por information scent.

Opción B (#54): los candidatos backward NO se materializan como filas del corpus — sus IDs salen en RankedCandidates.observed_refs para que el comando CLI los persista en referenced_but_not_fetched. El ranking backward SIGUE presente en RankedCandidates.ranking.

Los candidatos forward SÍ se materializan como filas en RankedCandidates.corpus (con metadata traída por fetch_citing_batch).

Devuelve un RankedCandidates con: - corpus: SOLO candidatos forward (no mergeado con el corpus semilla). - ranking: candidatos backward + forward rankeados por scent. - observed_refs: IDs backward observados (no en corpus, tabla auxiliar).

NO muta el corpus de entrada.

Parameters:

Name Type Description Default
corpus Corpus

Corpus actual (no muta).

required
direction Direction

'backward', 'forward' o 'both'.

'both'

Returns:

Type Description
RankedCandidates

RankedCandidates con candidatos, ranking y observed_refs.

RankedCandidates

Bases: BaseModel

Resultado del Forager.chain(): candidatos rankeados por scent.

El corpus contiene SOLO los candidatos nuevos materializados (forward: filas reales; backward: vacío — los IDs backward van a observed_refs). El corpus NO se mergeó con el corpus semilla. El ranking es una lista estable ordenada por scent descendente, con desempate por id ascendente.

Attributes:

Name Type Description
corpus Corpus

Corpus de candidatos materializados con curation_status='candidate' (solo forward; backward = vacío).

ranking list[tuple[str, float]]

Lista (id, information_scent) ordenada por scent desc. Incluye tanto candidatos forward (materializados) como backward (solo IDs observados, sin fila en corpus).

observed_refs list[str]

IDs de OpenAlex observados en backward chaining pero NO materializados como filas del corpus (opción B — #54). Son los candidatos backward que se persisten en la tabla auxiliar referenced_but_not_fetched por el comando CLI, no en el corpus. Vacío en chaining puramente forward.

GrowthPreview

Bases: BaseModel

Estimación de cuántos papers agregaría un chaining antes de traerlos.

Permite al investigador controlar el crecimiento del corpus sin hacer fetch (ADR 0008: control de crecimiento).

preview() opera solo localmente, sin red. El crecimiento backward se estima exactamente desde references_id.

Para el crecimiento forward hay dos casos:

  • Si el corpus fue enriquecido (b2g enrich), los papers tienen cited_by_id poblado: se calcula el número de IDs únicos aún no presentes en el corpus — estimación local exacta. En este caso forward_requires_fetch es False y forward_from_cited_by es True.
  • Si cited_by_id está vacío (corpus recién sembrado, sin enrich), el crecimiento forward no es estimable sin red. En este caso forward_requires_fetch es True, forward_from_cited_by es False y by_direction["forward"] vale 0.

Attributes:

Name Type Description
estimated_new int

Estimación total de candidatos nuevos (solo lo que puede calcularse localmente; forward vale 0 cuando forward_requires_fetch es True).

by_direction dict[str, int]

Desglose por dirección (backward/forward). La entrada "forward" vale 0 cuando el crecimiento forward no es estimable sin red.

direction Direction

Dirección pedida (backward/forward/both).

forward_requires_fetch bool

True cuando el crecimiento forward no puede estimarse localmente (corpus sin cited_by_id poblado) y es necesario llamar a chain() para obtener el conteo real. False cuando la dirección es solo backward, o cuando cited_by_id está disponible (estimación local exacta).

forward_from_cited_by bool

True cuando el crecimiento forward se estimó localmente desde cited_by_id (corpus enriquecido con b2g enrich). False en los demás casos.

capped_by_max bool

True cuando el resultado está acotado por max_candidates; False si no se aplicó límite o si el número de candidatos es menor al límite.

Preprocessor

Determinístico e idempotente. Normaliza y aplica thesaurus al Corpus.

Las funciones puras (normalize_row, apply_thesaurus_to_rows) viven en sus módulos; este orquestador reconstruye el Corpus Arrow y actualiza el Manifest.

Ver docs/API.md §6, ADR 0011.

normalize

normalize(
    corpus: Corpus, applied_at: datetime | None = None
) -> Corpus

Canonicaliza authors_id y language (normalización mínima).

Operaciones (decisión b=A): - authors_id: lowercase + quitar acentos + colapso de espacios. - language: trunca al subtag ISO 639-1 primario.

Idempotente: aplicar dos veces == aplicar una. Registra un PreprocRef(name='normalize') en el Manifest con el timestamp applied_at (R2: reloj en la frontera).

Parameters:

Name Type Description Default
corpus Corpus

Corpus a normalizar (no muta).

required
applied_at datetime | None

Timestamp de la operación. Si None, usa datetime.now(UTC) (para uso como librería independiente). La frontera CLI inyecta un único timestamp por invocación.

None

Returns:

Type Description
Corpus

Nuevo Corpus normalizado.

apply_thesaurus

apply_thesaurus(
    corpus: Corpus,
    thesaurus: dict[str, object] | Path,
    applied_at: datetime | None = None,
) -> Corpus

Normaliza keywords con el thesaurus multilingüe curado.

Lee keywords_raw y sobrescribe keywords_id con los conceptos canónicos del thesaurus. Determinista e idempotente (ADR 0011). Registra un PreprocRef(name='apply_thesaurus') en el Manifest con el timestamp applied_at (R2: reloj en la frontera).

Parameters:

Name Type Description Default
corpus Corpus

Corpus a procesar (no muta).

required
thesaurus dict[str, object] | Path

Dict con formato {concepts: {canonical: {aliases_*: [...]}}} o Path a un JSON con esa estructura.

required
applied_at datetime | None

Timestamp de la operación. Si None, usa datetime.now(UTC) (para uso como librería independiente). La frontera CLI inyecta un único timestamp por invocación.

None

Returns:

Type Description
Corpus

Nuevo Corpus con keywords_id poblado.

apply_filters

apply_filters(
    corpus: Corpus,
    criteria: list[FilterCriterion],
    *,
    decided_at: datetime | None = None,
) -> tuple[Corpus, list[FilterStep]]

Encadena varios criterios de filtro y sella Manifest.filters.

Los filtros se aplican en orden; cada uno ve los conteos PRISMA del resultado del anterior. Al final, Manifest.filters se actualiza con todos los pasos.

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
corpus Corpus

Corpus inicial (no muta).

required
criteria list[FilterCriterion]

Lista de criterios a aplicar en orden.

required
decided_at datetime | None

Instante de la decisión compartido por todos los pasos. Si es None, el backend usa datetime.now(UTC) como fallback.

None

Returns:

Type Description
tuple[Corpus, list[FilterStep]]

Tupla (corpus_final, [FilterStep, ...]) con todos los pasos.

FilterCriterion

Bases: BaseModel

Criterio de filtro PRISMA.

Attributes:

Name Type Description
field Literal['year', 'type', 'language', 'min_citations']

Campo del Corpus a filtrar.

op Literal['gte', 'lte', 'in', 'not_in', 'eq']

Operador de comparación.

value int | str | list[str]

Valor de referencia (int para year/min_citations; str o list[str] para type/language).

Redes (proyección)

Networks

API de alto nivel para construir redes bibliométricas desde un Corpus.

Todos los métodos son estáticos y puros (mismo input → mismo output).

build staticmethod

build(corpus: Corpus, spec: NetworkSpec) -> NetworkArtifact

Construye una red bibliométrica según la especificación dada.

Parameters:

Name Type Description Default
corpus Corpus

Corpus de origen.

required
spec NetworkSpec

Especificación declarativa de la red.

required

Returns:

Type Description
NetworkArtifact

NetworkArtifact con grafo, métricas y comunidades.

quick staticmethod

quick(
    corpus: Corpus, *, min_weight: int = 1
) -> list[NetworkArtifact]

Construye las redes principales con configuración razonable.

Arma specs para: acoplamiento bibliográfico (full), co-autoría, colaboración institucional y co-ocurrencia de keywords.

Co-citación (Hito 8b): se incluye si el corpus tiene cited_by_id poblado en al menos un paper (es decir, si pasó por el OpenAlexEnricher con la pasada 2). Si cited_by_id está vacío en todo el corpus, la co-citación se omite sin error (avisa por log).

Parameters:

Name Type Description Default
corpus Corpus

Corpus de origen.

required
min_weight int

Peso mínimo de arista (#159 — build --min-weight). Default 1 = sin filtro (comportamiento anterior intacto). Si es > 1, las aristas con peso < N se filtran en todas las redes.

1

Returns:

Type Description
list[NetworkArtifact]

Lista de 4 o 5 NetworkArtifact (coupling, co-autoría,

list[NetworkArtifact]

institución, co-word y, condicionalmente, co-citación).

NetworkArtifact dataclass

Resultado de proyectar y analizar un corpus con una NetworkSpec.

Agrupa el grafo, las métricas, las comunidades, la asortatividad, el layout y la especificación que lo originó.

Attributes:

Name Type Description
graph _Graph

Grafo NetworkX proyectado.

metrics dict[str, object]

Salida de network_metrics (densidad, componentes, clustering).

communities dict[Any, int] | None

Salida de detect_communities o None si no se calculó.

assortativity dict[str, object] | None

Salida de assortativity o None si no se calculó.

layout dict[Any, Any] | None

Dict nodo→coordenadas o None si no se calculó.

spec NetworkSpec

Especificación que generó este artefacto.

BibliographicCouplingProjector

Acoplamiento bibliográfico: dos papers están acoplados si comparten referencias.

Nodo = paper (id). Peso = nº de referencias compartidas (D1, D2). Scope por defecto: 'full' (corpus completo; ciudadano de primera, crítica #2). No requiere Enricher: las refs ya vienen en references_id (OpenAlex).

project

project(
    table: Table,
    *,
    min_weight: int = MIN_WEIGHT_DEFAULT,
    scope: Literal["full", "seeds_only"] = "full",
) -> _Graph

Proyecta el acoplamiento bibliográfico sobre la tabla.

Parameters:

Name Type Description Default
table Table

Tabla Arrow canónica del Corpus.

required
min_weight int

Umbral mínimo de peso (default 1 = sin filtro).

MIN_WEIGHT_DEFAULT
scope Literal['full', 'seeds_only']

Alcance de la proyección.

'full'

Returns:

Type Description
_Graph

Grafo de acoplamiento con weight en cada arista.

CoCitationProjector

Co-citación: dos papers son co-citados si un tercero los cita a ambos.

Nodo = paper (id). Peso = nº de papers que citan a ambos (D1, D2). Scope por defecto: 'seeds_only' (la más cara; requiere 2º nivel de fetch).

Nota: la co-citación COMPLETA requiere el OpenAlexEnricher (Hito 8) para traer el 2º nivel de citantes. En Hito 2 se proyecta desde cited_by_id de los papers disponibles en el corpus. El resultado es válido para el subset de citantes ya presentes en la tabla.

project

project(
    table: Table,
    *,
    min_weight: int = MIN_WEIGHT_DEFAULT,
    scope: Literal["full", "seeds_only"] = "seeds_only",
) -> _Graph

Proyecta la red de co-citación desde cited_by_id.

Dos papers P1 y P2 están co-citados si comparten al menos un citante en cited_by_id. El peso es el nº de citantes compartidos (D1).

Para la co-citación completa, el corpus debe haber pasado por el OpenAlexEnricher (Hito 8). Sin enriquecimiento, solo se usan los citantes ya presentes en la tabla.

Parameters:

Name Type Description Default
table Table

Tabla Arrow canónica del Corpus.

required
min_weight int

Umbral mínimo de peso (default 1 = sin filtro).

MIN_WEIGHT_DEFAULT
scope Literal['full', 'seeds_only']

Alcance de la proyección. Default 'seeds_only'.

'seeds_only'

Returns:

Type Description
_Graph

Grafo de co-citación con weight en cada arista.

AuthorCollaborationProjector

Co-autoría: dos autores están conectados si co-firman un paper.

Nodo = autor (authors_id). Peso = nº de papers co-firmados (D1, D2). Scope por defecto: 'full'.

project

project(
    table: Table,
    *,
    min_weight: int = MIN_WEIGHT_DEFAULT,
    scope: Literal["full", "seeds_only"] = "full",
) -> _Graph

Proyecta la red de co-autoría sobre la tabla.

Parameters:

Name Type Description Default
table Table

Tabla Arrow canónica del Corpus.

required
min_weight int

Umbral mínimo de peso (default 1 = sin filtro).

MIN_WEIGHT_DEFAULT
scope Literal['full', 'seeds_only']

Alcance de la proyección.

'full'

Returns:

Type Description
_Graph

Grafo de co-autoría con weight en cada arista.

InstitutionCollaborationProjector

Co-autoría institucional: dos instituciones están conectadas si co-aparecen en un paper.

Nodo = institución (institutions_id). Peso = nº de papers con co-aparición (D1, D2). Scope por defecto: 'full'.

project

project(
    table: Table,
    *,
    min_weight: int = MIN_WEIGHT_DEFAULT,
    scope: Literal["full", "seeds_only"] = "full",
) -> _Graph

Proyecta la red de colaboración institucional sobre la tabla.

Parameters:

Name Type Description Default
table Table

Tabla Arrow canónica del Corpus.

required
min_weight int

Umbral mínimo de peso (default 1 = sin filtro).

MIN_WEIGHT_DEFAULT
scope Literal['full', 'seeds_only']

Alcance de la proyección.

'full'

Returns:

Type Description
_Graph

Grafo de colaboración institucional con weight en cada arista.

KeywordCoOccurrenceProjector

Co-ocurrencia de keywords: dos keywords co-ocurren si aparecen en el mismo paper.

Nodo = keyword (keywords_id). Peso = nº de papers donde co-ocurren (D1, D2). Scope por defecto: 'full'.

project

project(
    table: Table,
    *,
    min_weight: int = MIN_WEIGHT_DEFAULT,
    scope: Literal["full", "seeds_only"] = "full",
) -> _Graph

Proyecta la red de co-ocurrencia de keywords sobre la tabla.

Parameters:

Name Type Description Default
table Table

Tabla Arrow canónica del Corpus.

required
min_weight int

Umbral mínimo de peso (default 1 = sin filtro).

MIN_WEIGHT_DEFAULT
scope Literal['full', 'seeds_only']

Alcance de la proyección.

'full'

Returns:

Type Description
_Graph

Grafo de co-ocurrencia de keywords con weight en cada arista.

Análisis de redes

network_metrics

network_metrics(g: _Graph) -> dict[str, object]

Densidad, nº de componentes y clustering promedio del grafo.

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX (no dirigido).

required

Returns:

Type Description
dict[str, object]

Dict con claves 'density', 'num_components', 'avg_clustering'.

centrality

centrality(g: _Graph) -> dict[str, dict[Any, float]]

Centralidad de grado e intermediación por nodo.

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX (no dirigido).

required

Returns:

Type Description
dict[str, dict[Any, float]]

Dict con claves 'degree' y 'betweenness', cada una mapeando

dict[str, dict[Any, float]]

nodo → valor de centralidad.

detect_communities

detect_communities(
    g: _Graph,
    method: str = "louvain",
    *,
    random_state: int | None = None,
    resolution: float = 1.0,
) -> dict[Any, int]

Detecta comunidades en el grafo con el método indicado.

R2 (ADR 0017 enmendado): el parámetro random_state siembra el generador de Louvain de forma determinista cuando se provee. Pasarlo derivado del corpus_hash de contenido (ver facade.py) garantiza "mismo corpus + mismo spec → mismas comunidades".

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX (no dirigido).

required
method str

Algoritmo de detección. Uno de 'louvain', 'label_prop', 'greedy_modularity'.

'louvain'
random_state int | None

Semilla entera para reproducibilidad de Louvain (solo se usa cuando method='louvain'). Si es None, Louvain corre sin semilla (no reproducible).

None
resolution float

Parámetro de resolución para Louvain (best_partition). Default 1.0 = comportamiento estándar de python-louvain. Se ignora para label_prop y greedy_modularity (no es un error; esos algoritmos no tienen parámetro de resolución equivalente).

1.0

Returns:

Type Description
dict[Any, int]

Dict nodo → id de comunidad (int).

Raises:

Type Description
ImportError

Si se solicita 'louvain' y python-louvain no está instalado. Falla explícito con el nombre del paquete a instalar (lección 7 de v0; AGENTS.md §manejo de errores).

ValueError

Si method no es reconocido.

assortativity

assortativity(
    g: _Graph,
    *,
    attribute: str | None = None,
    by_degree: bool = True,
    proxy: str | None = None,
) -> dict[str, object]

Asortatividad por atributo categórico configurable y/o por grado.

El atributo y sus categorías son config del usuario (no se hardcodea ningún campo como 'region'; crítica #5 del sandbox IED).

Cuando se pasa proxy, el resultado incluye 'proxy_disclaimer' advirtiendo que el atributo es un proxy, no el campo real (D4).

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX (no dirigido).

required
attribute str | None

Nombre del atributo de nodo para asortatividad categórica. Si es None, no se calcula la asortatividad por atributo.

None
by_degree bool

Si True, calcula asortatividad por grado.

True
proxy str | None

Si se pasa un string, indica que attribute es un proxy del campo real (p. ej. 'affiliation_per_paper'). Se añade un disclaimer al resultado.

None

Returns:

Type Description
dict[str, object]

Dict con 'attribute_assortativity' (si attribute fue dado),

dict[str, object]

'degree_assortativity' (si by_degree=True), y

dict[str, object]

'proxy_disclaimer' (si proxy fue dado).

community_composition

community_composition(
    g: _Graph, communities: dict[Any, int], attribute: str
) -> dict[int, dict[str, float]]

Composición porcentual de cada comunidad por atributo categórico.

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX con atributos de nodo.

required
communities dict[Any, int]

Dict nodo → id de comunidad (salida de detect_communities).

required
attribute str

Nombre del atributo de nodo a usar.

required

Returns:

Type Description
dict[int, dict[str, float]]

Dict comunidad → Dict categoría → fracción (0.0 a 1.0). La suma de

dict[int, dict[str, float]]

fracciones por comunidad es 1.0 si todos los nodos tienen el atributo.

cocitation_quality_report

cocitation_quality_report(
    corpus: Corpus,
    *,
    thresholds: QualityThresholds | None = None,
) -> dict[str, object]

Informe de calidad de la red de co-citación según metodología §4.

Evalúa 4 criterios configurables y devuelve un dict estructurado (D6): {criterio: {valor, umbral, pasa}} + "overall_pass": bool. Sin score ponderado.

R5: param muerto g eliminado (Nota 06, catálogo de secundarios). El grafo no se usaba en ningún criterio; pasarlo era un anti-patrón que ARCHITECTURE §8 dice evitar.

Parameters:

Name Type Description Default
corpus Corpus

Corpus a evaluar.

required
thresholds QualityThresholds | None

Umbrales configurables. Si None, usa QualityThresholds() con los defaults de metodología §4.

None

Returns:

Type Description
dict[str, object]

Dict con claves por criterio y "overall_pass": bool.

QualityThresholds

Bases: BaseModel

Umbrales configurables para el informe de calidad de co-citación.

Los defaults sensatos son los de metodología §4. Se pueden sobreescribir para adaptar a otros campos de investigación (crítica #5).

Attributes:

Name Type Description
min_volume int

Número mínimo de papers en el corpus.

min_doi_refs_pct float

Fracción mínima de papers con DOI.

min_countries int

Número mínimo de países distintos (vía institutions_id).

min_recurrent_authors int

Nº mínimo de autores que aparecen en ≥2 papers.

Exportadores

GraphMLExporter

Exporta un grafo NetworkX a GraphML con atributos de nodo y métricas.

Según la decisión D5, los atributos de nodo ya presentes en el grafo y las métricas del dict results se escriben como node attributes antes de llamar a nx.write_graphml. Gephi los lee como columnas.

export

export(
    g: _Graph,
    results: dict[str, object],
    out_dir: str | Path,
) -> None

Exporta el grafo a <out_dir>/network.graphml.

Fusiona los valores del dict results como atributos de nodo. Si results contiene un dict anidado (p. ej. {"degree": {nodo: val}}), cada valor se asigna al nodo correspondiente.

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX a exportar.

required
results dict[str, object]

Dict de métricas/análisis. Los valores pueden ser dicts {nodo: valor} (p. ej. salida de centrality).

required
out_dir str | Path

Directorio de salida. Se crea si no existe.

required

CsvExporter

Exporta un grafo NetworkX a dos archivos CSV.

Según la decisión D5
  • aristas.csv: columnas source,target,weight.
  • nodos.csv: id,label + atributos de nodo presentes + métricas de results (dict anidado {métrica: {nodo: valor}}) unidas por id.

export

export(
    g: _Graph,
    results: dict[str, object],
    out_dir: str | Path,
) -> None

Exporta el grafo a nodos.csv y aristas.csv en out_dir.

Parameters:

Name Type Description Default
g _Graph

Grafo NetworkX a exportar.

required
results dict[str, object]

Dict de métricas. Los valores deben ser dicts {nodo: valor} para fusionarse a nodos.csv.

required
out_dir str | Path

Directorio de salida. Se crea si no existe.

required