--- name: preregister_hypothesis kind: function lang: py domain: datascience version: "1.0.0" purity: impure signature: "def preregister_hypothesis(paper_dir: str, hypotheses: dict, analysis_plan: dict) -> dict" description: "Pre-registra (congela) la hipotesis y el plan de analisis de un paper ANTES de mirar los datos: antidoto al HARKing (Hypothesizing After the Results are Known). Escribe/actualiza /preregistration.md con un frontmatter (paper_slug, frozen_at, content_hash, status) y un cuerpo markdown DETERMINISTA derivado de (hypotheses, analysis_plan) (mismo input -> mismo cuerpo byte a byte, claves ordenadas alfabeticamente). El content_hash es sha256 del cuerpo NORMALIZADO (strip por linea + colapso de blancos), nunca del frontmatter. Una vez status=frozen es INMUTABLE: re-congelar con el mismo contenido es idempotente (no reescribe, devuelve unchanged) y re-congelar con contenido distinto se RECHAZA (no sobrescribe, devuelve error) para que no se pueda ajustar la hipotesis a los resultados. Estilo dict-no-throw: nunca lanza." tags: [papers, preregistration, reproducibility, anti-harking, python] params: - name: paper_dir desc: "ruta del directorio del paper, p.ej. 'papers/0001-mi-paper'. Debe existir (no se crea aqui). El paper_slug del frontmatter es el basename del dir. Si no existe o no es str -> {status:error, path, note} sin crash ni creacion." - name: hypotheses desc: "dict de hipotesis, p.ej. {'h0': 'no hay diferencia ...', 'h1': 'el grupo A > grupo B ...'}. Se renderiza en la seccion '## Hypotheses' con una linea por clave, ordenadas alfabeticamente para determinismo." - name: analysis_plan desc: "dict con el plan de analisis, p.ej. {'test': 'welch_t_test', 'effect_size_metric': 'cohens_d', 'decision_rule': 'rechazar H0 si p<0.05 tras Holm y |d|>=0.5', 'planned_n': 100, 'multiple_correction': 'holm'}. Se renderiza en '## Analysis plan' con una linea por clave (ordenadas alfabeticamente). Acepta valores no-str (int, etc.)." output: "dict dict-no-throw (NUNCA lanza). status='frozen' cuando escribe el archivo por primera vez o congela un draft previo ({status, path, content_hash, frozen_at}). status='unchanged' cuando ya estaba frozen con el mismo content_hash: no reescribe y preserva el archivo byte-identico incl. el frozen_at original ({status, path, content_hash, frozen_at}). status='error' cuando paper_dir no existe, ya esta frozen con un hash distinto (rechazo anti-HARKing, no sobrescribe), inputs invalidos o error de I/O ({status, path, note, [content_hash]})." uses_functions: [] uses_types: [] returns: [] returns_optional: false error_type: "error_go_core" imports: [hashlib] tested: true tests: ["test_golden_congela_y_escribe_archivo", "test_idempotente_mismo_input_no_reescribe", "test_inmutabilidad_anti_harking_rechaza_contenido_distinto", "test_error_paper_dir_inexistente_no_crash_no_crea"] test_file_path: "python/functions/datascience/preregister_hypothesis_test.py" file_path: "python/functions/datascience/preregister_hypothesis.py" --- ## Ejemplo ```python import os, tempfile from datascience import preregister_hypothesis # Un directorio de paper que ya existe. paper_dir = tempfile.mkdtemp(prefix="0001-") hypotheses = { "h0": "no hay diferencia entre el grupo A y el grupo B", "h1": "el grupo A tiene mayor conversion que el grupo B", } analysis_plan = { "test": "welch_t_test", "effect_size_metric": "cohens_d", "decision_rule": "rechazar H0 si p<0.05 tras Holm y |d|>=0.5", "planned_n": 100, "multiple_correction": "holm", } # 1) Primera vez: congela y escribe /preregistration.md r1 = preregister_hypothesis(paper_dir, hypotheses, analysis_plan) print(r1["status"]) # -> "frozen" print(r1["content_hash"]) # sha256 del cuerpo # 2) Mismo input: idempotente, no reescribe. r2 = preregister_hypothesis(paper_dir, hypotheses, analysis_plan) print(r2["status"]) # -> "unchanged" # 3) Cambiar la hipotesis tras congelar (HARKing): rechazado, archivo intacto. r3 = preregister_hypothesis(paper_dir, {"h0": "...", "h1": "otra cosa"}, analysis_plan) print(r3["status"]) # -> "error" ``` ## Cuando usarla Llamala al ARRANCAR el analisis de un paper, antes de tocar los datos, para dejar por escrito (y firmado por hash) que vas a probar y como vas a decidir. Es el primer paso de un flujo reproducible: pre-registras la hipotesis y el plan (`test`, `effect_size_metric`, `decision_rule`, `planned_n`, `multiple_correction`), y solo despues corres el analisis y comparas con lo pre-registrado. Si mas tarde el analisis "descubre" otra hipotesis que encaja mejor con los datos, el pre-registro congelado deja en evidencia el cambio: no se puede reescribir. Combinala con `effect_size_cohens_d` y `fdr_correction` para cerrar el plan declarado (effect size + correccion de multiples comparaciones). ## Gotchas - **Inmutabilidad (el corazon)**: una vez `status: frozen`, el pre-registro NO se puede editar. Re-congelar con el MISMO contenido es idempotente (`unchanged`, no reescribe, preserva incluso el `frozen_at` original). Re-congelar con contenido DISTINTO devuelve `error` y deja el archivo intacto: asi se mata el HARKing. Para cambiar de verdad la hipotesis hay que borrar el archivo a mano y asumir explicitamente que ya no es un pre-registro valido. - **dict-no-throw**: la funcion NUNCA lanza. Cualquier error previsible (directorio inexistente, inputs no-dict, fallo de I/O, excepcion inesperada) se captura y se devuelve como `{"status": "error", "note": ...}`. Siempre incluye `path` (la ruta esperada del `preregistration.md`). - **El hash es SOLO del cuerpo, nunca del frontmatter**: el frontmatter contiene el propio `content_hash` y el `frozen_at` (timestamp), asi que incluirlos en el hash seria circular y romperia la idempotencia. El cuerpo se normaliza antes de hashear (strip por linea + colapso de lineas en blanco + strip final): cambios irrelevantes de whitespace no alteran el hash, pero cambios de contenido SI. - **Determinismo**: el cuerpo se genera con las claves de `hypotheses` y `analysis_plan` ordenadas alfabeticamente, de modo que el orden de insercion del dict no afecta al hash. Mismo `(hypotheses, analysis_plan)` -> mismo cuerpo y mismo hash, byte a byte. - **No crea el directorio del paper**: si `paper_dir` no existe, devuelve `error` sin crear nada (ni el dir ni el archivo).