diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 34105fa0..a5500ea8 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -58,9 +58,14 @@ sqlite3 registry.db ".schema" **types** — columnas: `id, name, lang, domain, version, algebraic, definition, description, tags, uses_types, file_path, created_at, updated_at, examples, notes, documentation, code, content_hash, source_repo, source_license, source_file` - Enums: `algebraic`(product|sum) +**unit_tests** — columnas: `id, function_id, name, code, file_path, lang, created_at, updated_at` +- Extraidos automaticamente por `fn index` desde los archivos de test +- FK: `function_id` → `functions.id` + **FTS5 (columnas buscables):** - `functions_fts`: id, name, description, tags, signature, domain, example, notes, documentation, code - `types_fts`: id, name, description, tags, domain, examples, notes, documentation, code +- `unit_tests_fts`: id, name, code, function_id, lang --- diff --git a/cmd/fn/main.go b/cmd/fn/main.go index 63dc6346..79094ea5 100644 --- a/cmd/fn/main.go +++ b/cmd/fn/main.go @@ -123,7 +123,7 @@ func cmdIndex() { } } - fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis\n", result.Functions, result.Types, result.Apps, result.Analysis) + fmt.Printf("Indexed %d functions, %d types, %d apps, %d analysis, %d unit_tests\n", result.Functions, result.Types, result.Apps, result.Analysis, result.UnitTests) for _, e := range result.ValidationErrors { fmt.Fprintf(os.Stderr, " INVALID: %s\n", e) } diff --git a/dev/functions_to_implement/00_README.md b/dev/functions_to_implement/00_README.md new file mode 100644 index 00000000..049d1f14 --- /dev/null +++ b/dev/functions_to_implement/00_README.md @@ -0,0 +1,67 @@ +# Funciones a implementar desde OpenViking + +Fuente: `sources/OpenViking` (ByteDance) +Licencia principal: **AGPL-3.0** (NO permisiva) +Licencia bot/: **MIT** | Licencia examples/: **Apache 2.0** + +**Estrategia:** No copiar codigo. Reimplementar desde cero las funcionalidades +genericas y utiles, documentadas aqui como specs independientes. Las funciones +resultantes seran originales, sin dependencia alguna del codigo AGPL. + +## Funciones + +| # | Archivo | Dominio | Funciones | +|---|---------|---------|-----------| +| 01 | parse_markdown.md | core | extract_frontmatter, find_headings, smart_split_content, estimate_token_count, sanitize_for_path | +| 02 | parse_pdf_to_markdown.md | core | pdf_to_markdown, extract_pdf_bookmarks, detect_headings_by_font, format_table_to_markdown | +| 03 | parse_html_to_markdown.md | core | html_to_markdown, detect_url_type, fetch_and_parse_url, convert_github_to_raw_url | +| 04 | parse_docx_to_markdown.md | core | docx_to_markdown | +| 05 | parse_excel_to_markdown.md | core | excel_to_markdown | +| 06 | parse_epub_to_markdown.md | core | epub_to_markdown | +| 07 | directory_scanner.md | infra | scan_directory | +| 08 | circuit_breaker.md | core | CircuitBreaker (class) | +| 09 | retry_with_classify.md | core | classify_api_error, compute_backoff_delay, retry_sync, retry_async | +| 10 | hotness_score.md | datascience | hotness_score | +| 11 | time_utils.md | core | parse_iso_datetime, format_iso8601, format_simplified | +| 12 | safe_extract_zip.md | infra | safe_extract_zip, normalize_zip_filenames | +| 13 | envelope_encryption.md | cybersecurity | envelope_encrypt, envelope_decrypt | +| 14 | parse_code_ast.md | core | parse_code_ast (tree-sitter multi-language) | +| 15 | git_url_parser.md | core | parse_git_url, is_git_repo_url, validate_git_ssh_uri | +| 16 | media_strategy.md | core | calculate_media_strategy | +| 17 | parser_registry.md | core | ParserRegistry (patron extensible) | +| 18 | read_file_with_encoding.md | infra | read_file_with_encoding | + +## Tipos + +| # | Archivo | Dominio | Tipos | +|---|---------|---------|-------| +| 19 | type_parse_result.md | core | NodeType (sum), ResourceNode (product), ParseResult (product) | +| 20 | type_classified_file.md | infra | ClassifiedFile (product), DirectoryScanResult (product) | +| 21 | type_code_entity.md | core | CodeEntity (product) | +| 22 | type_message.md | core | TextPart, ContextPart, ToolPart, Part (sum), Message (product) | +| 23 | type_retrieval.md | core | ContextType (sum), TypedQuery, QueryPlan, MatchedContext, ScoreDistribution, QueryResult, FindResult | +| 24 | type_memory.md | core | FieldType (sum), MergeOp (sum), MemoryField, MemoryTypeSchema, MemoryData | +| 25 | type_context.md | core | ResourceContentType (sum), ContextLevel (sum), Context (product) | + +## Gaps identificados (specs propias) + +Funciones que faltan en el registry para completar capacidades de ingesta, +operaciones y automatizacion. Diseño original, sin fuente externa. + +### Funciones + +| # | Archivo | Dominio | Funciones | +|---|---------|---------|-----------| +| 26 | validation_schemas.md | core | validate_json_schema, validate_struct_fields, coerce_types | +| 27 | tabular_transforms.md | datascience/core | pivot, melt, join_by_key, aggregate_by_group | +| 28 | serialization_format.md | core | to_csv, from_csv, to_jsonl, from_jsonl, render_template, generate_html_report | +| 29 | http_client.md | infra | http_get_json, http_post_json, http_download_file | +| 30 | scheduling.md | core/infra | parse_cron_expr, next_cron_time, cron_ticker | +| 31 | cache_persistent.md | infra/core | cache_to_sqlite, cache_to_file, cache_decorator | +| 32 | diff_merge.md | datascience | diff_entities, diff_relations, detect_drift, merge_graphs | + +### Tipos + +| # | Archivo | Dominio | Tipos | +|---|---------|---------|-------| +| 30 | scheduling.md | core | CronSchedule (product) | diff --git a/dev/functions_to_implement/completed/01_parse_markdown.md b/dev/functions_to_implement/completed/01_parse_markdown.md new file mode 100644 index 00000000..805776d8 --- /dev/null +++ b/dev/functions_to_implement/completed/01_parse_markdown.md @@ -0,0 +1,80 @@ +# Parse Markdown — Suite de funciones + +Fuente conceptual: OpenViking `openviking/parse/parsers/markdown.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funciones a implementar + +### 1. extract_frontmatter + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `extract_frontmatter(content: str) -> tuple[str, dict | None]` +- **Descripcion:** Extrae YAML frontmatter (delimitado por `---`) del inicio de un string markdown. Retorna el contenido sin frontmatter y el dict parseado (o None si no hay). +- **Algoritmo:** + 1. Regex `^---\n(.*?)\n---\n` con DOTALL + 2. Si match, parsear cada linea `key: value` (o usar `yaml.safe_load`) + 3. Retornar (contenido_restante, dict_frontmatter) +- **Deps:** `re`, opcionalmente `yaml` +- **Tests:** contenido con frontmatter, sin frontmatter, frontmatter vacio, frontmatter con listas + +### 2. find_headings + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `find_headings(content: str) -> list[tuple[int, int, str, int]]` +- **Descripcion:** Encuentra todos los headings markdown (# a ######), excluyendo los que estan dentro de code blocks (``` ... ```), HTML comments () y bloques indentados. +- **Retorno:** Lista de `(start_pos, end_pos, title, level)` +- **Algoritmo:** + 1. Recopilar rangos excluidos: code blocks (triple backtick), HTML comments, bloques indentados (4 espacios/tab) + 2. Regex `^(#{1,6})\s+(.+)$` con MULTILINE + 3. Filtrar matches cuya posicion cae en un rango excluido + 4. Filtrar headings escapados (`\#`) +- **Deps:** `re` +- **Tests:** headings normales, headings dentro de code blocks (no deben detectarse), headings escapados, headings en HTML comments + +### 3. smart_split_content + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `smart_split_content(content: str, max_tokens: int = 1024, max_chars: int = 8000) -> list[str]` +- **Descripcion:** Divide contenido grande en partes respetando limites de tokens y caracteres. Divide por parrafos (doble newline). Si un parrafo individual excede el limite, lo corta por caracteres. +- **Algoritmo:** + 1. Split por `\n\n` (parrafos) + 2. Acumular parrafos mientras no excedan max_tokens/max_chars + 3. Si un parrafo individual excede, cortarlo en chunks de max_chars + 4. Retornar lista de partes +- **Deps:** `re` +- **Tests:** contenido corto (1 parte), contenido largo, parrafo gigante que requiere forzar corte + +### 4. estimate_token_count + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `estimate_token_count(content: str) -> int` +- **Descripcion:** Estimacion rapida de tokens sin tokenizer. CJK chars ~0.7 token/char, otros ~0.3 token/char. +- **Algoritmo:** + 1. Contar chars CJK con regex `[\u4e00-\u9fff\u3040-\u30ff\uac00-\ud7af]` + 2. Contar otros non-whitespace chars + 3. Retornar `int(cjk * 0.7 + otros * 0.3)` +- **Deps:** `re` +- **Tests:** texto solo latin, texto solo CJK, texto mixto, texto vacio + +### 5. sanitize_for_path + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `sanitize_for_path(text: str, max_length: int = 50) -> str` +- **Descripcion:** Convierte texto a nombre seguro para uso en paths. Remueve caracteres especiales, reemplaza espacios con `_`, trunca con hash suffix si excede max_length. +- **Algoritmo:** + 1. Regex: mantener solo `\w`, CJK ranges, espacios y guiones + 2. Reemplazar espacios por `_`, strip underscores + 3. Si vacio, retornar "section" + 4. Si excede max_length: truncar y anadir `_` + sha256[:8] +- **Deps:** `re`, `hashlib` +- **Tests:** texto normal, texto con caracteres especiales, texto muy largo, texto vacio, texto CJK diff --git a/dev/functions_to_implement/completed/02_parse_pdf_to_markdown.md b/dev/functions_to_implement/completed/02_parse_pdf_to_markdown.md new file mode 100644 index 00000000..5ae625a5 --- /dev/null +++ b/dev/functions_to_implement/completed/02_parse_pdf_to_markdown.md @@ -0,0 +1,74 @@ +# Parse PDF to Markdown + +Fuente conceptual: OpenViking `openviking/parse/parsers/pdf.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funciones a implementar + +### 1. pdf_to_markdown + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (I/O: lee archivo PDF) +- **Signature:** `pdf_to_markdown(pdf_path: str, heading_detection: str = "auto") -> tuple[str, dict]` +- **Descripcion:** Convierte un PDF a markdown. Extrae texto, tablas e inyecta headings detectados desde bookmarks o analisis de fuentes. Retorna (markdown_content, metadata_dict). +- **Algoritmo:** + 1. Abrir PDF con pdfplumber + 2. Detectar headings: + - `"bookmarks"`: extraer outlines/bookmarks del PDF + - `"font"`: analizar distribucion de font sizes para detectar headings + - `"auto"`: intentar bookmarks primero, fallback a font + 3. Agrupar headings por pagina + 4. Por cada pagina: inyectar headings como `# titulo`, extraer texto, extraer tablas como markdown + 5. Unir todo con `\n\n` +- **Deps:** `pdfplumber` +- **Error type:** Exception (archivo no encontrado, PDF corrupto) +- **Tests:** PDF con bookmarks, PDF sin bookmarks, PDF con tablas, PDF vacio + +### 2. extract_pdf_bookmarks + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (requiere objeto PDF abierto) +- **Signature:** `extract_pdf_bookmarks(pdf) -> list[dict]` +- **Descripcion:** Extrae la estructura de bookmarks/outlines de un PDF abierto con pdfplumber. Retorna lista de `{"level": int, "title": str, "page_num": int | None}`. +- **Algoritmo:** + 1. Acceder a `pdf.doc.get_outlines()` + 2. Construir mapping `objid -> page_number` desde pdf.pages + 3. Resolver cada destino de outline a su numero de pagina + 4. Limitar nivel a [1, 6] +- **Deps:** `pdfplumber` +- **Tests:** PDF con outlines, PDF sin outlines + +### 3. detect_headings_by_font + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (requiere objeto PDF abierto) +- **Signature:** `detect_headings_by_font(pdf, min_delta: float = 2.0, max_levels: int = 4) -> list[dict]` +- **Descripcion:** Detecta headings analizando la distribucion de font sizes. El font size mas comun es el body; sizes significativamente mayores son headings. +- **Algoritmo:** + 1. Samplear font sizes (cada 5ta pagina) → Counter + 2. Determinar body_size (most_common) + 3. Heading sizes: font sizes >= body_size + min_delta Y con frecuencia < 50% del body + 4. Ordenar heading sizes desc → asignar niveles 1,2,3... + 5. Recorrer todas las paginas, extraer texto de chars con heading size + 6. Deduplicar: filtrar titulos que aparecen en >30% de paginas (son headers/footers) +- **Deps:** `pdfplumber`, `collections.Counter` +- **Tests:** PDF con headings claros por font, PDF con fuente uniforme (sin headings detectados) + +### 4. format_table_markdown (reutilizable) + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `format_table_to_markdown(rows: list[list[str]], has_header: bool = True) -> str` +- **Descripcion:** Convierte una lista 2D de celdas a tabla markdown con alineacion de columnas. +- **Algoritmo:** + 1. Calcular max width por columna + 2. Formatear header row con `|` + 3. Anadir separador `| --- | --- |` + 4. Formatear data rows + 5. Escapar pipes en celdas +- **Deps:** ninguna +- **Tests:** tabla normal, tabla con celdas vacias, tabla con 1 fila, tabla vacia, celdas con pipes diff --git a/dev/functions_to_implement/completed/03_parse_html_to_markdown.md b/dev/functions_to_implement/completed/03_parse_html_to_markdown.md new file mode 100644 index 00000000..79e9a562 --- /dev/null +++ b/dev/functions_to_implement/completed/03_parse_html_to_markdown.md @@ -0,0 +1,66 @@ +# Parse HTML to Markdown + +Fuente conceptual: OpenViking `openviking/parse/parsers/html.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funciones a implementar + +### 1. html_to_markdown + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `html_to_markdown(html: str) -> str` +- **Descripcion:** Convierte HTML a markdown. Usa readabilipy para extraer contenido principal (filtra nav, ads, boilerplate), luego markdownify para convertir a markdown. +- **Algoritmo:** + 1. Preprocesar HTML: manejar contenido oculto (ej: WeChat js_content), lazy loading images (data-src → src) + 2. Extraer contenido principal con readabilipy (basado en Mozilla Readability) + 3. Convertir a markdown con markdownify (headings ATX, strip script/style) +- **Deps:** `readabilipy`, `markdownify`, `beautifulsoup4` +- **Tests:** HTML con nav/footer (debe filtrarse), HTML limpio, HTML con imagenes lazy-loaded + +### 2. detect_url_type + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (hace HTTP HEAD request) +- **Signature:** `detect_url_type(url: str, timeout: float = 10.0) -> tuple[str, dict]` +- **Descripcion:** Detecta el tipo de contenido de una URL. Retorna tipo ("webpage", "pdf", "markdown", "text", "code_repository") y metadata. +- **Algoritmo:** + 1. Verificar patrones de repos de codigo (github.com/org/repo, git@...) + 2. Verificar extension en URL (.pdf, .md, .txt, .html, .git) + 3. Si no se determino: HTTP HEAD request → leer Content-Type header + 4. Default: "webpage" +- **Deps:** `httpx`, `urllib.parse` +- **Error type:** Exception (timeout, network error) +- **Tests:** URL .pdf, URL github repo, URL webpage, URL con Content-Type custom + +### 3. fetch_and_parse_url + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (I/O: HTTP request) +- **Signature:** `fetch_and_parse_url(url: str, timeout: float = 30.0) -> str` +- **Descripcion:** Descarga una pagina web y la convierte a markdown. Combina fetch HTML + html_to_markdown. +- **Algoritmo:** + 1. Detectar tipo de URL con detect_url_type + 2. Si webpage: fetch HTML con httpx, convertir a markdown + 3. Si download link: descargar a archivo temporal, delegar al parser apropiado + 4. Si code repository: delegar a parser de codigo +- **Deps:** `httpx` +- **Error type:** Exception + +### 4. convert_github_to_raw_url + +- **Dominio:** core +- **Lang:** Python +- **Purity:** pure +- **Signature:** `convert_github_to_raw_url(url: str) -> str` +- **Descripcion:** Convierte una URL de blob de GitHub/GitLab a su URL raw. Ej: `github.com/org/repo/blob/main/file.py` → `raw.githubusercontent.com/org/repo/main/file.py` +- **Algoritmo:** + 1. Parsear URL + 2. Si GitHub y path contiene `/blob/`: remover `/blob/` y cambiar dominio a raw.githubusercontent.com + 3. Si GitLab y path contiene `/blob/`: reemplazar por `/raw/` + 4. Si no aplica, retornar URL sin cambiar +- **Deps:** `urllib.parse` +- **Tests:** URL GitHub blob, URL GitLab blob, URL que no es blob, URL no-GitHub diff --git a/dev/functions_to_implement/completed/04_parse_docx_to_markdown.md b/dev/functions_to_implement/completed/04_parse_docx_to_markdown.md new file mode 100644 index 00000000..e460f0e0 --- /dev/null +++ b/dev/functions_to_implement/completed/04_parse_docx_to_markdown.md @@ -0,0 +1,30 @@ +# Parse DOCX to Markdown + +Fuente conceptual: OpenViking `openviking/parse/parsers/word.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funcion a implementar + +### docx_to_markdown + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (I/O: lee archivo .docx) +- **Signature:** `docx_to_markdown(docx_path: str) -> str` +- **Descripcion:** Convierte un documento Word (.docx) a markdown preservando estructura, formato y tablas en su posicion original. +- **Algoritmo:** + 1. Abrir con python-docx + 2. Construir mapa de tablas: `{element_xml: Table}` para lookup O(1) + 3. Recorrer `doc.element.body` en orden (preserva posicion de tablas): + - Si es parrafo (`w:p`): + - Si estilo es Heading N: `{'#' * level} {text}` + - Si no: convertir runs con formato (bold→`**`, italic→`*`, underline→``) + - Si es tabla (`w:tbl`): convertir filas a markdown table con `format_table_to_markdown` + 4. Unir partes con `\n\n` +- **Deps:** `python-docx` +- **Error type:** Exception (archivo no encontrado, formato invalido) +- **Notas:** + - Las tablas deben aparecer en su posicion original, no al final + - Los heading levels se extraen del nombre del estilo ("Heading 1" → nivel 1) + - El formato inline (bold/italic/underline) se preserva +- **Tests:** docx con headings y parrafos, docx con tablas intercaladas, docx con formato bold/italic, docx vacio diff --git a/dev/functions_to_implement/completed/05_parse_excel_to_markdown.md b/dev/functions_to_implement/completed/05_parse_excel_to_markdown.md new file mode 100644 index 00000000..da0cdcd6 --- /dev/null +++ b/dev/functions_to_implement/completed/05_parse_excel_to_markdown.md @@ -0,0 +1,32 @@ +# Parse Excel to Markdown + +Fuente conceptual: OpenViking `openviking/parse/parsers/excel.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funcion a implementar + +### excel_to_markdown + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (I/O: lee archivo) +- **Signature:** `excel_to_markdown(path: str, max_rows_per_sheet: int = 1000) -> str` +- **Descripcion:** Convierte un archivo Excel (.xlsx, .xls, .xlsm) a markdown con cada sheet como seccion H2. +- **Algoritmo:** + 1. Si extension `.xls`: usar xlrd (legacy) + 2. Si `.xlsx`/`.xlsm`: usar openpyxl + 3. Para cada sheet: + - Header: `## Sheet: {name}` + - Metadata: `**Dimensions:** {rows} x {cols}` + - Truncar a max_rows_per_sheet + - Convertir filas a tabla markdown + - Si truncado: anadir nota de filas omitidas + 4. Manejo de tipos de celda para xlrd: + - EMPTY/BLANK → "" + - DATE → formato ISO (con hora si no es 00:00:00) + - BOOLEAN → "TRUE"/"FALSE" + - ERROR → codigo de error Excel (#NULL!, #DIV/0!, etc.) + - NUMBER → entero si es entero, float si tiene decimales +- **Deps:** `openpyxl` (xlsx), `xlrd` (xls legacy) +- **Error type:** Exception +- **Tests:** xlsx con multiples sheets, xls legacy con fechas, sheet vacio, sheet con formulas (data_only), sheet truncado diff --git a/dev/functions_to_implement/completed/06_parse_epub_to_markdown.md b/dev/functions_to_implement/completed/06_parse_epub_to_markdown.md new file mode 100644 index 00000000..0b1e913b --- /dev/null +++ b/dev/functions_to_implement/completed/06_parse_epub_to_markdown.md @@ -0,0 +1,36 @@ +# Parse EPUB to Markdown + +Fuente conceptual: OpenViking `openviking/parse/parsers/epub.py` (AGPL-3.0) +Reimplementar desde cero. No copiar codigo. + +## Funcion a implementar + +### epub_to_markdown + +- **Dominio:** core +- **Lang:** Python +- **Purity:** impure (I/O: lee archivo) +- **Signature:** `epub_to_markdown(epub_path: str) -> str` +- **Descripcion:** Convierte un ebook EPUB a markdown. Intenta ebooklib primero, fallback a extraccion manual con zipfile. +- **Algoritmo con ebooklib:** + 1. Leer con `epub.read_epub(path)` + 2. Extraer metadata: titulo, autor + 3. Generar header: `# {titulo}` + `**Author:** {autor}` + 4. Para cada ITEM_DOCUMENT: decodificar contenido HTML → convertir a markdown + 5. Unir con `\n\n` +- **Algoritmo fallback manual (sin ebooklib):** + 1. Abrir como ZIP + 2. Listar archivos .html/.xhtml/.htm + 3. Para cada uno: decodificar HTML → convertir a markdown basico +- **Conversion HTML basica a markdown:** + 1. Remover `