---
name: upsert_sentinel_block
kind: function
lang: py
domain: core
version: "1.0.0"
purity: pure
signature: "def upsert_sentinel_block(text: str, block_id: str, content: str, marker: str = \"osintdb\") -> str"
description: "Inserta o reemplaza de forma idempotente un bloque gestionado delimitado por sentinels HTML-comment (<!-- {marker}:begin id={block_id} --> ... <!-- {marker}:end id={block_id} -->) dentro de un texto. Util para mantener secciones auto-generadas en notas Markdown de Obsidian sin pisar el resto del cuerpo."
tags: [markdown, sentinel, managed-block, obsidian]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: [re]
params:
  - name: text
    desc: "Texto sobre el que operar (tipicamente el body de una nota Markdown de Obsidian). Puede contener otros bloques gestionados con ids distintos, que no se tocan."
  - name: block_id
    desc: "Identificador unico del bloque dentro del texto. Se usa literal en la regex (se escapa con re.escape), asi que puede contener caracteres especiales."
  - name: content
    desc: "Contenido a colocar entre los sentinels. NO debe contener los propios sentinels o se corromperia el bloque."
  - name: marker
    desc: "Prefijo del namespace de los comentarios sentinel. Default 'osintdb'. Se usa literal en la regex (se escapa con re.escape)."
output: "El texto resultante con el bloque insertado al final (si no existia) o con su contenido reemplazado entre sentinels (si ya existia). Los sentinels siempre se conservan."
tested: true
tests:
  - "insercion nueva anade bloque al final"
  - "reemplazo existente sustituye solo el contenido"
  - "idempotencia dos aplicaciones mismo resultado"
  - "bloque corrupto solo inicio lanza valueerror"
  - "bloque corrupto solo fin lanza valueerror"
  - "multiples bloques ids distintos en mismo texto"
  - "marker personalizado"
test_file_path: "python/functions/core/upsert_sentinel_block_test.py"
file_path: "python/functions/core/upsert_sentinel_block.py"
---

## Ejemplo

```python
from core.upsert_sentinel_block import upsert_sentinel_block

nota = "# Dossier OSINT\n\nNotas manuales del investigador."

# Primera vez: inserta el bloque al final.
nota = upsert_sentinel_block(nota, "persons", "- Alice\n- Bob")

# Segunda vez con nuevo contenido: reemplaza solo lo que hay entre sentinels.
nota = upsert_sentinel_block(nota, "persons", "- Alice\n- Bob\n- Carol")

print(nota)
# # Dossier OSINT
#
# Notas manuales del investigador.
#
# <!-- osintdb:begin id=persons -->
# - Alice
# - Bob
# - Carol
# <!-- osintdb:end id=persons -->
```

## Cuando usarla

Cuando necesites mantener una seccion auto-generada dentro de un documento de texto (tipicamente una nota Markdown de Obsidian) sin sobreescribir lo que el usuario haya escrito a mano. Llamala cada vez que regeneres los datos: el primer uso inserta el bloque y los siguientes solo refrescan su contenido, dejando intacto el resto de la nota y otros bloques gestionados con ids distintos.

## Gotchas

- Si en `text` aparece solo uno de los dos sentinels del `block_id` (bloque corrupto, por ejemplo si alguien borro el sentinel de fin a mano) la funcion lanza `ValueError` con un mensaje que indica cuantos sentinels de inicio y de fin se encontraron. Lo mismo si aparece mas de un par de sentinels para el mismo `block_id` (bloque duplicado).
- `content` NO debe contener los propios sentinels (`<!-- {marker}:begin id={block_id} -->` / `<!-- {marker}:end id={block_id} -->`). Si los contiene, el siguiente upsert vera sentinels de sobra y fallara o reemplazara de forma incorrecta.
- Es funcion pura: no escribe en disco. Lee/devuelve strings; la persistencia de la nota (lectura del fichero, escritura del resultado) corre por cuenta del llamante.
- `block_id` y `marker` se escapan con `re.escape`, por lo que es seguro usar valores con caracteres especiales de regex. Aun asi, conviene mantenerlos en formato simple (alfanumerico, guiones) para que los comentarios HTML sean legibles en la nota.
