tawasuyu

tawasuyu/brahman

Rust 233TOML 72JSON 14HTML 12953 Sutichaykuna

gioser-web: add docs/ dir with frontmatter for Qdrant indexing

- 4 md files (aire, fuego, tierra, agua) with YAML frontmatter
- caminos mapped: logos, nomos, kay, uku
- original md/ unchanged
- add scripts/index-gioser-docs.py (adapted from gioserv)

b17149c5283785a72e6e87858ab0178278c3d078 · ruwaq Sergio · (5 killa ñawpaq) · tayta 12e3b1d4d · kay commitpi sach'ata qhaway

Semántico sayk'uy · 7

scripts/index-gioser-docs.py

5 qillqakuna · +340 −0

diff --git a/crates/apps/gioser-web/docs/agua.md b/crates/apps/gioser-web/docs/agua.md
new file mode 100644
index 0000000..c7ce68e
--- /dev/null
+++ b/crates/apps/gioser-web/docs/agua.md
@@ -0,0 +1,41 @@
+---
+title: Mística · Espiritualidad
+camino: uku
+tags: [mística, espiritualidad, meditación, contemplación]
+---
+
+> *La práctica como puente. El misterio como interlocutor.*
+
+Acá vive lo místico, lo espiritual, las prácticas que sostienen la
+atención. No es decoración: es la otra mitad del trabajo. Sin esto,
+el resto se vuelve ruido.
+
+## Prácticas
+
+Lo que sostiene día a día:
+
+- **Meditación.** Sentarse a observar lo que sucede, sin agarrarlo.
+- **Lectura contemplativa.** Textos que se vuelven a leer hasta que
+  cambian.
+- **Ceremonia.** Marcar inicios y cierres con gestos que pesan.
+- **Naturaleza.** Estar en lugares donde uno no es el centro.
+- **Silencio.** Día completo, una vez por mes mínimo.
+
+## Por qué mística
+
+Porque la racionalidad sola no alcanza para vivir. Y porque las
+tradiciones llevan miles de años elaborando vocabulario para lo que
+nos pasa cuando atendemos en serio: contemplación, ego, símbolo,
+muerte, asombro.
+
+**Mística aplicada** = no quedarse en el libro. Pasar por el cuerpo,
+por la relación, por la vida cotidiana.
+
+## Lo que leo
+
+Andino, budista, cristiano-contemplativo, hindú, sufí. Sin
+exclusividad: cada tradición resuelve algunas cosas mejor que otras.
+
+## Próximamente
+
+*Acá se va a ir armando una bitácora de lecturas, prácticas y notas.*
diff --git a/crates/apps/gioser-web/docs/aire.md b/crates/apps/gioser-web/docs/aire.md
new file mode 100644
index 0000000..6ed6de1
--- /dev/null
+++ b/crates/apps/gioser-web/docs/aire.md
@@ -0,0 +1,37 @@
+---
+title: Software · Tecnología
+camino: logos
+tags: [software, tecnología, open-source, rust, python]
+---
+
+> *Lo público. Lo que se mantiene abierto.*
+
+Acá viven los proyectos de **software libre**, herramientas, librerías
+y exploraciones técnicas que voy publicando. La premisa es mantener
+el código abierto, documentado y útil más allá del autor.
+
+## Qué vas a encontrar acá
+
+- Repos públicos de cosas que escribo (Rust, Python, embedded, web).
+- Notas técnicas sobre arquitectura, sistemas distribuidos, runtimes.
+- Ensayos sobre **IA aplicada** — sin hype, con ejemplos concretos.
+- Bitácoras de exploración: lo que probé, lo que descarté, lo que sigo
+  usando.
+
+## Por qué open source
+
+Porque el conocimiento técnico se multiplica cuando circula. Y porque
+mucho de lo que uso a diario me lo regaló alguien que decidió compartir.
+La reciprocidad importa.
+
+## Stack actual
+
+- **Rust** para lo que necesita ser rápido, seguro y portable.
+- **Python** para análisis, ML y prototipos rápidos.
+- **Linux** (Artix/Arch) como sistema operativo de trabajo.
+- **gitea** + **nix** para infraestructura personal.
+
+## Próximamente
+
+*Voy a ir enlazando proyectos específicos acá: tools, runtimes,
+experimentos. Por ahora, este placeholder vive en `docs/aire.md`.*
diff --git a/crates/apps/gioser-web/docs/fuego.md b/crates/apps/gioser-web/docs/fuego.md
new file mode 100644
index 0000000..cda7c3e
--- /dev/null
+++ b/crates/apps/gioser-web/docs/fuego.md
@@ -0,0 +1,36 @@
+---
+title: Quién Soy · Bitácora
+camino: nomos
+tags: [identidad, bitácora, crónica, personal]
+---
+
+> *La identidad como verbo. La crónica como práctica.*
+
+Acá vive lo personal: quién soy, qué hago, qué leo, qué pienso. Una
+bitácora honesta, no curada para impresionar. Si vas a leer esto,
+asumí que es borrador.
+
+## Quién soy
+
+**Sergio**. Programador, lector, padre, alguien que practica
+mantenerse despierto. Vivo entre código, café, montañas y libros.
+
+Las cosas que más me importan no son las que mejor cuento todavía.
+Por eso escribo: para precisar lo que sé y lo que no.
+
+## Bitácora
+
+Notas más o menos diarias sobre lo que voy pensando, viviendo,
+fallando. Sin algoritmo de engagement, sin métricas. Sólo crónica.
+
+Las entradas se ordenan por fecha. Las más viejas a veces dicen cosas
+que ya no pienso así — las dejo igual.
+
+## Por qué publicarlo
+
+Porque escribir en público obliga a precisar. Y porque a veces lo que
+uno escribe para sí mismo le sirve a otra persona que no conoce.
+
+## Próximamente
+
+*Acá se va a ir armando una bitácora con entradas fechadas.*
diff --git a/crates/apps/gioser-web/docs/tierra.md b/crates/apps/gioser-web/docs/tierra.md
new file mode 100644
index 0000000..42f9a39
--- /dev/null
+++ b/crates/apps/gioser-web/docs/tierra.md
@@ -0,0 +1,37 @@
+---
+title: Manifiesto · Invariantes
+camino: kay
+tags: [manifiesto, principios, invariantes, ética]
+---
+
+> *Lo que no cambia. La piedra de toque.*
+
+Acá vive el manifiesto de GioSer: las **invariantes** que sostienen
+todo lo demás. Lo que no negocio, lo que define la forma del trabajo
+antes que cualquier proyecto particular.
+
+## Invariantes
+
+Cosas que considero **no-negociables** en cómo hago el trabajo:
+
+- **Código abierto por defecto.** Si tiene sentido, se publica.
+- **Honestidad por encima de marketing.** No prometo lo que no puedo
+  cumplir, ni vendo lo que no probé.
+- **El cuerpo es infraestructura.** Cuidarlo es parte del trabajo, no
+  opuesto al trabajo. Sin cuerpo no hay nada.
+- **Las ideas se prueban escribiéndolas.** Si no hay documento, todavía
+  no existe la idea.
+- **Compatibilidad hacia abajo > novedad arriba.** Las invariantes
+  duran, las modas no.
+- **Una sola voz.** Lo que digo en privado coincide con lo que publico.
+
+## Por qué un manifiesto
+
+Porque sin invariantes, cada decisión es ad hoc. Tener un set chico de
+principios reduce la energía gastada en cada elección — y deja en
+claro cuándo estoy contradiciéndome.
+
+## Revisión
+
+Este manifiesto se revisa una vez al año, no antes. Si una invariante
+deja de aplicarse, se quita con una explicación pública.
diff --git a/scripts/index-gioser-docs.py b/scripts/index-gioser-docs.py
new file mode 100644
index 0000000..a59e10a
--- /dev/null
+++ b/scripts/index-gioser-docs.py
@@ -0,0 +1,189 @@
+#!/usr/bin/env python3
+"""Indexador de docs/ de gioser-web → Qdrant.
+
+Recorre crates/apps/gioser-web/docs/, parsea YAML frontmatter,
+trocea cada documento en fragmentos de párrafo, pide embeddings al
+servicio agnóstico y hace upsert a Qdrant.
+
+Uso:
+    python scripts/index-gioser-docs.py                          # usa defaults
+    python scripts/index-gioser-docs.py --rebuild                # recrea colección
+    python scripts/index-gioser-docs.py --docs ./docs --rebuild  # docs custom
+"""
+
+from __future__ import annotations
+
+import argparse
+import hashlib
+import os
+import re
+import sys
+import uuid
+from dataclasses import dataclass
+from pathlib import Path
+
+import httpx
+import yaml
+from qdrant_client import QdrantClient
+from qdrant_client.http import models as qm
+
+
+DEFAULT_DOCS = Path(__file__).resolve().parent.parent / "crates/apps/gioser-web/docs"
+DEFAULT_QDRANT_URL = os.getenv("QDRANT_URL", "http://localhost:6333")
+DEFAULT_EMBED_URL = os.getenv("EMBEDDINGS_URL", "http://localhost:8001")
+DEFAULT_COLLECTION = os.getenv("QDRANT_COLLECTION", "gioser")
+
+VALID_CAMINOS = {"logos", "uku", "kay", "nomos", "aire", "fuego", "tierra", "agua"}
+FRONTMATTER_RE = re.compile(r"^---\s*\n(.*?\n)---\s*\n(.*)$", re.DOTALL)
+
+
+@dataclass
+class Chunk:
+    doc_id: str
+    chunk_index: int
+    text: str
+    title: str
+    camino: str
+    tags: list[str]
+
+
+def parse_md(path: Path) -> tuple[dict, str]:
+    raw = path.read_text(encoding="utf-8")
+    m = FRONTMATTER_RE.match(raw)
+    if m:
+        meta = yaml.safe_load(m.group(1)) or {}
+        body = m.group(2)
+    else:
+        meta = {}
+        body = raw
+    return meta, body
+
+
+def chunk_body(body: str, min_chars: int = 200, max_chars: int = 900) -> list[str]:
+    """Fragmenta por párrafos respetando un mínimo y un máximo."""
+    paragraphs = [p.strip() for p in re.split(r"\n\s*\n", body) if p.strip()]
+    chunks: list[str] = []
+    buf = ""
+    for p in paragraphs:
+        candidate = f"{buf}\n\n{p}".strip() if buf else p
+        if len(candidate) >= max_chars:
+            if buf:
+                chunks.append(buf)
+            buf = p
+        else:
+            buf = candidate
+            if len(buf) >= min_chars:
+                chunks.append(buf)
+                buf = ""
+    if buf:
+        if chunks and len(buf) < min_chars:
+            chunks[-1] = f"{chunks[-1]}\n\n{buf}"
+        else:
+            chunks.append(buf)
+    return chunks
+
+
+def discover_chunks(docs_dir: Path) -> list[Chunk]:
+    out: list[Chunk] = []
+    for path in sorted(docs_dir.rglob("*.md")):
+        meta, body = parse_md(path)
+        camino = (meta.get("camino") or path.stem).lower()
+        if camino not in VALID_CAMINOS:
+            print(f"  ⚠ saltando {path}: camino '{camino}' inválido", file=sys.stderr)
+            continue
+        title = meta.get("title") or path.stem.replace("-", " ").title()
+        tags = list(meta.get("tags") or [])
+        doc_id = meta.get("id") or hashlib.sha1(str(path).encode()).hexdigest()[:12]
+        for i, chunk in enumerate(chunk_body(body)):
+            out.append(
+                Chunk(
+                    doc_id=doc_id,
+                    chunk_index=i,
+                    text=chunk,
+                    title=title if i == 0 else f"{title} · §{i + 1}",
+                    camino=camino,
+                    tags=tags,
+                )
+            )
+    return out
+
+
+def embed_batches(http: httpx.Client, embed_url: str, texts: list[str], batch: int = 32) -> list[list[float]]:
+    out: list[list[float]] = []
+    for i in range(0, len(texts), batch):
+        chunk = texts[i : i + batch]
+        r = http.post(
+            f"{embed_url}/embed",
+            json={"texts": chunk, "kind": "passage", "normalize": True},
+            timeout=120.0,
+        )
+        r.raise_for_status()
+        out.extend(r.json()["vectors"])
+    return out
+
+
+def ensure_collection(qdrant: QdrantClient, name: str, dim: int, rebuild: bool):
+    existing = {c.name for c in qdrant.get_collections().collections}
+    if name in existing and rebuild:
+        qdrant.delete_collection(name)
+        existing.discard(name)
+    if name not in existing:
+        qdrant.create_collection(
+            collection_name=name,
+            vectors_config=qm.VectorParams(size=dim, distance=qm.Distance.COSINE),
+        )
+
+
+def main():
+    ap = argparse.ArgumentParser(description="Indexa docs/ de gioser-web en Qdrant")
+    ap.add_argument("--docs", default=str(DEFAULT_DOCS))
+    ap.add_argument("--qdrant", default=DEFAULT_QDRANT_URL)
+    ap.add_argument("--embed", default=DEFAULT_EMBED_URL)
+    ap.add_argument("--collection", default=DEFAULT_COLLECTION)
+    ap.add_argument("--rebuild", action="store_true", help="borra y recrea la colección")
+    args = ap.parse_args()
+
+    docs_dir = Path(args.docs)
+    if not docs_dir.is_dir():
+        sys.exit(f"docs no existe: {docs_dir}")
+
+    chunks = discover_chunks(docs_dir)
+    if not chunks:
+        sys.exit("no se encontraron docs para indexar")
+    print(f"→ {len(chunks)} fragmentos descubiertos")
+
+    with httpx.Client() as http:
+        health = http.get(f"{args.embed}/health", timeout=10.0).json()
+        dim = int(health["dim"])
+        print(f"→ embeddings: {health['model']} (dim={dim})")
+
+        qdrant = QdrantClient(url=args.qdrant)
+        ensure_collection(qdrant, args.collection, dim, rebuild=args.rebuild)
+
+        vectors = embed_batches(http, args.embed, [c.text for c in chunks])
+
+    points = [
+        qm.PointStruct(
+            id=str(uuid.uuid5(uuid.NAMESPACE_URL, f"{c.doc_id}:{c.chunk_index}")),
+            vector=v,
+            payload={
+                "doc_id": c.doc_id,
+                "chunk_index": c.chunk_index,
+                "title": c.title,
+                "text": c.text,
+                "camino": c.camino,
+                "tags": c.tags,
+                "source": "gioser-web",
+            },
+        )
+        for c, v in zip(chunks, vectors)
+    ]
+    qdrant.upsert(collection_name=args.collection, points=points)
+    print(f"✓ {len(points)} puntos en colección '{args.collection}'")
+
+    if args.rebuild:
+        print("  ✔ colección recreada")
+
+
+if __name__ == "__main__":
+    main()