Guía para agentes que operen en este repositorio. El proyecto es una reescritura clean-room construida de adentro hacia afuera (docs → núcleo puro y tests → costuras). Estado (v0.3): Hitos 0–6 + 1.5 construidos, remediación R1–R5 COMPLETA, Hito 8 COMPLETO (Enricher OpenAlex: refs→DOI + co-citación end-to-end) y Hito 7 COMPLETO (dedup fuzzy determinista
rapidfuzz, automático en la ingesta desde #88;rapidfuzzen el núcleo, sin extra[dedup]— ADR 0031), tras el red-team de la Nota 06 y el modelo nuevo (ADR 0022/0023; el producto no usa IA generativa — el desarrollo SÍ es asistido por IA, pero el scent es bibliométrico determinista) y el Hito 9 COMPLETO (capa declarativaNetworkSpecYAML:load_specs+resolution+ 16° subcomandob2g networks). Verdocs/ROADMAP/y "Estado actual" abajo. El diseño objetivo vive endocs/ARCHITECTURE.md; los contratos públicos endocs/API.md; el producto endocs/PRD.md; las reglas que motivan este código endocs/Notas/01-lecciones-v0.md. Las decisiones vigentes tras el giro son los ADR 0007 (OpenAlex backbone), 0008 (wedge = forrajeo), 0009 (biblioteca viva en DuckDB), 0010 (agente-native columna) y 0011 (thesaurus), sobre la base del 0006 (tabla canónica Arrow).
- Hitos 0–6 + 1.5 CONSTRUIDOS y remediación R1–R5 COMPLETA (v0.3, 2026-06-16): de una
ecuación de búsqueda a las redes bibliométricas, desde código Python o desde el CLI
b2g, sobre una biblioteca viva en DuckDB. El árbolsrc/bib2graph/tiene ~30 módulos:constants.pyyschemas.py(capa base, R1;ProvenanceEventvive enschemas.py, no haymodels.py),corpus.py,cycle.py(FSM cíclico de dominio, R3),backends/(TabularBackend+InMemoryBackend+DuckDBBackend),stores/(DuckDBStore),sources/(OpenAlexSource,BibtexSource),foraging/(Forager, scent bibliométrico),preprocessors/(normalize + dedup automáticos en la ingesta + thesaurus explícito),filters/(PRISMA),networks/(proyectores, analyzer, spec, facade),sources/equation.py(capa declarativa de la ecuación, 9a),exporters/(GraphML, CSV) ycli/. El CLIb2ges real —paquetecli/, no un placeholder—. Superficie 0.10.0 (ADR 0037/0038/0039/0040, AS-BUILT): 10 verbos del ciclo (init,seed,chain,curate,build,read,export,snapshot,status,validate) + 3 grupos noun-verb (read {list,stats,show,top},curate {dump,apply,accept,reject,filter},snapshot {create,restore}) + 1 comando meta fuera de los 10 (skill add, ADR 0039 — instala la skill de Claude Code end-user que materializa el mensaje "la mejor forma de usar bib2graph es pedirle a Claude que lo use" [pip install bib2graph→b2g skill add]; vendoreada en el wheel con version-lock skill==cli) + 9 aliases deprecados (accept/reject/filter/inspect/monitor/networks/enrich/restore/resolve, retiro 0.11.0).thesaurusse retiró como verbo (#164): su capacidad esb2g build --thesaurus. El verboguise retiró con la GUI local (ADR 0040, #190 — ver abajo). Conteo verificable contrab2g --help(10 del ciclo +skill); detalle endocs/API.md§Convenciones CLI. Grupo noun-verbread {list,stats,show,top}(#156/#157, ADR 0037 §b): primer grupo del CLI (lectura pura del corpus, no transiciona);read listfiltra por--query/--status/--seeds|--candidates/--year,read stats --group-by {status,year,is_seed},read show --id(resuelve id/doi/source_id, ADR 0036),read top --top N --kind {…}(la salida de investigación: nodos centrales + co-citación con título; defaultbibliographic_coupling, robusto en one-shot; co-citación vacía → honest-empty exit 0 + reason/fix_command). Artefactos one-shot honestos (#160, ADR 0037 §f): el--jsondebuild,snapshot createyread topsuma un bloque aditivomaturity({curated, scope, saturated, empty_networks},schema="1"intacto) que autodeclara que el resultado es un borrador sin pulir; forma estable endocs/API.md§Apéndicematurity.readsin subcomando → ayuda + exit 0 (invoke_without_command=True, workaround Click 8.4); elcommanddel envelope usa la ruta completa ("read list").inspectqueda en deprecación (#165, lo absorbenread show+status) pero sigue vivo. Verdocs/API.md§Convenciones CLI. Grupo noun-verbcurate {dump,apply,accept,reject,filter}(#155, ADR 0037 §b): SEGUNDO grupo del CLI. BREAKING: la forma-flagcurate --dump/--from-csvy--allfueron eliminadas sin alias (dump --scope allreemplaza a--all;apply <csv>reemplaza a--from-csv). A diferencia deread(transversal entero), la transición la define el VERBO (precedente D1 de #159): solocurate filter→FILTERED;dump/apply/accept/rejectson transversales. Lógica fuente única enservice/curate.py(run_curate_dump/run_curate_from_csv/filter_corpuscondecided_atinyectado). Los verbos sueltosaccept/reject/filtersiguen vivos como alias deprecados (retiro 0.11.0, ADR 0038 P1 + enmienda #155). Verdocs/API.md§curate. Grupo noun-verbsnapshot {create, restore}(#163, ADR 0038): TERCER grupo del CLI. BREAKING: elsnapshotplano →snapshot create(sin alias);snapshot restore= ex verbo planorestore(el sueltorestoresigue vivo como alias deprecado,command="restore", retiro #165). La transición la define el VERBO:snapshot createNO transiciona y lleva el bloquematurity;snapshot restore→FILTERED. Lógica fuente única enservice/snapshot.py(run_snapshot/run_restore,decided_atinyectado). Verdocs/API.md§snapshot. 645 tests verdes (mypy/ruff limpios; el núcleo importa sinduckdb). Entre las redes, la composición de comunidades es exportable:networks/cluster_table(función pura) resume cada comunidad de una red de paper en una fila yb2g buildla escribe comoclusters.csv(#31, AS-BUILT 2026-06-17; verdocs/API.md§7.2).b2g snapshot create/b2g exportresuelven por workspace (--out-diroverride opcional →<workspace>/snapshots|exports/) yb2g statusavisa de staleness de la cache de redes (networks_cache_stale; avisa, no regenera) — remanentes del modelo workspace cerrados (#32, AS-BUILT 2026-06-17; ver el bullet de workspace abajo). - Hito 9 COMPLETO — capa declarativa
NetworkSpecYAML (AS-BUILT 2026-06-17):NetworkSpec(networks/spec.py) suma el camporesolution: float = 1.0(resolución de Louvain, fuera delcorpus_hash→ seed intacto) yextra="forbid". Nueva funciónload_specs(path)(re-exportada desdenetworks/; esquema raíznetworks:, errores accionables citando archivo +red #<idx>+ campo). 16° subcomandob2g networks --spec <yaml>(cli/commands/networks.py): carga el YAML →Networks.buildpor red → escribe artefactos con el helper compartido_write_artifacts(mismos GraphML + metrics.json + clusters.csv quebuild); NO transiciona elCycleStateni sella.corpus_hash(transversal al lazo, comoenrich/curate).pyyamlpasó a dependencia del núcleo (import perezoso). 516 tests verdes. Verdocs/API.md§10. Absorbido porb2g build --spec(#159, ADR 0037 (a) / 0038):buildahora carga el mismo YAML (helper compartido_build_from_spec_file) y sí transiciona aBUILT+ sella.corpus_hash(decisión D1);buildsuma--scope all|accepted|seeds(defaultall) y--min-weight(solo modo quick).networksy--corpus-scopequedan como alias en deprecación (cierran 0.11.0). - GUI local — ⛔ FUERA de la librería (ADR 0040,
#190): el core es CLI/agente-native. No
existen
b2g gui, la API local FastAPI (api/), la SPAfrontend/, el extra[gui]ni el vendoreo del frontend en el wheel; el repo es 100% Python con uv. La capa de servicios neutralservice/(incl.reads.py) se conserva (la usa el CLI:read/curate/snapshot/…). La experiencia visual library-centric vive en un producto separado. El historial de la SPA vive engit log. - #88 — preprocesamiento automático en la ingesta (AS-BUILT 2026-06-18, ADR
0031):
normalize+ dedup fuzzy corren automáticamente enseed/seed_from_bib/chain/restore(helpercli/_ingest.py::normalize_and_dedupsobre el corpus completo mergeado ⇒ dedup cross-biblioteca); el corpus queda siempre normalizado y deduplicado.rapidfuzzpasa al núcleo ([project.dependencies]; el extra[dedup]se elimina, import ya no perezoso). El thesaurus era entonces el 18° subcomandob2g thesaurus --from <archivo>—RETIRADO como verbo en 0.10.0 (#164, ADR 0038): su capacidad vive como flagb2g build --thesaurus—. La ingesta y la pasadabuild --thesauruspersisten conpersist_replace/overwrite_corpus(DELETE+INSERT, preservan tablas hermanas; evita que el upsert-concat D3 reintroduzca variantes).build/networkssiguen puros. Deuda conocida: dedup O(n²) por ingesta (optimización futura) y skip #93 (test_run_seed_from_bib_reseed_incrementa_ronda, crashBibDataString/pyparsingen reseed mismo-proceso; no afecta el CLI real). La revisión asistida de clusters ambiguos queda diferida (requiere superficie interactiva). Verdocs/API.md§6/§11/§4.1. - Ciclo B —
examples/valoraciones/rehecho 100% por CLI (AS-BUILT 2026-06-17, ADR 0030 §Ciclo B): materializa el principio CLI-puro del PO.build_corpus.pyeliminado; el ejemplo se arma y reproduce por CLI (seed --spec equation.yamlmax_results 80→curate --from-csv curacion.csv10accepted→enrich --max-citing 25→snapshot). Corpus = ~80 filas, 10acceptedenriquecidas, co-citación presente (rala) — antes 137 filas / co-citación vacía (9b). Nuevo artefacto congeladocuracion.csv(receta determinista de curación). Gate R2 ajustado (pison>=50,test_cocitacion_con_datoscon 5 redes). La procedencia de un ejemplo deja de ser un script y pasa a ser la receta CLI del README +equation.yaml+curacion.csv(supersede la convención de 9b). 598 tests por defecto (los 2networkquedan fuera del gate). Verdocs/API.md§2.1. - Ciclo 10 —
seed --from-bib+ filtro de año real (AS-BUILT 2026-06-17, ADR 0030 §Ciclo 10, cierra #50):b2g seedpasa a 3 modos mutuamente excluyentes (--equation/--spec/--from-bib <archivo.bib>, este último sin red víaBibtexSource.load;SEEDED/reseed; exit 3 si faltabibtexparser; combinar--from-bibcon flags OpenAlex → exit 1).min_year/max_yearahora filtran de verdad contra OpenAlex (from_publication_date/to_publication_date; flags--min-year/--max-yearen--equation+ campos del YAML en--spec). Nuevo ejemploexamples/bibtex/(sample.bib+ README CLI-puro). 594 tests verdes. Verdocs/API.md§2. - Ciclo 9a — ecuación declarativa +
restore(AS-BUILT 2026-06-17, ADR 0030): (1)b2g seedtenía entonces 2 modos (--equation/--spec equation.yaml) —EquationSpec+load_equation_spec(sources/equation.py, Pydanticextra="forbid";min_year/max_yearaún no filtraban en 9a; filtran desde el Ciclo 10, arriba). (2) Nuevo 17° subcomandob2g restore --from-corpus <parquet>(cli/commands/restore.py): rehidrata un corpus ya curado sin red (inverso desnapshot;CORPUS_SCHEMA→Corpus.from_arrow→ merge+persist), preserva la curación y transiciona aFILTERED(reusa la transición permisivafilter, ADR 0016). No hayseed --from-corpus(esrestore);seed --from-bibestaba diferido en 9a y se construyó en el Ciclo 10 (arriba). Verdocs/API.md§2 + §convenciones CLI. - Ciclo 9b — corpus de ejemplo + gate R2 · #33 CERRADO (AS-BUILT 2026-06-17, ADR
0030): se construye la convención
examples/(corpus curado congelado por carpeta:corpus.parquet+equation.yamlde procedencia +README.md+ script determinista de regeneración; verdocs/API.md§2.1) y su primer ejemplo,examples/valoraciones/(137 filas: 7accepted, 130candidate, 107 seeds; corpus real del PO reducido determinísticamente, CC0/OpenAlex). Excepción acotada al.gitignore(!examples/- regla defensiva
examples/**/*.duckdb). El gate R2 (tests/unit/test_example_r2_gate.py, 7 tests) correrestore --from-corpus→build→networkssin red sobre el corpus real y assertacorpus_hashestable + composición de comunidades Louvain estable entre corridas (cierra el agujero R2 de la Nota 09). Con esto #33 queda cerrado (caso real reproducible sin red). Verdocs/API.md§2.1.
- regla defensiva
- Hito 8 COMPLETO (Ciclos 8a + 8b, ADR
0025): el
OpenAlexEnricher(opt-in, núcleo) hace 2 pasadas — refs→DOI (8a) + co-citación end-to-end (8b): poblacited_by_idtrayendo los citantes de las semillas aceptadas víaOpenAlexSource.fetch_citing_batch(batcheo OR ≤50 con presupuesto por semilla) y los une (idempotente, sin crecer el corpus).b2g enrichcon--max-citing(tope por semilla);Networks.quick→ 4 o 5 redes según hayacited_by_id. - Forward chaining materializa metadata REAL (#78, 2026-06-17, AS-BUILT CERRADO): el forward del
Forager(b2g chain) ya no persiste placeholders[candidate:W...]— materializa filas reales (título/año/autores). Causa raíz:fetch_citing_batchtraía la metadata completa (_FIELDS) y la descartaba; el fix A1 (cero red extra) la conserva vía el método nuevoOpenAlexSource.fetch_citing_batch_with_works(ids, *, max_per_paper) -> (attribution, works_map)(fetch_citing_batchqueda intacto, thin wrapper)._build_forward_candidate_roweliminado;_work_to_rowganóchaining_hop/source_tag(defaults backward-compat). Asimetría deliberada con el backward (#54): el backward observa sin materializar, el forward materializa (citantes pocos, acotados, se curan, metadata ya en la request). Con #78, el materializador on-demand #71 queda solo para backward. 645 tests verdes, verifier PASA. Verdocs/API.md§2 (fetch_citing_batch_with_works)/§5 y ADR 0020 §AS-BUILT #78. - Backward chaining sin placeholders (#54, 2026-06-17): el backward del
Forager(b2g chain) ya no persiste filas-fantasma[candidate:W...]en el corpus (revierte el comportamiento de Hito 5 — la promesa de "no contaminan" era falsa: los stubs llegaron a ser ~la mitad del corpus y entraban alcorpus_hash; Notas 09/12). Los IDs observados salen porRankedCandidates.observed_refsyb2g chainlos persiste en la tabla append-only hermanareferenced_but_not_fetched(backends/base.pyProtocol +DuckDBBackend/InMemoryBackend:add_referenced_refs/referenced_refs_count/referenced_refs), fuera delcorpus_hash(arregla la contaminación previa).b2g statussumareferenced_not_fetched;b2g chainsumaobserved_refs_count. El forward arrastraba el MISMO footgun, cerrado en #78 (arriba). Verdocs/API.md§5/§4 y ADR 0020 §AS-BUILT #54. - Forward chaining del
Foragerbatcheado (#21, 2026-06-16): el forward delForager(b2g chain, incl.chain --since—exmonitor, #158) ya no es N+1 — reusaOpenAlexSource.fetch_citing_batch(batcheo OR- cap por semilla
max_citing_per_paper/--max-citing, default 50) con preview sin red. Opera sobreis_seed=True(todas las semillas, sin filtrarcuration_status): el chaining precede a la curación; la restricción aacceptedes del Enricher (Hito 8b), no del Forager. Verdocs/API.md§5 y ADR 0020 AS-BUILT #21.
- cap por semilla
- Labels legibles en las redes (#25, 2026-06-16): las redes ahora salen con
labellegible (másyear/is_seed/curation_status/degree_centrality/community) vía la nueva capa fronteradecorate(networks/decorate.py), aplicada enfacade.py:_build_artifact;b2g build/exportexportan grafos legibles en Gephi/VOSviewer. Los proyectores siguen puros (ADR 0014). Cierra el hueco de la Nota 09 B3 (redes con id crudo). Verdocs/API.md§7.1. - Tanda de remediación R1–R5 COMPLETA (v0.3, 2026-06-16). Tras el red-team del AS-BUILT
(
docs/Notas/06-critica-as-built-v0.2.md) el PO bloqueó un modelo nuevo (ADR 0022/ 0023 + enmiendas), ya construido: R1 — capa baseconstants.py/schemas.pyúnica (conProvenanceEventenschemas.py, no en unmodels.py); R2 — identidad ≠ procedencia (elcorpus_hashexcluye timestamps, reloj en la frontera, Louvain seeded); R3 — FSM cíclico de dominiocycle.py(sale del backend) conreseed/ronda + curación transversal enstatus; R4 — scent bibliométrico vía proyectores, el producto NO usa IA generativa (se eliminaronforaging/explain.py,explain_candidate, el extra[llm]y la "máquina de tensiones"); R5 — robustez (bulk-load, UTF-8 en la frontera, retry, footguns). Verdocs/ROADMAP/(Hitos R1–R5). Tras la remediación se construyeron el Hito 8 (Enricher OpenAlex: refs→DOI + co-citación end-to-end) y el Hito 7 ✅ (dedup fuzzy deterministarapidfuzz:deduplicate_authors/deduplicate_keywords; ADR 0026 — automático en la ingesta yrapidfuzzal núcleo desde #88, ADR 0031), el Hito 9 ✅ (NetworkSpecYAML) y el Ciclo #33 ✅ (ecuación declarativa +restore+ corpus de ejemplo, 9a+9b). El entorno se levanta conuv sync. - Fundación workspace COMPLETA (ADR
0029, AS-BUILT 2026-06-16; issues
#32/
#38/
#39): una investigación = un workspace =
carpeta (
workspace.json+library.duckdb+networks//snapshots//exports/). Nuevo módulosrc/bib2graph/workspace.py(Workspace,WorkspaceManifest; el núcleo NO importaduckdb) + 14° subcomandob2g init. Se agregó--workspace(opcional) con resolución ambiente (flag > envB2G_WORKSPACE> walk-up del cwd buscandoworkspace.json).b2g statussumaworkspace: {root, source};b2g buildsellanetworks/.corpus_hash. 422 tests verdes, 14 subcomandos. Flujo:b2g init <name>→ trabajar dentro de la carpeta. Remanentes cerrados (#32, AS-BUILT 2026-06-17):b2g snapshot/b2g exportya resuelven por workspace (--out-dirpasó a override opcional →<workspace>/snapshots|exports/) yb2g statussumanetworks_cache_stale: bool+warningsaccionable cuando elnetworks/.corpus_hashno coincide con el corpus vivo (avisa, NO regenera: invalidación por hash, no build-system).Workspaceganóread_networks_corpus_hash()/is_networks_cache_stale(). Con esto el modelo workspace queda completo (no quedan remanentes). BREAKING (#75, 2026-06-17): la opción--storese eliminó por completo del CLI (pasarla da el error estándar de ClickNo such option) y el modo degenerado dejó de existir — la carpeta conworkspace.jsones la única unidad canónica; un.duckdblegacy se adopta conb2g init .. - Curación a escala vía CSV (#22 + #26, 2026-06-16): nuevo 15° subcomando
b2g curate(cli/commands/curate.py) con dos modos mutuamente excluyentes —--dumpescribecuracion.csv(default<workspace>/exports/;--outoverride;--allpara todo el corpus, default solo candidatos) para revisión offline en Excel/Calc, y--from-csvaplica las decisiones en lote (accepted→accept /rejected→reject /undecided→no-op), idempotente (reimportar = mismocorpus_hash;decided_atinyectado en la frontera, R2) y con validación accionable + reporte de IDs huérfanos (not_found_count, cierra el no-op silencioso).noteadvisory (round-trip, ignorado al importar);scent_scorebest-effort,clusterdiferido. Curación transversal (NO transiciona elCycleState). Cierra el hueco de la Nota 09 B4/B5/P1 (sin dump CSV ni reimport en lote, la curación a escala no era viable). 476 tests verdes, 15 subcomandos. Verdocs/API.md§convenciones CLI. - Ergonomía de
b2g seed(#14 + #30, 2026-06-16):--max-results INTpropaga aOpenAlexSource(max_results=...)(sin flag = default 200) para explorar con muestras chicas;--exclude TEXT(repetible) son negaciones quirúrgicas que inyectan cadaAND NOT "<término>"dentro de la única expresióntitle_and_abstract.search:((query) AND NOT "<término>")(el campo no se repite; la forma vieja con campo repetido devolvía 0 en OpenAlex, corregido AS-BUILT 2026-06-17, fix de #30 validado contra la API real vía test@pytest.mark.network) y se reportan en eltranslation_reportdelSeedResult(query visible, ignorado con--native). Cierra el síntoma B1 de la Nota 09. 476 tests verdes. Verdocs/API.md§2 + §convenciones CLI. - Toda la información del producto, la arquitectura, los contratos y la secuencia de
construcción está en
docs/. Leerdocs/ROADMAP/antes de tocar nada: cada hito declara qué historias del PRD §7 cumple, sus criterios de aceptación (DoD) y los tests TDD que se escriben. El orden es deliberado (núcleo puro → costura local DuckDB → costura red OpenAlex → forrajeo → CLI → opcionales). - No hay Cursor rules (
.cursor/,.cursorrules) ni Copilot rules (.github/copilot-instructions.md). - El modelo de dominio es una tabla Arrow (no 4 dicts + dataclasses). Las "entidades"
son vistas derivadas. Validación con Pydantic v2. Detalle en
docs/API.md§1. - La persistencia por defecto es
DuckDBStorestateful — la biblioteca viva (ADR 0009): acumula entre corridas, con tablas de procedencia/curación. Es núcleo, no extra. El snapshot (CorpusSnapshot: parquet +manifest.json) es un export sellado derivable del estado vivo, no la persistencia en sí;ParquetStorees solo formato de export. - OpenAlex es el backbone de datos (ADR 0007): trae refs + citantes + afiliaciones per-autor.
BibTeX es
Sourcesecundaria. El enricher S2 ya no es estructural. - El CLI es la API para LLM/agentes (Hito 6). Subprocess + JSON stdout, exit codes claros, sin estado entre invocaciones (el estado vive en DuckDB).
- Los docs vivos describen el PRESENTE, no el camino.
docs/ARCHITECTURE.md,docs/API.mdydocs/PRD.mddescriben lo que el sistema ES. Cuando una decisión cuaja, el doc vivo se reescribe para reflejar el presente; el debate y el "porqué" viven en el ADR (docs/decisiones/, historia inmutable), no en el doc vivo. El changelog lo gestiona release-please — no se narra la evolución en los docs vivos. Sacá la dualidad descripción/realidad: nada de marcadoresAS-BUILT/TARGET/SUPERADO/HISTÓRICOni banners de "antes era X, ahora Y" en el cuerpo. - Regla de prosa. Preferí la idea en una línea y al punto. Sin banners redundantes, sin repetir lo que ya dice el código o un ADR (linkealo). Gastá palabras en el drift real, no en re-narrar lo construido. Si está bien, decilo corto.
- Sincronía tras un cambio de código: actualizá
docs/API.md/ARCHITECTURE.md/PRD.mdal nuevo presente y, si se tomó una decisión, redactá el ADR. El índice (README/AGENTS) tiene que seguir siendo verdad.
Modelo GitFlow-lite con dos ramas protegidas (PR + CI verde obligatorios; nunca
pushear directo). Detalle en CONTRIBUTING.md §Modelo de ramas.
dev— rama de integración y default del repo. Acá se acumula el trabajo. Protección no-estricta.main— rama estable / de release. Solo recibedeval liberar y el PR de release. Protección estricta (la rama del PR debe estar actualizada conmainantes de mergear).
Flujo de un cambio (agente o humano):
git checkout dev && git pull
git checkout -b feat/lo-que-sea # ramear SIEMPRE desde dev
# ...commits Conventional Commits...
git push -u origin feat/lo-que-sea
gh pr create --base dev # PR a dev (NO a main)
# CI verde (lint + test 3.11/3.12) → es el gate
gh pr merge --squash --delete-branch # 1 commit conventional limpio por idea
Dos tipos de PR, no confundir:
- PR de trabajo (
feat/...→dev): lo abrís vos/el agente a mano. Squash al mergear. - PR de release (
chore(main): release X.Y.Z): lo crearelease-pleasesolo; no se crea a mano. Ver §Comandos de release.
Liberar (cuando hay varias cosas en dev, no por cada cambio): PR dev → main con
merge commit (NO squash, para que release-please vea los feat/fix) → release-please
abre su PR de release → mergearlo crea el tag + GitHub Release.
Reglas para agentes: ramear desde dev; nunca commitear directo a dev/main; un PR =
una idea; el commit/PR sigue Conventional Commits (abajo); no bumpear versión ni editar
CHANGELOG.md a mano (lo hace release-please).
Milestones de GitHub = la versión que un issue va a liberar. Un milestone de GitHub representa
la versión que un issue va a cortar (p. ej. 0.10.0, 0.11.0). Al encuadrar un issue se lo
asigna a su milestone destino (la versión donde debe entrar). Un release = cerrar su milestone:
el milestone es el espejo de lo que falta para liberar esa versión. Si un issue no tiene milestone,
todavía no está encuadrado; si un milestone tiene issues abiertos, esa versión no está lista para
cortarse.
El repo versiona su propia config de Claude Code para que el equipo herede los roles y los
guardarraíles al clonar (project-level gana sobre la config de usuario). Se versiona
.claude/settings.json + .claude/agents/ + .claude/hooks/ + .claude/commands/; queda ignorado
el estado local (settings.local.json, worktrees/, System_prompt.md).
Comandos de proyecto (.claude/commands/*.md, slash commands del equipo): /retro-ciclo —
retrospectiva metacognitiva de fin de ciclo que mide dónde se fue el tiempo y baka las lecciones
en el proceso (ver §"Ejecución concurrente y testing").
Subagentes (.claude/agents/*.md), afinados a bib2graph y con una frontera dura por rol
("cada uno es responsable de sus artefactos"):
| Agente | Dueño de | Frontera (mecánica) |
|---|---|---|
architect |
docs/ (+ docs raíz) |
hook le niega escribir src//tests/ |
coder |
src/ + tests/ |
hook le niega escribir docs//README/AGENTS/CONTRIBUTING |
verifier |
nada (read-only) | sin Write/Edit en tools |
Los orquesta feature-cycle (PO → architect → coder → verifier → architect).
Hooks PreToolUse — hacen cumplir las reglas de forma mecánica (corren incluso en modo
bypass, son más fuertes que los permisos). Se invocan con uv run --no-sync --quiet python
(no python pelado: garantiza el intérprete vía uv y silencia el warning de deprecación):
hooks/guard.py(global, ensettings.json): bloqueanpm(usar pnpm),pip install(usar uv),git pushamain/dev,git commitestando parado enmain/dev, y editarCHANGELOG.mda mano. Son las reglas duras de §Flujo de trabajo, vueltas imposibles de violar.hooks/fence.py(por agente, en el frontmatter decoder/architect): aplica la frontera de la tabla de arriba según los directorios/archivos que recibe como argumento.
Caveat operativo: los agentes cargan al iniciar la sesión (un agente nuevo o un cambio a
su frontmatter no toma efecto hasta reiniciar). Los hooks de settings.json sí recargan en
caliente. Si un guardarraíl bloquea algo legítimo, se afloja editando el script en
.claude/hooks/.
Destiladas del giro de superficie 0.10.0 (medición forense: ~50% del tiempo de cada coder se
fue esperando el suite completo de tests). Las captura y actualiza el comando /retro-ciclo
(.claude/commands/) al cerrar cada ciclo.
- Testing por capas. El
coderitera con tests pertinentes (pytest test_X.py::test_Y, 7-60 s) y auto-formatea (ruff format+ruff check --fix) antes de gatear; el gate completo (pytestentero, ~6 min) lo corren elverifiery el CI, no el coder en loop. Elimina una de las 3 corridas redundantes del suite por sub-issue. - Paralelizar con prudencia (archivos disjuntos). Fan-out de varios sub-issues a la vez solo
si tocan archivos disjuntos. Ramas que comparten un archivo caliente (
build.py,cli/__init__.py) → serializar (mergear una, rebasar la siguiente) para no pagar el baile de conflictos. Batchear los encuadres y resolver las decisiones del PO en una sola ronda es ganancia neta sin riesgo. - Confiabilidad de worktrees. Los
Edit/Writede un subagente se aíslan al worktree de la sesión, no a la ruta que se le pase en el prompt. Para trabajo sobre una rama: tenerla checked out en el worktree de la sesión (o recuperar el trabajo víagit diff/patch). No asumir que el agente escribe en la ruta del prompt. - Windows: evitar rutas con acentos en Git Bash (rompe el quoting); preferir PowerShell para operaciones de filesystem. Reservar Bash para comandos POSIX simples.
El proyecto se gestiona con uv (entorno + lockfile + versión de Python). No uses
pip install ni edites [project.dependencies] a mano: uv mantiene pyproject.toml y
uv.lock sincronizados. Comandos canónicos (siempre uv run, sin activar el venv):
- Setup dev completo:
uv sync(crea.venv, instala núcleo + dev-deps desdeuv.lock) yuv run pre-commit install. - Con una capacidad opcional:
uv sync --extra bibtex(siembra BibTeX) — el único extra poblado hoy. Sin dev-deps:uv sync --no-dev. (No hay extra[gui]: se eliminó al retirar la GUI local —fastapi/uvicorn/b2g gui/API— ADR 0040, #190. Tampoco hay extra[llm]: se eliminó en la remediación R4 — el producto no usa IA generativa, ADR 0022. Tampoco hay extra[dedup]:rapidfuzzpasó al núcleo en #88 porque el dedup es automático en la ingesta, ADR 0031.) - Agregar dependencias:
uv add <pkg>(núcleo) ·uv add --dev <pkg>(desarrollo) ·uv add --optional <extra> <pkg>(capacidad opcional). - Tests (toda la suite):
uv run pytest - Un solo archivo:
uv run pytest tests/unit/test_corpus.py -x - Un solo test:
uv run pytest tests/unit/test_corpus.py::test_merge_idempotente -xvs - Por marcador:
uv run pytest -m unit/uv run pytest -m integration(los tests que toquen red o Neo4j se marcanintegrationy usan Testcontainers o mocks; el núcleo va enunit). El marcadornetworkes aparte: tests que pegan a la API real de OpenAlex (no mock) — fuera del gate por defecto (addopts -m "not network"); se corren explícitos conuv run pytest -m network. Losintegrationde DuckDB/store sí quedan en el gate. - Lint:
uv run ruff check .yuv run ruff format --check .(así lo corre el CI;exploracion/excluido) - Tipos:
uv run mypy src - Todo en uno (gate de CI):
uv run ruff check . && uv run ruff format --check . && uv run mypy src && uv run pytest
Regla de Hito 0: el tooling LOCAL —uv, linter (ruff), tipos (mypy), tests
(pytest), hooks (pre-commit) y commitizen (linter de Conventional Commits +
cz bump --dry-run para previsualizar el bump)— quedó configurado desde el día uno
(ADR 0006/0010). El mismo gate (ruff + mypy + pytest) corre en CI en cada push
a main/dev y en cada PR (.github/workflows/ci.yml). La automatización de releases
(release-please) YA está conectada (.github/workflows/release-please.yml); falta solo
la publicación a PyPI (ver §Comandos de release). La versión de Python la fija
.python-version (3.12; requires-python >=3.11).
release-please YA está conectado (.github/workflows/release-please.yml): vigila
main y, cuando llegan commits liberables (vía el merge dev → main), abre/actualiza
un PR chore(main): release X.Y.Z con el CHANGELOG.md + bump de pyproject.toml;
al mergearlo crea el tag vX.Y.Z y el GitHub Release. Pre-1.0: feat→minor,
fix→patch, breaking→minor. No publica a PyPI (decisión del PO: solo GitHub Releases
por ahora). commitizen no es el publicador: es (a) el linter de Conventional Commits
(hook de pre-commit) y (b) preview del bump con cz bump --dry-run.
- Hacer un commit conventional:
uv run cz commit(interactivo, recomendado). - Previsualizar qué versión saldría:
uv run cz bump --dry-run(solo preview, no publica). - No bumpear/taggear a mano: lo hace release-please al mergear su PR de release.
- Tags publicados en
origin:v0.1.0,v0.2.0,v0.3.0,v0.3.1(GitHub Releases). - Caveat: el PR de release no dispara CI (los commits del
GITHUB_TOKENno disparan workflows); se mergea con bypass de admin hasta que exista el secretRELEASE_PLEASE_TOKEN.
Detalle en CONTRIBUTING.md y VERSIONING.md.
- PEP 8 +
ruff format(ancho 88). Sin debates de estilo: el formatter decide. - Docstrings en español (la doc y los comentarios de los ADRs están en español; mantener
el idioma del proyecto). Una línea para funciones triviales, multilínea con secciones
Args:/Returns:/Raises:para lo demás. - Comentarios: escasos, actuales, solo el por qué no obvio. Un comentario se justifica solo
si da contexto o una decisión que el código no muestra, está vigente y es escaso (si hay
muchos, dejan de ser relevantes — para eso están los docs/ADRs). Los docstrings son el lugar
del qué; los comentarios, del por qué.
Cortar: los que parafrasean el qué (lo dice el código); los desactualizados (migraciones
o compat de datos "pre-X"/legacy que ya no existen, narraciones de código borrado); los banners
separadores (
# ----,# ====, enumeraciones# 1./# 2.— a lo sumo un título de sección de una línea, y solo en archivos largos); y las referencias ADR/issue peladas (# ADR 0024,# #126sin explicación). Conservar: el por qué que justifica un orden, un workaround o una decisión no evidente — incluida la referencia ADR/issue cuando viene con esa explicación (# ADR 0024: _seq es interna, no parte de CORPUS_SCHEMA). from __future__ import annotationsen todos los módulos del paquete.
- No hay efectos de import (lección 6 de v0). Importar un módulo nunca debe tocar config, red, disco ni estado global.
- Dependencias opcionales (extras) se importan de forma perezosa dentro de la función que las usa, con un mensaje de error claro que apunte al extra faltante.
- Orden: stdlib → third-party → local, separados por línea en blanco.
rufflo enforce.
- Tipado estático en todas las firmas públicas (
docs/API.md§Convenciones). El núcleo y las costuras sonProtocolo ABC; las implementaciones concretas los cumplen. - Modelos de datos serializables (
Manifest,NetworkSpec, configs) son Pydantic v2 (BaseModel), no dataclasses. Esto da validación, serialización JSON nativa y compatibilidad con el CLI/JSON-schema. - Para entidades internas efímeras (ej. dataclasses para vistas materializadas en tests),
usar
dataclass(frozen=True). No son parte del contrato público. - Para campos opcionales:
str | None, nuncaOptional[str](mypy + ruff lo prefieren). - Colecciones mutables en dataclasses:
field(default_factory=list)odict.
- snake_case para funciones, métodos, variables, módulos.
- PascalCase para clases (
Corpus,BibtexSource,CoCitationProjector). - UPPER_SNAKE solo para constantes reales (
MIN_WEIGHT_DEFAULT = 1). - Costuras terminan con su rol:
XxxSource,XxxEnricher,XxxStore,XxxProjector,XxxExporter,XxxPreprocessor. Esto las hace localizables con grep y respeta el vocabulario deldocs/API.md. - No nombrar cosas como v0 (
enriquecimiento.py,analisis/, scripts ad-hoc). El producto es genérico; los nombres deben reflejar el dominio, no el estudio que valida.
src/bib2graph/
__init__.py
constants.py # CAPA BASE (ADR 0023, Hito R1): Col/CurationStatus/NetworkKind (StrEnum),
# fuente única de literales. Todo lo demás depende de esta capa.
corpus.py # Corpus, Manifest, CorpusSnapshot (wrapper sobre tabla Arrow)
schemas.py # CAPA BASE (ADR 0023): PaperRow (Pydantic) ÚNICA fuente; CORPUS_SCHEMA (Arrow)
# derivado/verificado; ProvenanceEvent(BaseModel) consolidado acá (NO hay
# models.py separado), parseo que falla ruidoso
cycle.py # FSM CÍCLICO de dominio puro (ADR 0016 enmendado, Hito R3): SEEDED→…→
# MONITORED + reseed/ronda. Sale del backend; el backend solo lo persiste.
sources/ # OpenAlexSource (núcleo, backbone); BibtexSource (secundaria, cableada al CLI
# como seed --from-bib, 3er modo sin red —ADR 0030 Ciclo 10, #50);
# equation.py (EquationSpec + load_equation_spec, capa declarativa de la
# ecuación —seed --spec, 9a); RIS, CSV (futuro, no publicar)
backends/ # TabularBackend (Protocol) + InMemoryBackend (núcleo puro) +
# DuckDBBackend (biblioteca viva, carga perezosa de duckdb; persiste cycle).
# #54: tabla hermana referenced_but_not_fetched (IDs observados por el backward
# sin materializar) → add_referenced_refs/referenced_refs_count/referenced_refs,
# fuera del corpus_hash
foraging/ # Forager (chaining + ranking por scent BIBLIOMÉTRICO vía proyectores, Hito R4).
# SIN explain.py / explain_candidate / [llm] (eliminados, ADR 0022)
preprocessors/ # normalize + dedup fuzzy DETERMINISTA (rapidfuzz NÚCLEO, automáticos en la
# ingesta, ADR 0031) + thesaurus multilingüe DETERMINISTA explícito, sin
# fallback LLM
filters/ # filtros de inclusión/exclusión con conteo PRISMA (núcleo)
enrichers/ # OpenAlexEnricher opt-in, NÚCLEO (Hito 8 ✅: refs→DOI 8a + co-citación 8b → pobla cited_by_id);
# Enricher Protocol; S2 ([s2]) reservado para señal adicional, NO el Enricher (ADR 0025)
networks/ # Projector, Analyzer, NetworkSpec (resolution + extra="forbid"), load_specs (YAML, Hito 9),
# NetworkArtifact, Networks, cluster_table (#31)
exporters/ # GraphML, CSV
service/ # CAPA DE SERVICIOS NEUTRAL (ADR 0028, AS-BUILT G1+G2+G3 del MVP GUI): contrato
# compartido por CLI/API, agnóstico de transporte (sin print/sys.exit/Click/
# FastAPI). envelope.py = build_envelope + ENVELOPE_SCHEMA_VERSION; errors.py =
# jerarquía B2GError (+ Usage/Data/Dependency/Network/StoreError) + code_for
# (mapeo puro error→exit code 0–5). reads.py (G2 ✅) = lecturas read-only del corpus. SE CONSERVA tras
# retirar la GUI (ADR 0040): el grupo CLI read la usa (list_papers/corpus_stats/get_paper/
# get_top). Las ex-API-only (get_workspace/list_rounds/get_scent/get_network/compare_rounds)
# quedan inertes, poda opcional → #191. Sin red/mutación/transición; API.md §0.1.
# curate.py = orquestación de curación (fuente única CLI): accept_papers/
# reject_papers/curate_paper/filter_corpus (toma store_path; decided_at inyectado en la
# frontera); los verbos del CLI delegan. cli/ re-exporta el contrato (envelope·errores).
# api/ ⛔ RETIRADO (ADR 0040, #190): la API local FastAPI, la SPA frontend/ y el extra [gui] se
# eliminaron de la librería (GUI fuera del foco; el core es CLI/agente-native). La capa
# service/ que la alimentaba se conserva (la usa el CLI). Limpieza profunda: #191.
stores/ # DuckDBStore (núcleo, por defecto: biblioteca viva);
# ParquetStore (export); ZoteroStore ([zotero], V1.1);
# Neo4jStore ([neo4j], post-V1)
cli/ # paquete de 3 capas (Click → run_<cmd>() núcleo → envelope/errores);
# _ingest.py = helper normalize_and_dedup (auto-preproc en la ingesta, ADR 0031);
# cli/commands/ = superficie 0.10.0 (ADR 0037/0038/0039/0040): 10 verbos del ciclo + 3 grupos
# noun-verb (read/curate/snapshot) + 1 comando meta (skill add —ADR 0039) + 9 aliases deprecados
# (accept/reject/filter/inspect/monitor/networks/enrich/restore/resolve, retiro 0.11.0).
# chain --since absorbe monitor →MONITORED (#158); enrich absorbido en chain (refs→DOI)
# + build (co-citación) (#162); thesaurus retirado → build --thesaurus (#164);
# _deprecation.py emite avisos a stderr + warnings[] (#165). init scaffold —ADR 0029;
# build --spec absorbe networks —#159. El verbo gui SE RETIRÓ con la GUI local (ADR 0040, #190).
# CLI = API
# para LLM y agentes (Hito 6, ARCHITECTURE.md §6.3). No es un cli.py plano.
workspace.py # Workspace (init/open/resolve; snapshots_dir/exports_dir/networks_dir;
# read_networks_corpus_hash/is_networks_cache_stale —staleness #32) +
# WorkspaceManifest (ADR 0029): la carpeta es la unidad de persistencia;
# resolución ambiente; import perezoso de DuckDBStore
tests/
unit/ # tests puros, sin red ni I/O (default)
integration/ # red / APIs externas / Neo4j; @pytest.mark.integration
La estructura es orientativa (ADR 0006): un módulo plano (corpus.py) o un paquete
(sources/) es decisión del implementador según crezca. Lo fijo son los nombres del
dominio y los contratos de docs/API.md.
El subárbol JS frontend/ (SPA "Observatorio") y el prototipo app/ se retiraron de la librería
junto con la GUI local (ADR 0040,
#190; supersede 0027/0028). El repo vuelve a
ser 100% Python con uv, sin Node/pnpm. El wheel es Python puro (sin force-include del
frontend, sin job frontend en CI ni build de Node en los workflows de publish). El historial de la
SPA vive en git log y en docs/ROADMAP/05-gui.md (deprecado).
- Fallar fuerte, no en silencio (lección 7 de v0). Si falta una dependencia requerida
(p. ej.
python-louvainparadetect_communities(method="louvain")), lanzar un error explícito y temprano con un mensaje que diga qué instalar. Nunca degradar a otra estrategia en silencio. - Nada de
try/exceptque oculte incompatibilidades de contrato (lección 3 de v0). Si una función recibe una firma distinta, la llamada debe fallar ruidosamente, no enmascararse. - Acceso defensivo a campos de entrada (lección de v0 con
research-areas): usarentry.get("author")oentry.get("author", []), no acceso directo. En BibTeX conbibtexparser, los campos opcionales faltan seguido. - Idempotencia.
Corpus.mergey losEnricher.enrichdeben ser idempotentes: re-ejecutarlos sobre el mismo corpus no debe duplicar datos. - Exit codes del CLI (Hito 6):
0éxito,1error de uso,2error de datos,3dependencia faltante,4red no disponible,5store/snapshot corrupto. Sin estado entre invocaciones.
- Una sola fuente de configuración, construida explícitamente y pasada a quien la use. Ningún secreto embebido como literal (lección 1 de v0). API keys de S2, credenciales de Neo4j, etc., se inyectan por config / CLI / entorno; nunca un default secreto en código.
- Sin contraseñas por defecto. Si falta una credencial requerida, error claro.
- Sin
os.environ.get("X", "default_literal")para secretos. Para lo no-secreto, defaults explícitos y documentados.
- El
Corpusse documenta una sola vez (docs/API.md§1): el schema de columnas de la tabla Arrow + la API del wrapper + elManifest+ elCorpusSnapshot. Los docstrings del código deben coincidir con esa sección. Nada de columnas divergentes con campos inexistentes (lección 4 de v0:Institution.address,Paper(note=...),CITED_BY). - Las "entidades" (
Paper,Author,Keyword,Institution) no son tipos del modelo. Si el código define dataclasses con esos nombres, son vistas temporales para tests/debugging víaCorpus.materialize(...), no contrato público. - Relaciones derivadas (
CO_CITED_WITH,COLLABORATED_WITH,CO_OCCURS_WITH) no viven en el corpus: son salida de unProjector. Si aparecen como columna de la tabla, está mal. is_seeddistingue el corpus original (ecuación/semillas) del traído por el forrajeo/ chaining. El acoplamiento bibliográfico se proyecta sobre el corpus completo (no solo semillas; ciudadano de primera, crítica #2); la co-citación usascope="seeds_only"y requiere el 2º nivel de fetch (el más caro). Verdocs/API.md§7.
- Proyectores, analizadores y la lógica de deduplicación son funciones puras sobre
pa.Tableonx.Graph. Sin I/O, sin red, sin estado global, sin servidor. Esto es lo que permite tests rápidos y reproducibles (la victoria de v0 que faltaba en v0). - Los
Store,Source,EnricheryPreprocessorsí pueden tener I/O y red; ese es su trabajo. Pero las interfaces se inyectan, no se construyen dentro del núcleo. Networks.build(corpus, spec)yNetworks.quick(corpus)son funciones puras: mismo corpus + mismo spec → mismoNetworkArtifact.
- Cada subcomando expone
--json(por-comando, post-verbo) con salida estructurada (un objeto por corrida, estable y versionado). Alternativa por entorno:export B2G_JSON=1(truthy:1/true/yes) activa el modo JSON en todos los comandos sin repetir el flag; precedencia--json>B2G_JSON, sin--no-json(#151). - stdout puro en modo JSON: stdout = una línea-envelope
schema="1"(incl. el camino de error); el texto humano va a stderr. - Exit codes claros (ver §Manejo de errores).
- Sin estado entre invocaciones: cada llamada es independiente. El agente orquesta orquestando subprocess.
- Tool schemas JSON y/o servidor MCP son trabajo futuro (post-v0.3). El CLI ya alcanza como frontera programática.
- Las costuras futuras (
RisSource,CsvSource,CrossRefEnricher,ScopusEnricher, tool schemas JSON, MCP) no se mencionan en el README ni en__init__.pyhasta que existan (lección 5 de v0). Documentarlas endocs/API.mdcon estado "futuro — no implementado" es válido; importarlas o listarlas en extras sin código real, no. - Si un cliente de una API externa se inicializa, debe usarse. No cablear imports muertos.
TDD selectivo. En el núcleo, el test va antes del código. Pero no se testea cada cosa: se testea donde hay lógica, un contrato o riesgo de regresión; no wrappers finos, plumbing de Click, ni el cliente HTTP de terceros. La disciplina completa (qué SÍ / qué NO) y los tests concretos por hito están en
docs/ROADMAP/(§"Disciplina de tests" + cada hito).
- El núcleo se testea primero, sin red ni servidores (Hitos 1 y 2). Tests sobre
Corpus, proyectores y analizadores con datos sintéticos pequeños y resultados conocidos calculados a mano. - Tests para
Source:OpenAlexSourcecontra respuestas mockeadas (httpx.MockTransport), incluyendo el parser defensivo delabstract_inverted_index;BibtexSourcesobre.bibcon campos opcionales ausentes (regresión del bug T1 /KeyError). - Tests para
Forager: orden del ranking por information scent, preview/tope sin mutar el corpus. - Tests para
DuckDBStore: persistir → releer en instancia nueva (acumulación entre corridas), idempotencia depersist, procedencia/curación recuperables — DuckDB en proceso. - Tests para
Enrichercon respuestas de la API mockeadas. Sin red en CI. - Tests para
Neo4jStorecontra una Neo4j efímera (Testcontainers) o mockeando el driver. Marcados comointegration. - Tests para
CorpusSnapshot: sellar, recargar, compararcorpus_hashestable, detectarschema_versionincompatible. - Tests de contrato
--jsondel CLI (Hito 6): la forma de la salida no driftea; mapeo de errores a exit codes. - Cada test debe poder correr en aislamiento: nada de orden implícito, nada de fixtures que compartan estado mutable entre tests.
Mensajes en español, imperativo, formato Conventional Commits estricto:
<tipo>(<alcance>): <descripción corta en imperativo, español, sin punto final>
<cuerpo opcional: por qué, no qué>
<footer opcional: BREAKING CHANGE: ... o referencia a issue>
Tipos: feat (Added), fix (Fixed), refactor (Changed), perf (Changed),
docs (no release), test (no release), chore (no release), build (no
release), ci (no release), style (no release). Alcance sugerido:
corpus, sources, foraging, preprocessors, filters, enrichers,
networks, exporters, stores, cli. Detalle completo en
CONTRIBUTING.md.
- Cambios de código van con su test en el mismo commit/PR.
- Cambios a contratos públicos (
docs/API.md) se discuten en un ADR nuevo endocs/decisiones/antes de mergear. - Breaking changes:
BREAKING CHANGE:en el footer del commit. Bumpea MINOR (o MAJOR si estamos en1.x+). VerVERSIONING.md.
SemVer estricto (MAJOR.MINOR.PATCH). Mientras la mayor sea 0, la API
se considera inestable: cualquier cambio visible al usuario (no bugfix) bumpa
MINOR. El congelamiento en 1.0.0 requiere API pública estable, cobertura de
tests razonable y un caso real validado (el caso IED reproducido; ver PRD §10).
Detalle y tabla de ejemplos en VERSIONING.md.
Keep a Changelog. El CHANGELOG.md lo gestiona release-please (ya conectado): su
PR de release agrega la sección nueva desde los Conventional Commits que llegan a main. Las
secciones por debajo de [0.3.0] son el historial previo a la conexión (mantenido a mano); de
ahí en adelante las gestiona el bot. cz bump --dry-run sigue sirviendo como preview local.
Plantilla en docs/RELEASE_TEMPLATE.md.
- Empezar cualquier hito →
docs/ROADMAP/: historias (PRD §7), criterios de aceptación (DoD) y los tests TDD a escribir. - Tocar el modelo de datos →
docs/API.md§1,docs/ARCHITECTURE.md§3, ADR 0006. - Añadir una red nueva →
docs/ARCHITECTURE.md§3.2, tabla de proyectores endocs/API.md§7. - Sembrar / forrajear →
docs/API.md§2 (Source/OpenAlex) y §5 (Forager), ADR 0007, ADR 0008. - Persistencia / biblioteca viva →
docs/API.md§4, ADR 0009. - Normalización / thesaurus →
docs/API.md§6, ADR 0011. - Añadir una costura (
Source/Enricher/Store) →docs/API.md§2-4, ADR correspondiente,docs/Notas/01-lecciones-v0.md(reglas 1, 3, 5, 6, 7). - CLI agente-native →
docs/API.md§convenciones,docs/ARCHITECTURE.md§6.3, ADR 0010 (Hito 6). - Capa D /
NetworkSpec→docs/API.md§10, se libera en v0.3+ (Hito 9). - Decisiones de dependencias / extras →
docs/decisiones/0005-.... - Cambios al método bibliométrico (qué cuenta como co-citación, umbrales) →
docs/Notas/metodología.md.