diff --git a/docs/API.md b/docs/API.md index c5f3887..4246ef9 100644 --- a/docs/API.md +++ b/docs/API.md @@ -133,7 +133,10 @@ VERBO:** solo **`curate filter`→`FILTERED`**; el resto transversal. **BREAKING - **`curate filter`** (`--year-gte`/`--year-lte`, `--language`, `--type`, `--min-citations`): aplica inclusión/exclusión PRISMA **marcando `rejected`** (no borra) con conteo por paso. **Transiciona a `FILTERED`.** Comparte `filter_corpus(store_path, *, year_gte, year_lte, language, type_in, - min_citations, decided_at)` con el verbo suelto `filter`. + min_citations, decided_at)` con el verbo suelto `filter`. **La inclusión manual gana** (ADR + [0044](decisiones/0044-precedencia-inclusion-manual-en-curate.md)): el filtro **omite los papers + `accepted`** (nunca los rechaza, aunque no cumplan el criterio), igual que ya omite a los `rejected`; + solo marca `rejected` a los **no-aceptados** (`candidate` y demás) que no pasan. **`build` y `export` separados** (ADR 0021 §B). `build` computa `Networks.quick` (4-5 redes) y escribe a `/networks//` (transiciona a `BUILT`); `export --format graphml|csv` **relee** esos @@ -1213,8 +1216,15 @@ def apply_filters(corpus: Corpus, criteria: list[FilterCriterion]) -> tuple[Corp - **Los filtros MARCAN `rejected`, NO borran:** un paper excluido queda en la tabla con `curation_status='rejected'` vía `corpus.reject(...)` (con el criterio en `provenance`), nunca se borra. La exclusión es curación **reversible y auditable** (biblioteca viva, ADR 0009/0013). +- **La inclusión manual gana — el filtro OMITE `accepted`** (ADR + [0044](decisiones/0044-precedencia-inclusion-manual-en-curate.md)): un paper `accepted` a mano queda + **intacto** aunque no cumpla el criterio; el filtro **nunca** lo mueve `accepted`→`rejected` (lo + excluye del conjunto a rechazar, igual que ya excluye a los `rejected`). Solo actúa sobre **no-aceptados** + (`candidate` y demás). Precisa el scope de 0020 §C, sin revertirlo. Un `accepted` sale del corpus solo + por decisión humana explícita (`curate reject --ids ...`). - **Conteo PRISMA por paso:** cada `FilterStep` lleva `count_before`/`count_after` sobre los papers - **no-rejected** (candidate + accepted). + **no-rejected** (candidate + accepted). Los `accepted` **nunca abandonan** ese conjunto por efecto de + un filtro (ADR 0044). - **`keywords_id` es post-thesaurus:** los proyectores de co-ocurrencia de keywords (§7) deben correr **después** de `apply_thesaurus`. - **Campo/operador desconocido LANZA** `ValueError` accionable (lista los válidos); no es no-op diff --git a/docs/Notas/29-backlog-sinergias-plan-0_12_0.md b/docs/Notas/29-backlog-sinergias-plan-0_12_0.md new file mode 100644 index 0000000..e1a15a3 --- /dev/null +++ b/docs/Notas/29-backlog-sinergias-plan-0_12_0.md @@ -0,0 +1,178 @@ +# 29 — Backlog, sinergias y plan 0.12.0: agent-native de verdad, listo para cualquier agente + +> **Género:** nota de planificación (encuadre antes de abrir sub-issues / ADR; no es ADR ni doc canónico). +> **Origen:** revisión del backlog de GitHub (junio 2026) en torno al eje *"experiencia de uso + agent-native"*: cómo cerrar el lazo end-to-end para que un investigador (humano o agente, en Claude Code / OpenCode / otro cliente) pueda correr el ciclo completo sin ayuda y obtener un entregable defendible. +> **Para qué:** dejar por escrito el mapa del backlog, las sinergias detectadas, las decisiones del PO (qué entra, qué se difiere) y el orden de merge propuesto para 0.12.0. Insumo para abrir sub-issues, escribir las ADR nuevas y secuenciar el trabajo. +> **Auditoría as-built (2026-06-30):** las afirmaciones sobre superficie CLI / ADR vigentes están contrastadas contra `docs/API.md`, `docs/decisiones/0043-posicionamiento-agent-native-cli.md`, `docs/decisiones/0044-precedencia-inclusion-manual-en-curate.md`, el milestone activo en GitHub y las ramas locales. Las dependencias duras están verificadas. +> **Relacionadas:** `22-frontera-y-alcance-de-bib2graph.md` (la frontera que no se cruza), `23-RETROALIMENTACION_bib2graph_agente.md` (fricciones P1/P2/P3 desde un agente), `26-retroalimentacion-cli-agent-native.md`, `28-marco-software-donde-nos-paramos.md` (el marco de software que sostiene el producto), ADR [0021](docs/decisiones/0021-cli-agente-native-contrato.md), ADR [0037](docs/decisiones/0037-superficie-cli-10-verbos-ciclo.md), ADR [0038](docs/decisiones/0038-destino-verbos-huerfanos-0037.md), ADR [0039](docs/decisiones/0039-skill-comando-meta-distribucion.md), ADR [0043](docs/decisiones/0043-posicionamiento-agent-native-cli.md). + +--- + +## TL;DR + +El backlog tiene **bastante trabajo**, pero no todo cabe y no todo tiene el mismo retorno. Si el eje es **experiencia de uso + agent-native**, hay un subconjunto cuya sinergia es grande y se puede entregar junto en **0.12.0**. La propuesta, ya validada con el PO: + +> **0.12.0 = "agent-native de verdad, listo para cualquier agente".** +> Tres olas: (1) cerrar el lazo end-to-end que hoy se rompe; (2) cerrar las 3 grietas que el ADR [0043](docs/decisiones/0043-posicionamiento-agent-native-cli.md) declaró como roadmap; (3) hacer amable el último kilómetro + democratizar la skill + abrir la puerta a investigadores no-técnicos. + +Lo que queda fuera: multi-proveedor / DOAJ (#256, #252 — scope-future), colaboración por Git como flujo de primera clase (#205 — necesita ADR de bordes, 0.13.0), back-merge automático de release-please (#245 — toil manual documentado por ahora). + +--- + +## 1. Estado del repo y de GitHub + +- **Rama actual:** `feat/233-curate-precedencia-manual` (clean). Tiene el fix de #233 + ADR [0044](docs/decisiones/0044-precedencia-inclusion-manual-en-curate.md) mergeados en la historia reciente (commits `3417c68`, `40d9f7c`, `2f58ab5`). Lista para PR a `dev`. +- **ADR 0043** (Aceptada, 2026-06-30) — **posiciona el CLI como agent-native deliberadamente** y declara **3 grietas como roadmap**: (3a) `error.subcode` para 429 vs 504, (3b) eco del workspace resuelto, (3c) `b2g schema`. +- **ADR 0044** (Aceptada) — cierra #233: `curate filter` no rechaza `accepted`. +- **Milestone activo:** `0.12.0` (vence 2026-07-05), 8 issues abiertos. +- **0.11.0** cerrado (5 issues cerrados). +- **Backlog sin milestone:** ~12 issues abiertos relevantes para el eje "experiencia de uso + agente-native". + +## 2. Mapa del backlog (issues vivos, agrupados por eje) + +### Eje A — Cerrar el lazo end-to-end (P1 de #204 + #233 + #221) + +- **#233** `curate filter` pisa `accepted` sin avisar — *cerrado en rama actual, listo para PR*. +- **#204** (P1) `chain --direction backward` reporta "0 candidatos" con output contradictorio pese a `--preview`列出 miles — *rompe el lazo*. +- **#204** (P1) Co-citación inalcanzable: tres comandos tocan `cited_by_id` y ninguno solo lo completa; `enrich` está deprecado y solo puebla sobre `accepted`; `chain forward` no puebla `cited_by_id`. Sin camino feliz único. +- **#221** Split de #207 — grupo `read {list,stats,top,show}` con `read top --by-community`, abstracts en lote, `group-by` ampliado. Aterriza la fricción #1 de la [Nota 23](23-RETROALIMENTACION_bib2graph_agente.md). +- **#207** Poda 0038 + cierre de la ventana de deprecación (breaking). El propio cuerpo del issue dice *"el conteo '20 subcomandos' tiene que dejar de ser falso"*. + +### Eje B — Agent-native real: las 3 grietas del ADR 0043 + +- **#258** `error.subcode` (3a) — `RATE_LIMITED` (429) vs `UPSTREAM_TIMEOUT` (504). Única con daño documentado en uso real: el agente ruteó alrededor del motor y perdió procedencia. +- **#259** (3b) Eco del workspace resuelto en el envelope + warning en walk-up. +- **#260** (3c) `b2g schema` — introspección versionada del contrato. Cierra el *hop* a `docs/API.md` en prosa. + +### Eje C — Salida amable + skill multi-proveedor + docs para no-técnicos + +- **#203** CSV: URL al paper + campos faltantes + encoding i18n. Cierra el feedback hispano (mojibake de tildes en Excel-Windows). +- **#188 + #193** Skill distribuida en el wheel (✅ `b2g skill add` para Claude Code en 0.10.0) **+ skill agnóstica del proveedor** (Claude Code → OpenCode, etc.). Sin esto, bib2graph queda atado a un cliente. +- **#187** Propuesta de visión: posicionar bib2graph como *"revisión narrativa asistida por bibliometría"* (antídoto contra el sesgo del related work escrito de memoria). +- **#134 (cierre)** + **#208** Documentación de usuario en el tono del investigador, no del bibliómetra. Diátaxis: tutorial + how-tos. +- **#196** Epic 0.12.0 / Nota 22 — responder a la frontera bib2graph/producto (paraguas, auditoría contra la frase-ancla). + +### Eje D — Diferido (con justificación) + +- **#256** Epic multi-proveedor post-S2 hacia 0.12/0.13 — superficie multi-source es enorme y se cruza con ADR 0042. **Fuera** de 0.12.0. +- **#252** DOAJ como fuente Sur-global — reentra cuando haya demanda real + capacidad. +- **#205** Colaboración por Git como flujo de primera clase — viable pero necesita ADR de bordes (`curate merge`, `curate diff`, gitignore default, hidratación). Diferir a 0.13.0. +- **#245** Back-merge automático de release-please — bug de CI/infra. **Diferido** (toil manual documentado en `CONTRIBUTING.md`). +- **#251, #253, #254, #255** Wiring de Semantic Scholar / Doaj / decisiones de identidad cross-proveedor — acoplados a #256; no entran solos. +- **#235** Benchmark `@slow` huérfano — housekeeping de CI, no bloqueante. + +## 3. Sinergias detectadas (lo que hace que esto *cabe junto*) + +### Sinergia A — Cerrar la grieta agent-native (ADR 0043, 3 grietas) +Las 3 grietas (#258, #259, #260) son **aditivas, pequeñas y comparten el mismo contrato** (`docs/API.md`). Una sola sub-práctica + **un ADR paraguas** las cierra juntas. Esa conversión de "lo declarado/roadmap" a "lo implementado" hace que cualquier otra mejora de AX se apoye sobre un envelope honesto. + +### Sinergia B — P1 de #204 + #207 + #221 +El epic 0.12.0 contiene 3 P1 que el agente operativo topó: `chain --direction backward` reporta "0 candidatos"; co-citación inalcanzable; podas de deprecación que no driftean con el contrato. Juntas son **un mismo arco: hacer que el lazo se cierre sin help humano**. + +### Sinergia C — Polish ofensivo: encoding CSV i18n + GraphML enriquecido +El feedback de usuario (#203: encoding i18n + URL + campos) converge con los patrones de BibGraph (#206: errores honestos, GraphML rico). Baja la fricción del **último kilómetro**: el artefacto que el investigador abre ya está listo. + +### Sinergia D — Distribución de la skill agnóstica del proveedor (#188 + #193) +El epic skill ya está prácticamente hecho (ADR [0039](docs/decisiones/0039-skill-comando-meta-distribucion.md)); solo falta la **abstracción por proveedor** (Claude Code → OpenCode → otros). Esto desbloquea *"lo pueden usar desde cualquier agente"* en una sola entrega. + +### Sinergia E — Documentación de usuario / quickstart no-técnico (#134 cierre + Nota 22 + #187) +Toda la potencia construida está, pero **faltan los manuales en el tono del investigador** (no del bibliómetra). El quickstart *"5 pasos para no repetir tu related work de memoria"* (#187) cierra el ciclo *"que la pueda usar alguien curioso, no técnico"*. + +## 4. Decisiones del PO (validadas en sesión) + +| Decisión | Valor | Por qué | +|---|---|---| +| **Alcance 0.12.0** | Ola 1 + Ola 2 + Ola 3 completas | Eje único: agent-native + experiencia de uso. Las tres olas convergen sobre el mismo entregable. | +| **Poda de deprecación (#207)** | Romper en 0.12.0 (breaking) | Ya hubo una ventana 0.10.0→0.11.0; cerrarla limpia el conteo de "10 verbos" y elimina drift. | +| **Back-merge release-please (#245)** | Diferido (toil manual documentado) | Pequeño pero de infra; no compite por archivos con el resto del release. | +| **ADR nuevas** | Una ADR paraguas para las 3 grietas + ADR para skill multi-proveedor | Regla del repo: cada cambio del contrato arrastra su ADR. Las 3 grietas juntas justifican un paraguas; skill multi-proveedor es cambio de contrato aparte. | +| **Multi-proveedor (#256, #251-#255)** | Diferido a 0.13.0 | Toca decisiones de identidad cross-proveedor (ADR 0042) que merecen su propio encuadre. | +| **Colaboración por Git (#205)** | Diferido a 0.13.0 | Necesita ADR de bordes; no bloqueante para "experiencia de uso". | +| **Visión #187** | Entra como docs (no como feature) | El quickstart se materializa como tutorial Diátaxis; el "motor de revisión narrativa asistida" es posicionamiento del README, no código nuevo. | + +## 5. Plan consolidado: 3 olas dentro de 0.12.0 + +### Ola 1 — Cerrar el lazo (P1 + structural) + +**Objetivo:** que el lazo end-to-end corra sin help humano. Subtareas: + +1. **Cerrar #233** — la rama `feat/233-curate-precedencia-manual` ya tiene el fix + ADR 0044. PR a `dev`. +2. **#204 P1a — Backward chain bug.** Fix: `chain --direction backward` no debe reportar "0 candidatos" cuando `--preview`列出 miles y hay refs observadas. Separar el conteo del output de top-candidatos. Issue propio. +3. **#204 P1b — Co-citación con camino feliz único.** Decisión de diseño: `chain forward` puebla `cited_by_id` además de la metadata del citante. O un comando explícito `b2g build --cocitation` que orquesta el camino completo (`accept` → `chain forward --mode cite` → `build`). ADR de camino único antes de mergear. +4. **#221 grupo `read {list,stats,top,show}`** con `read top --by-community`, abstracts en lote (`read list --fields abstract,…` o `read dump --json`), `group-by` ampliado (source/community/decade/language). Split de #207 según el cuerpo del propio issue. +5. **#207 poda 0038 + cierre de ventana de deprecación** (breaking). `BREAKING CHANGE:` footer. `b2g --help` debe listar exactamente los 10 verbos del ciclo + meta + aliases-con-warning. `inspect`/`enrich`/`thesaurus`/`restore`/`resolve` se retiran sin alias-cortesía (P1 0038) o con alias+warning (P1 #207). + +### Ola 2 — Agent-native real (ADR 0043, 3 grietas) + +**Objetivo:** cerrar la deuda declarada en ADR 0043. Subtareas: + +6. **ADR 0045 (paraguas)** — cierra las 3 grietas del ADR 0043 como una sola decisión registrada: (3a) `error.subcode`, (3b) eco del workspace resuelto, (3c) `b2g schema`. *Debe existir antes del primer PR de Ola 2.* +7. **#258 `error.subcode`** — campo aditivo `error.subcode` en el envelope. Mapeo `RATE_LIMITED` (429) / `UPSTREAM_TIMEOUT` (504). Tests que sellan el mapeo. +8. **#259 eco del workspace resuelto** — `data.workspace` aditivo en el envelope + warning accionable cuando se resolvió por walk-up. Tests. +9. **#260 `b2g schema`** — comando meta (fuera de los 10 verbos del ciclo, patrón `skill` de ADR [0039](docs/decisiones/0039-skill-comando-meta-distribucion.md)). Emite envelope-schema + exit codes + versión. Conteo "10 + `skill` + `schema`". +10. **#204 P2 — Deprecaciones visibles en `--help`.** Marca `[DEPRECATED → X]` en la 1ª línea del `--help` y en el listado de comandos. Barato, gran delta de AX. + +### Ola 3 — Salida amable + skill multi-proveedor + docs no-técnicas + +**Objetivo:** democratizar el acceso y abrir la puerta a investigadores no-técnicos. Subtareas: + +11. **#203 CSV — URL + encoding i18n + campos faltantes.** Cierra el feedback hispano (mojibake de tildes en Excel-Windows). Sumar columna URL canónica al landing. +12. **ADR 0046 + #193 skill multi-proveedor.** `--provider claude-code|opencode`. Mapa proveedor → ruta + formato. El **version-lock skill==cli** se preserva. `b2g skill providers`/`list` para descubrir lo soportado. +13. **Quickstart "5 pasos para revisión narrativa asistida por bibliometría" (#187)** + **docs Diátaxis (#134 cierre, #208).** README no-técnico. Tutorial de los 5 pasos del #187: lista ingenua → barrido → diff → lectura dirigida → escritura narrativa. +14. **#245 back-merge release-please** → **diferido**, documentar como toil en `CONTRIBUTING.md`. + +## 6. Dependencias duras y orden de merge + +``` +[Ola 1] +#233 ─┐ + ├─ #204 P1a (backward) ─ #204 P1b (camino único co-citación) + ├─ #221 read group (split de #207, aditivo) + └─ #207 poda deprecación (BREAKING; va al final de Ola 1) + +[Ola 2] +ADR 0045 (paraguas, previa) ─ #258 ─ #259 ─ #260 (serializados: envelope compartido) + └─ #204 P2 (deprecaciones visibles, baratísimo) + +[Ola 3] +#203 CSV ─────────────────┐ +ADR 0046 + #193 skill ────┤ (paralelos: archivos disjuntos) +#187 + #134/#208 docs ────┘ +``` + +**Reglas de serialización (single-writer + archivos calientes):** +- #221 y #207 tocan `cli/__init__.py` → **serializar**. +- Las 3 grietas comparten envelope → **serializar** (un PR chiquito revisable > un PR grande mezclado). +- #203 CSV y ADR 0046/#193 skill tocan archivos disjuntos → pueden ir **en paralelo**. +- La rama actual `feat/233-curate-precedencia-manual` se mergea primero (sin conflicto con nadie). + +## 7. Riesgos identificados + +| # | Riesgo | Mitigación | +|---|---|---| +| 1 | **#207 es breaking.** Va tarde en Ola 1 para que el último PR de Ola 1 antes de Ola 2 no quede con alias deprecados. Si rompe el flujo de alguien, queda en el CHANGELOG con `BREAKING CHANGE:` footer. | Anunciar en el PR; coordinar con quien use los aliases. | +| 2 | **#221 read group + #207** ambos tocan `cli/__init__.py` → riesgo de conflicto. | Serializar. Mergear uno, rebasar el siguiente. | +| 3 | **ADR 0045 paraguas** debe existir ANTES del primer PR (#258). | Escribirla primero como PR independiente; mergear antes de tocar el envelope. | +| 4 | **#193 skill multi-proveedor** es investigación (formato OpenCode a relevar). | ADR 0046 con diseño + DoD + decisión de abstracción antes de implementar. | +| 5 | **#203 CSV i18n** puede requerir reproducir el bug de encoding en Excel-Windows primero. | Repro mínimo como issue previo; el fix puede esperar confirmación. | +| 6 | **Ventana de release ajustada** (2026-07-05). Si una ola se atrasa, no todo entra. | Recortar Ola 3 si hace falta; Ola 1+Ola 2 son el core. | + +## 8. Métricas de cierre del release 0.12.0 + +- ADR 0045 mergeada (paraguas 3 grietas). +- ADR 0046 mergeada (skill multi-proveedor). +- Suite del gate verde (target: **700+ tests**, hoy ~645). +- **El lazo end-to-end corre sin help humano:** `seed → chain forward → curate accept → build → read top --by-community` produce un resumen investigable en **una** llamada. +- **`b2g schema` funciona** y los agentes pueden introspectar el contrato por el mismo canal. +- **`b2g skill add --provider opencode`** instala la skill en OpenCode; `--provider claude-code` sigue funcionando. +- **CSV abre sin mojibake en Excel-Windows** y trae URL/landing. +- `b2g --help` lista **exactamente** los 10 verbos del ciclo + meta + aliases-con-warning. +- README/quickstart "5 pasos para revisión narrativa asistida" publicado. + +## 9. Próximo paso inmediato + +Pasar a modo ejecución. Empezar por mergear la rama actual `feat/233-curate-precedencia-manual` (lista, sin conflictos), luego abrir la ADR 0045 paraguas como PR independiente para que Ola 2 tenga su base contractual. + +--- + +*Auditoría as-built: 2026-06-30. Inconsistencias detectadas durante la revisión → ver §6-riesgos y §7-dependencias.* \ No newline at end of file diff --git a/docs/decisiones/0044-precedencia-inclusion-manual-en-curate.md b/docs/decisiones/0044-precedencia-inclusion-manual-en-curate.md new file mode 100644 index 0000000..d87cdc2 --- /dev/null +++ b/docs/decisiones/0044-precedencia-inclusion-manual-en-curate.md @@ -0,0 +1,133 @@ +# 0044 — Precedencia de la inclusión manual sobre el filtro PRISMA en `curate filter`: `accepted` gana + +- **Estado:** Aceptada +- **Fecha:** 2026-06-30 +- **Decidido por:** **Product Owner humano** (2026-06-30). El PO eligió la opción "respetar + `accepted`": la inclusión manual gana sobre el criterio automático. El **encuadre** (clasificar el + hallazgo como *footgun invisible* y traer las tres opciones de precedencia) es síntesis de la IA + (arquitecto) validada por el PO. +- **Precisa:** [0020](0020-metodo-forrajeo-scent-filtros-reject.md) **§C** ("Filtros PRISMA marcan + `rejected`, NO borran"). 0020 §C fijó **qué** hace un filtro (marca `rejected`, no borra) pero **no** + fijó **sobre qué papers puede actuar** respecto de una decisión de inclusión humana previa. Este ADR + precisa ese scope: el filtro **nunca** actúa sobre `accepted`. No revierte 0020 §C (los filtros + siguen marcando `rejected`, sin borrar, con conteo PRISMA); lo acota. +- **Relacionada con:** [0016](0016-maquina-estados-lazo.md) (enmienda 2026-06-15: la **curación es + transversal** y `accept`/`reject` son "lo único irreductiblemente humano" — la aceptación manual es + una decisión de inclusión del humano, no una etiqueta que un filtro masivo pueda pisar), + [0037](0037-superficie-cli-10-verbos-ciclo.md) (**enmienda D2**: dentro del grupo `curate`, la + transición la define el verbo; **`curate filter`→`FILTERED`** — este ADR fija la **política de + precedencia** de ese mismo verbo, sin tocar su transición), [0009](0009-biblioteca-viva-duckdb.md) / + [0013](0013-identidad-hash-merge-corpus.md) (biblioteca viva, curación reversible y `provenance` + append-only: la re-aceptación sigue disponible), [0022](0022-producto-sin-ia-generativa.md) + (determinismo/honestidad: el motor no toma decisiones silenciosas que el humano no pidió). +- **Origen:** issue [#233](https://github.com/complexluise/bib2graph/issues/233) — en una corrida e2e + autónoma del CLI (2026-06-30, sobre `examples/valoraciones/`), `curate filter` reclasificó a + `rejected` **2 papers** que estaban `accepted` a mano, **sin aviso**. Clase **footgun invisible**: + `exit 0`, sin señal, con pérdida silenciosa de una decisión de inclusión humana. Encuadre: + [Nota 22](../Notas/22-marco-software-donde-nos-paramos.md) (frontera bib2graph/producto: honestidad + y determinismo; "el humano decide la inclusión"). + +## Contexto + +`curate filter` (ADR [0037](0037-superficie-cli-10-verbos-ciclo.md), decisión (b) + enmienda D2) +aplica criterios de inclusión/exclusión tipo PRISMA (`--year-gte`/`--year-lte`, `--language`, +`--type`, `--min-citations`) marcando `curation_status='rejected'` a los papers que **no pasan** el +criterio, sin borrarlos (ADR [0020](0020-metodo-forrajeo-scent-filtros-reject.md) §C). La lógica pura +vive en `filters/prisma.py` (`apply_filter`/`apply_filters`), invocada por +`service/curate.py:filter_corpus`. + +El único paper que hoy el filtro **excluye del set a rechazar** es el que **ya está** `rejected` (para +no re-rechazarlo). Un paper `accepted` **manualmente** que no cumple el criterio **sí** es candidato a +`rejected`. Ese es el hallazgo de #233: un filtro masivo pisó, sin aviso, dos decisiones de inclusión +humanas. + +Esto contradice dos principios ya registrados: + +1. **La curación es lo irreductiblemente humano** (ADR [0016](0016-maquina-estados-lazo.md), enmienda + 2026-06-15, §3): `accept`/`reject` son la decisión que el sistema **no** toma por el investigador. + Un `accepted` es una afirmación explícita de "este entra". +2. **Honestidad y determinismo, sin decisiones silenciosas** (Nota 22 / ADR + [0022](0022-producto-sin-ia-generativa.md)): un criterio automático que revierte, sin señal, una + inclusión humana es exactamente el tipo de sorpresa que el motor determinista no debe producir. + +La pregunta abierta: **cuando un criterio masivo alcanza a un paper `accepted` a mano, ¿quién gana?** + +## Decisión + +**La inclusión manual gana: `curate filter` NUNCA mueve un paper `accepted` → `rejected`.** + +- El filtro actúa **solo** sobre papers **no aceptados** (estado `candidate` y demás no-aceptados). Un + paper con `curation_status='accepted'` queda **intacto** aunque no cumpla el criterio del filtro; el + filtro lo **omite** del conjunto a rechazar (igual que ya omite a los `rejected`). +- **Racional:** coherente con "el humano decide la inclusión" (Nota 22) y con la curación como acto + irreductiblemente humano (ADR 0016 §3). Un filtro masivo es una herramienta de **cribado del + candidato**, no un mecanismo para revertir inclusiones humanas explícitas. La aceptación manual es + una señal de mayor autoridad que un criterio de barrido. +- **Alcance:** esta decisión fija la **política de precedencia** del verbo `curate filter`. **No** + toca su transición (`curate filter`→`FILTERED`, ADR 0037 D2), ni el marcado `rejected`-no-borra (ADR + 0020 §C), ni el conteo PRISMA por paso, ni el determinismo del filtro. +- **Nota de trabajo futuro (NO parte de esta decisión):** si más adelante hiciera falta un *reset + duro*, la evolución natural del default seguro sería un flag **`--force` explícito** que permita al + filtro rechazar `accepted` **con aviso accionable** (p. ej. *"N accepted fueron rechazados por el + criterio X: …"*). Se deja **mencionado como evolución posible**, no decidido aquí; requeriría su + propio encuadre (y probablemente una precisión de contrato en `docs/API.md`). + +## Consecuencias + +- **Se cierra el footgun de #233.** Ningún `curate filter` puede revertir una inclusión humana en + silencio. El default es **seguro por construcción**: lo que el investigador aceptó, se queda. +- **El conteo PRISMA sigue siendo honesto.** `count_before`/`count_after` ya se computan sobre los + papers **no-rejected** (candidate + accepted, ADR 0020 §C / `API.md` §curate); con esta decisión los + `accepted` simplemente **nunca abandonan** ese conjunto por efecto de un filtro. El reporte de + cribado describe qué se excluyó del **candidato**, que es lo que PRISMA quiere contar en la etapa de + screening. +- **La reversibilidad se conserva** (ADR 0009/0013): si el investigador **quiere** que un `accepted` + salga, lo hace explícito con `curate reject --ids ...` — decisión humana registrada en `provenance`, + no efecto lateral de un barrido. +- **Determinismo intacto** (ADR 0022): el filtro sigue siendo función pura y reproducible; solo cambia + el conjunto sobre el que puede marcar `rejected` (excluye `accepted`, como ya excluía `rejected`). +- **Costo / trade-off:** un usuario que **sí** quisiera un cribado destructivo total (rechazar todo lo + que no cumpla, incluido lo aceptado) hoy **no** tiene un atajo masivo; debe rechazar a mano los + `accepted` que quiera sacar. Es el costo aceptado del default seguro, y es exactamente el hueco que + el futuro `--force` (arriba) cubriría si la necesidad aparece. +- **`docs/API.md` a precisar en la implementación.** La sección de `curate filter` (§curate y las + "Notas de contrato" del ADR 0020) debe declarar que el filtro **omite `accepted`**. *Esa edición es + trabajo del `coder` al implementar #233, no parte de este ADR* (coherente con cómo 0037/0038 dejaron + la edición de `API.md` para el hito de implementación). + +### Recomendación para el `coder` (implementación de #233) + +**Archivo/símbolo:** `src/bib2graph/filters/prisma.py:179-184` (`apply_filter`, construcción de +`ids_to_reject`). + +**Cambio:** hoy el guard excluye solo `CurationStatus.REJECTED`; sumar la exclusión de +`CurationStatus.ACCEPTED` para que el filtro **omita** los aceptados: + + ids_to_reject = [ + str(row[Col.ID]) + for row in rows + if row.get(Col.CURATION_STATUS) + not in (CurationStatus.REJECTED, CurationStatus.ACCEPTED) # ← accepted intocable + and not _passes(row, criterion) + ] + +- **Test (TDD, ancla la semántica):** corpus con un paper `accepted` que **no** cumple el criterio + (p. ej. `year=2005` con `--year-gte 2010`) → tras `curate filter` sigue `accepted` (no pasa a + `rejected`); un paper `candidate` que no cumple sí pasa a `rejected`. Regresión directa del #233 + sobre `examples/valoraciones/`. +- **`filter_corpus`** (`service/curate.py:391`) no necesita cambio: consume la lógica pura de + `prisma.py`. Verificar que el conteo PRISMA por paso sigue coherente (los `accepted` ya estaban del + lado `count_before`/`count_after`). + +## Alternativas + +- **Avisar / requerir `--force`** (el filtro **puede** rechazar `accepted`, pero emite advertencia + accionable —"N accepted fueron rechazados: …"— y/o exige `--force`). **Rechazada como default:** sigue + permitiendo la **pérdida silenciosa** si el usuario no lee la advertencia, y `exit 0` con salida + masiva es fácil de no leer (justo el modo de fallo de #233). La **idea del `--force`** se **reserva** + como evolución futura del default seguro (ver "Nota de trabajo futuro"), no como comportamiento por + omisión. +- **Conducta actual (el filtro pisa `accepted` por diseño), solo documentarla.** **Rechazada:** es el + footgun mismo — máximo riesgo de pérdida silenciosa de decisiones humanas, y contradice de frente la + Nota 22 (honestidad/determinismo) y el ADR 0016 §3 (la curación es lo irreductiblemente humano). + Documentar un comportamiento peligroso no lo hace seguro. diff --git a/docs/decisiones/README.md b/docs/decisiones/README.md index 2488c5d..306d3eb 100644 --- a/docs/decisiones/README.md +++ b/docs/decisiones/README.md @@ -91,3 +91,4 @@ Qué se vuelve posible/fácil y qué se vuelve costoso/imposible. Trade-offs hon | [0040](0040-retiro-gui-local.md) | Retirar la GUI local de la librería (BREAKING): se eliminan `b2g gui`, la API local FastAPI (`api/`), la SPA `frontend/`, el extra `[gui]` y el vendoreo del frontend en el wheel; el core es CLI/agente-native sobre la biblioteca viva. **La capa `service/` (incl. `reads.py`) se conserva** (la usa el CLI). Conteo: "10 verbos + `skill`" | **Aceptada** (2026-06-28). **Supersede [0027](0027-pivote-posicionamiento-gui-local.md)/[0028](0028-arquitectura-gui-api-capa-servicios.md)**; enmienda [0038](0038-destino-verbos-huerfanos-0037.md) (retira la excepción `gui`); relacionada 0010/0021/0037/0039/0005/0019/0032/0033/0034; no introduce IA (0022). Issue [#190](https://github.com/complexluise/bib2graph/issues/190); limpieza profunda de docs en [#191](https://github.com/complexluise/bib2graph/issues/191) | | [0041](0041-documentacion-por-superficie-de-entrega.md) | Organizar la documentación por **superficie de entrega**, no por tipo de lector: los tres usuarios = **dos superficies + un mediador** (agente dentro del repo → `AGENTS.md`/código; humano-y-agente navegando → sitio Diátaxis; IA externa → `llms.txt`). **No se forkea el sitio por audiencia**; se publica `llms.txt` (derivado de `AGENTS.md` + referencia CLI) en la raíz del sitio; persona = **veneer de navegación** (3 puertas por intención, 1 cuerpo); `AGENTS.md` sube al nav. La operación-por-IA es objetivo de 1ª clase | **Aceptada** (2026-06-29). Net-new (ningún ADR gobernaba la estructura de la doc); **no cambia contratos** (envelope/exit/FSM intactos). Relacionada 0039 (skill = doc operativa mediada por IA, fuera del sitio)/0010/0021/0037/0038/0040; no introduce IA (0022) | | [0043](0043-posicionamiento-agent-native-cli.md) | bib2graph **implementa deliberadamente** el contrato CLI-para-agentes (envelope `schema="1"` versionado, exit codes 0–5 tipados, comandos self-contained, identidad de recurso explícita): postura de diseño, no accidente. Las **relajaciones de trust & safety (principios 4–7)** son **decisión por bajo blast radius** (analítica local, store DuckDB local, sin borrado, daño recuperable por snapshot), no huecos — *fail-open advisory*; **no** se adopta fricción de confirmación (mataría la autonomía del agente). Tres grietas = **roadmap explícito**: subcódigo de red (`RATE_LIMITED` 429 vs `UPSTREAM_TIMEOUT` 504), eco del workspace resuelto, `b2g schema` (introspección versionada) | **Aceptada** (2026-06-30). **Extiende [0021](0021-cli-agente-native-contrato.md)** (posiciona el contrato; **no** lo cambia: envelope/exit/FSM intactos). Relacionada 0010/0037/0038/0039 (`schema` = comando meta fuera de los 10)/0041; no introduce IA (0022). Origen: dos auditorías (PO + agente) contra el *Rubric for Agent-Native CLI Design* | +| [0044](0044-precedencia-inclusion-manual-en-curate.md) | Precedencia en `curate filter`: la **inclusión manual gana** — el filtro PRISMA **nunca** mueve un paper `accepted`→`rejected` (actúa solo sobre no-aceptados); cierra el *footgun invisible* de reclasificar sin aviso decisiones humanas | **Aceptada** (2026-06-30). **Precisa [0020](0020-metodo-forrajeo-scent-filtros-reject.md) §C** (acota el scope del filtro que marca `rejected`, sin revertirlo). Relacionada 0016 (curación transversal = lo irreductiblemente humano)/0037 (D2: `curate filter`→`FILTERED`)/0009/0013/0022. Origen: issue [#233](https://github.com/complexluise/bib2graph/issues/233); futuro `--force` mencionado, no decidido | diff --git a/src/bib2graph/filters/prisma.py b/src/bib2graph/filters/prisma.py index 9e6dd40..f3fe251 100644 --- a/src/bib2graph/filters/prisma.py +++ b/src/bib2graph/filters/prisma.py @@ -15,6 +15,10 @@ R5: campo o operador desconocido → ``ValueError`` accionable (antes era no-op silencioso). +ADR 0044: la inclusión manual gana — el filtro nunca rechaza un paper +``accepted``, solo actúa sobre ``candidate`` (y demás no-``accepted``, +no-``rejected``). + Ver docs/API.md §convenciones (CLI ``filter``). """ @@ -160,6 +164,11 @@ def apply_filter( Los papers rechazados siguen en la tabla con ``curation_status='rejected'`` (NUNCA se borran; flujo PRISMA). + ADR 0044: la inclusión manual gana sobre el criterio automático — el + filtro NUNCA mueve un paper ``accepted`` a ``rejected``. El filtro actúa + solo sobre papers no-aceptados (``candidate`` y demás no-``accepted``), + igual que ya omite a los ya-``rejected``. + 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. @@ -179,7 +188,8 @@ def apply_filter( ids_to_reject = [ str(row[Col.ID]) for row in rows - if row.get(Col.CURATION_STATUS) != CurationStatus.REJECTED + if row.get(Col.CURATION_STATUS) + not in (CurationStatus.REJECTED, CurationStatus.ACCEPTED) and not _passes(row, criterion) ] diff --git a/tests/unit/test_filters.py b/tests/unit/test_filters.py index 31459e0..16c170a 100644 --- a/tests/unit/test_filters.py +++ b/tests/unit/test_filters.py @@ -131,6 +131,50 @@ def test_rejected_previos_no_se_cuentan(self) -> None: assert step.count_after == 1 +class TestApplyFilterPrecedenciaInclusionManual: + """Regresión #233 / ADR 0044: ``accepted`` es intocable para el filtro.""" + + def test_accepted_que_no_cumple_criterio_no_se_rechaza(self) -> None: + """Un paper accepted que no cumple el criterio queda intacto.""" + corpus = _make_corpus( + _base_row("P1", year=2005, curation_status="accepted"), + _base_row("P2", year=2020, curation_status="candidate"), + ) + criterion = FilterCriterion(field="year", op="gte", value=2010) + new_corpus, step = apply_filter(corpus, criterion) + + rows = new_corpus.to_arrow().to_pylist() + statuses = {r["id"]: r["curation_status"] for r in rows} + # El accepted NUNCA se mueve a rejected, aunque no pase el criterio. + assert statuses["P1"] == "accepted" + # El candidate que no cumple sí se rechaza: el filtro sigue vivo. + assert statuses["P2"] == "candidate" + + # count_before/count_after siguen contando candidate+accepted (ADR 0020 §C): + # ambos papers están del lado no-rejected antes y después, porque el + # accepted nunca sale de ese conjunto por efecto del filtro. + assert step.count_before == 2 + assert step.count_after == 2 + + def test_candidate_que_no_cumple_criterio_se_rechaza(self) -> None: + """El filtro sigue funcionando: un candidate que no cumple sí se rechaza.""" + corpus = _make_corpus( + _base_row("P1", year=2005, curation_status="accepted"), + _base_row("P2", year=2005, curation_status="candidate"), + ) + criterion = FilterCriterion(field="year", op="gte", value=2010) + new_corpus, step = apply_filter(corpus, criterion) + + rows = new_corpus.to_arrow().to_pylist() + statuses = {r["id"]: r["curation_status"] for r in rows} + assert statuses["P1"] == "accepted" + assert statuses["P2"] == "rejected" + + # count_before = 2 (accepted + candidate); count_after = 1 (solo accepted). + assert step.count_before == 2 + assert step.count_after == 1 + + class TestApplyFilterIdioma: def test_filtra_por_language_eq(self) -> None: corpus = _make_corpus(