Forrajeo y curación¶
Expandir y curar el corpus: el Forager rankea candidatos por information
scent (estructura bibliométrica determinista, sin IA), con preview de
crecimiento antes de comprometer; el Preprocessor normaliza, y los filtros
PRISMA acotan el corpus de forma trazable.
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 |
|
|
depth |
Profundidad de chaining; solo 1 está implementado. |
|
max_candidates |
Tope de candidatos en el ranking; |
|
max_citing_per_paper |
Presupuesto de citantes por semilla en el forward
chaining; |
Methods:
| Name | Description |
|---|---|
preview |
Estima cuántos papers nuevos agregaría un chaining. |
chain |
Computa candidatos rankeados por information scent. |
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_idlocal. - Forward (dos casos):
- Si el corpus tiene
cited_by_idpoblado (pasó porb2g enrich), se cuentan los IDs únicos encited_by_idque aún no están en el corpus — estimación local exacta sin red. - Si
cited_by_idestá vacío (corpus recién sembrado), el conteo forward no es estimable sin red;forward_requires_fetch=Trueyby_direction["forward"]vale0.
NO muta el corpus de entrada.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
corpus
|
Corpus
|
Corpus actual (semillas + curados). |
required |
direction
|
Direction
|
|
'both'
|
Returns:
| Type | Description |
|---|---|
GrowthPreview
|
|
GrowthPreview
|
Cuando |
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).
ADR 0048 (#270): en direction forward/both, corpus también
incluye filas de semillas ya existentes con cited_by_id
actualizado (unión con los citantes traídos en esta pasada). No son
candidatos nuevos: el merge posterior (Corpus.merge) las fusiona
por id con la semilla ya persistida, sin duplicar filas.
Devuelve un RankedCandidates con:
- corpus: candidatos forward + actualizaciones de cited_by_id de
semillas (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
|
|
'both'
|
Returns:
| Type | Description |
|---|---|
RankedCandidates
|
|
RankedCandidates
¶
Bases: BaseModel
Resultado del Forager.chain(): candidatos rankeados por scent.
El corpus contiene los candidatos nuevos materializados (forward: filas
reales; backward: vacío — los IDs backward van a observed_refs) MÁS,
en direction forward/both, filas de semillas ya existentes con
cited_by_id actualizado (ADR 0048, #270). El corpus NO se mergeó con
el corpus semilla — eso lo hace el llamador (Corpus.merge, unión de
sets en columnas lista — D3, idempotente).
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
|
ranking |
list[tuple[str, float]]
|
Lista |
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
|
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 tienencited_by_idpoblado: se calcula el número de IDs únicos aún no presentes en el corpus — estimación local exacta. En este casoforward_requires_fetchesFalseyforward_from_cited_byesTrue. - Si
cited_by_idestá vacío (corpus recién sembrado, sin enrich), el crecimiento forward no es estimable sin red. En este casoforward_requires_fetchesTrue,forward_from_cited_byesFalseyby_direction["forward"]vale0.
Attributes:
| Name | Type | Description |
|---|---|---|
estimated_new |
int
|
Estimación total de candidatos nuevos (solo lo que
puede calcularse localmente; forward vale 0 cuando
|
by_direction |
dict[str, int]
|
Desglose por dirección ( |
direction |
Direction
|
Dirección pedida ( |
forward_requires_fetch |
bool
|
|
forward_from_cited_by |
bool
|
|
capped_by_max |
bool
|
|
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.
Methods:
| Name | Description |
|---|---|
normalize |
Canonicaliza |
apply_thesaurus |
Normaliza keywords con el thesaurus multilingüe curado. |
normalize
¶
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
|
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 |
required |
applied_at
|
datetime | None
|
Timestamp de la operación. Si |
None
|
Returns:
| Type | Description |
|---|---|
Corpus
|
Nuevo Corpus con |
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
|
Returns:
| Type | Description |
|---|---|
tuple[Corpus, list[FilterStep]]
|
Tupla |
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
|