0029 — Workspace por investigación: carpeta autocontenida + resolución ambiente¶
- Estado: Aceptada — AS-BUILT (2026-06-16) (firmado por el PO e implementado) ·
enmienda BREAKING #75 (2026-06-17):
--storeeliminado del CLI y fin del modo degenerado (ver §ENMIENDA al final) - Fecha: 2026-06-16
- Enmienda (de este ADR): 0009 y
0019 — la unidad de persistencia pasa de "1 archivo
.duckdb" a "1 workspace = 1 carpeta" (el single-writer sobre el.duckdbsigue válido); 0021 §E —--storedeja de ser opción global obligatoria y pasa a opcional vía resolución ambiente. - Relacionada con: 0016 (una investigación = una unidad de
persistencia; el
CycleState/loop-state vive en el.duckdb), 0017 (el snapshot sigue siendo lo reproducible; redes/exports = cache regenerable), 0010 (CLI agente-native). - Prerequisito de: epic GUI #34 (la GUI necesita una unidad de proyecto portable y abrible).
- Issues: #32 (modelo workspace),
#38 (
b2g init), #39 (resolución ambiente).
Contexto¶
Hoy la unidad de persistencia es un archivo .duckdb suelto: "una investigación = un archivo"
(ADR 0009 / 0019 /
0016). El estado del lazo, el corpus, la procedencia y las
decisiones de curación viven dentro de ese archivo. El CLI lo recibe con la opción global
obligatoria --store <archivo.duckdb> (ADR 0021 §E).
Ese modelo ya derivó de hecho hacia una carpeta: b2g build no escribe solo el .duckdb,
sino que materializa los artefactos de red en <store_dir>/networks/<kind>/network.graphml +
metrics.json (ADR 0021 §B). Es decir, alrededor del .duckdb ya nace una carpeta con
subproductos — una convención emergente sin nombre ni contrato. A esto se suman tres tensiones:
- Portabilidad / GUI. El epic GUI (#34)
necesita "abrir una investigación" como una unidad: db + redes + snapshots + exports juntos y
movibles. Un
.duckdbsuelto cuyos artefactos viven en un dir hermano implícito no es portable ni autodescriptivo. - Ergonomía del
--storeobligatorio. El error de uso más común es olvidar--store(ADR 0021 §E, Consecuencias): sale fuera del envelope (exit 1, stderr de Click). Repetir--store mi.duckdben cada invocación es fricción tanto para el humano como para el agente. - Onboarding. No hay un gesto de "empezar una investigación nueva": el
.duckdbse auto-crea al primerseed, sin estructura ni manifest que diga qué es.
Las sub-decisiones (qué marcador usa el workspace, qué flag, qué estructura de dirs, qué pasa con
los .duckdb existentes) se refinaron en la conversación de
#32 y derivados; este ADR las fija.
Decisión¶
Una investigación = un workspace = una carpeta autocontenida. Se formaliza la convención emergente: la carpeta es la unidad de persistencia, portabilidad y "proyecto" para la GUI.
mi-investigacion/
├── workspace.json # manifest mínimo (marcador del workspace)
├── library.duckdb # la biblioteca viva (corpus + procedencia + curación + loop-state)
├── networks/ # cache de redes (build) — regenerable
├── snapshots/ # snapshots sellados (parquet + manifest) — reproducible
└── exports/ # exports a formatos (graphml/csv) — regenerable
b2g init <name>scaffolds la carpeta con su estructura yworkspace.json.b2g init .inicializa el cwd como workspace.- Resolución ambiente (patrón git/cargo).
b2gcamina hacia arriba desde el cwd buscando unworkspace.json. Precedencia, de mayor a menor: --workspace/--storeexplícito en la invocación,- variable de entorno
B2G_WORKSPACE, - el workspace del cwd (subiendo hasta encontrar
workspace.json). - Sin migración forzada. Un
.duckdbsuelto sigue funcionando como workspace "degenerado":--store ruta.duckdbapunta al archivo y los artefactos caen en su dir hermano como hoy. No se exige convertir investigaciones existentes. b2g statusmuestra el workspace resuelto (de dónde salió). Si no hay ninguno (niworkspace.jsonhacia arriba, ni env, ni flag), se emite un error accionable que sugiereb2g init .o pasar--workspace.
Sub-decisiones resueltas¶
- Marcador =
workspace.json(manifest mínimo):{name, created_at, bib2graph_version, schema_version}. El corpus, la procedencia y el loop-state NO se duplican acá: ya viven enlibrary.duckdb(una pregunta = un doc; ADR 0009). El manifest es solo el marcador del límite del workspace (lo queb2gbusca al caminar hacia arriba) + metadatos de versión. - Flag =
--workspaceprimario,--storeopcional/degenerado.--workspace <carpeta>es la forma canónica;--store <archivo.duckdb>se conserva para apuntar a un.duckdbsuelto (modo degenerado, retrocompatible). Ambos son opcionales: sin ellos, gana la resolución ambiente. - Estructura de directorios estándar y fija.
networks/,snapshots/,exports/son nombres convencionales (no configurables en V1): un workspace es reconocible y la GUI sabe dónde mirar. - Redes/exports = cache regenerable, sellada por
corpus_hash. Las redes denetworks/y losexports/son derivables del estado vivo; se sellan con elcorpus_hashdel corpus que las produjo (ADR 0017). El artefacto reproducible sigue siendo el snapshot (parquet + manifest), no la cache. La staleness (cache cuyocorpus_hashya no coincide con el del corpus) se maneja avisando/regenerando, no con un grafo de dependencias: es una invalidación por hash, no un build system.
Consecuencias¶
- (+) Portabilidad: una investigación se mueve/comparte/respalda como una sola carpeta
autocontenida y autodescriptiva (
workspace.json). - (+) Prerequisito GUI (#34) cubierto: la GUI abre una carpeta-proyecto con db + redes + snapshots + exports en su sitio.
- (+) Onboarding:
b2g inites el gesto explícito de "empezar una investigación", con estructura y manifest desde el día cero (hoy el.duckdbnace implícito al primerseed). - (+) Ergonomía sin
--store: trabajar dentro de un workspace (o conB2G_WORKSPACE) elimina el--storerepetido y el error de uso más común (ADR 0021 §E); el patrón git/cargo es familiar. - (+) Formaliza una convención emergente:
b2g buildya escribe<store_dir>/networks/; el workspace le da nombre, límite y manifest en vez de un dir hermano implícito. - (+) Retrocompatible: sin migración forzada; el
.duckdbsuelto sigue siendo un workspace degenerado válido. - (−) Nueva capa
Workspace(resolución, scaffolding, lectura del manifest) a construir y testear; más superficie que un único archivo. - (−) Manejo de staleness por hash en la cache (
networks//exports/): hay que sellar concorpus_hash, comparar y avisar/regenerar. Se acota deliberadamente a invalidación por hash (no un grafo de dependencias) para no convertir el producto en un build system. - (−) Cambio suave del contrato CLI (ADR 0021 §E):
--storedeja de ser global obligatorio. Es aditivo/retrocompatible (la resolución ambiente solo cubre el caso en que falta el flag), pero toca el contrato público y exige sincronizardocs/API.md/ARCHITECTURE.mdcuando se implemente.
AS-BUILT (2026-06-16)¶
Implementado y verificado (gate verde, 416 tests). Lo construido en este corte:
src/bib2graph/workspace.py— claseWorkspace(factoriesinit/open/resolve),WorkspaceManifest({name, created_at, bib2graph_version, schema_version}) y las excepcionesWorkspaceNotFoundError/WorkspaceExistsError. El núcleo NO importaduckdb:DuckDBStorese importa de forma perezosa dentro deWorkspace.b2g init <name>(14º subcomando,cli/commands/init.py): scaffolds<name>/conworkspace.json+library.duckdb+networks//snapshots//exports/.b2g init .inicializa el cwd. Si la carpeta ya es un workspace → error (WorkspaceExistsError).--storeglobal pasó a OPCIONAL y se agregó--workspace(ambos opcionales). Resolución ambiente con precedencia:--workspace/--storeexplícito >B2G_WORKSPACE(env) > walk-up del cwd buscandoworkspace.json.--workspacey--storeson mutuamente excluyentes (pasarlos juntos = error de uso). Sin ninguno y sin workspace resoluble → error accionable que sugiereb2g init.- Retrocompat (workspace degenerado):
--store archivo.duckdbsuelto sigue válido; los artefactos caen en su dir hermano, como hoy. Sin migración forzada. b2g status: campo aditivoworkspace: {root, source}(de dónde se resolvió).schema="1"intacto.b2g build: escribe las redes en<workspace>/networks/y sellanetworks/.corpus_hash(cache regenerable; el snapshot sigue siendo lo reproducible).
Fuera de este corte (acotado deliberadamente):
snapshot/exportsiguen usando--out-direxplícito — no se redirigen automáticamente a<workspace>/snapshots//exports/.- Staleness = solo se sella el hash.
buildgrabanetworks/.corpus_hash, pero no hay aún aviso de cache obsoleta ni regeneración automática cuando elcorpus_hashdeja de coincidir; se implementará cuando aparezca la necesidad (la invalidación por hash sigue siendo el modelo, no un build system — ver Consecuencias).
ADDENDUM AS-BUILT (2026-06-17) — remanentes del corte cerrados (#32)¶
Implementado y verificado (gate verde, 534 tests). Los dos puntos que el corte del 2026-06-16 dejó explícitamente "fuera" (sección arriba) pasan a CONSTRUIDOS:
snapshot/exportredirigidos por ambiente.--out-dirpasó de obligatorio a override OPCIONAL en ambos comandos. Sin él,b2g snapshotescribe en<workspace>/snapshots/yb2g exporten<workspace>/exports/(resolución víaresolve_workspace, el mismo path quebuild).exportademás resuelve la fuente de redes conws.networks_dir(mismo path que antes, en ambos modos). En modo degenerado (--storesuelto) los artefactos caen en el dir hermano del.duckdb, sin regresión.- Aviso de staleness en
b2g statusporcorpus_hash.statussuma el campo aditivodata["networks_cache_stale"]: bool(schema="1"intacto) y unwarningsaccionable ("ejecutáb2g build") cuando elnetworks/.corpus_hashsellado por el últimobuildno coincide con elcorpus_hashdel corpus vivo. El hash vivo se computa con el mismocompute_corpus_hash(corpus.to_arrow())quebuildusa para sellar → sin falsos positivos. Si la cache no existe (nunca se corrióbuild), no es stale. Es un AVISO, NO regenera: se respeta la invalidación por hash decidida arriba — no un build-system, no un grafo de dependencias. El snapshot sigue siendo lo reproducible;networks/es cache regenerable.
Capa Workspace (lo construido para esto): se agregaron read_networks_corpus_hash() -> str |
None (lee networks/.corpus_hash; None si no existe) e is_networks_cache_stale(live_hash) -> bool
(True solo si el archivo existe Y difiere del hash vivo). Los accessors
snapshots_dir/exports_dir/networks_dir (_library_path.parent / "...", también en modo
degenerado) ya existían desde el corte anterior. Con esto, la fundación workspace queda
completa: no quedan remanentes del modelo en el backlog.
ENMIENDA (2026-06-17) — --store eliminado y fin del modo degenerado (BREAKING, #75)¶
- Estado: Aceptada — breaking change #75.
- Fecha: 2026-06-17.
Esta enmienda supera —sin reescribirlas, son historia AS-BUILT— las siguientes sub-decisiones y consecuencias de este ADR:
- La Decisión › "Sin migración forzada" (donde dice que un
.duckdbsuelto sigue funcionando como "workspace degenerado" vía--store). - La Sub-decisión "Flag =
--workspaceprimario,--storeopcional/degenerado" (donde--storese conservaba para apuntar a un.duckdbsuelto). - La Consecuencia "(+) Retrocompatible" (donde el
.duckdbsuelto seguía siendo un workspace degenerado válido) y los puntos del AS-BUILT (2026-06-16) / ADDENDUM (2026-06-17) que mencionan el modo degenerado (--store archivo.duckdb→ dir hermano). - La enmienda de 0021 §E que dejaba
--store"opcional".
Decisión:
--storese elimina por completo del CLI. Ya no está registrada como opción global en Click; pasarla produce el error estándar de Click (No such option: --store), no un mensaje custom de migración ni un rechazo "amable". No es un cambio retrocompatible: es BREAKING.- La única unidad canónica de persistencia es la carpeta con
workspace.json. El modo degenerado se elimina entero: un.duckdbsuelto sinworkspace.jsonya no se resuelve como persistencia. - Migración: un
.duckdblegacy se adopta ejecutandob2g init .en su carpeta (scaffoldsworkspace.json+ estructura junto al.duckdbexistente). - La resolución ambiente sobrevive intacta salvo que pierde la rama
--store: precedencia--workspaceexplícito >B2G_WORKSPACE(env) > walk-up del cwd buscandoworkspace.json> error accionable.
El resto del contrato (envelope schema="1", exit codes, estructura del workspace, cache por
corpus_hash, snapshot reproducible) no cambia.