# kanban — Parser/writer de issues y flows del registry Cluster de funciones para leer, escribir y vigilar los archivos `dev/issues/*.md` y `dev/flows/*.md`. Base del backend de `kanban_cpp v2` (issue 0130b) y de cualquier herramienta que opere sobre el board de desarrollo. ## Funciones | ID | Firma corta | Que hace | |---|---|---| | `parse_issue_md_go_infra` | `(path) → (Issue, []byte, error)` | Lee un .md de issue, extrae frontmatter YAML + body | | `write_issue_md_go_infra` | `(path, Issue, body) → error` | Serializa Issue a YAML y reescribe el .md preservando body | | `scan_issues_dir_go_infra` | `(root) → ([]Issue, error)` | Escanea dev/issues/ + completed/, devuelve todos los Issues ordenados | | `scan_flows_dir_go_infra` | `(root) → ([]Flow, error)` | Escanea dev/flows/, devuelve todos los Flows ordenados | | `watch_dir_fsnotify_go_infra` | `(ctx, root) → (<-chan FsEvent, error)` | Watcher recursivo con debounce 200ms, emite FsEvent por cambio | ## Tipos | ID | Que es | |---|---| | `issue_go_infra` | Frontmatter de dev/issues/*.md: id, title, status, domain, priority, depends, blocks… | | `flow_go_infra` | Frontmatter de dev/flows/*.md: id, name/title, status, kind, tags | | `fs_event_go_infra` | Evento de watcher: {Path, Op} donde Op ∈ {create, write, remove, rename} | ## Ejemplo canónico — arrancar el backend de kanban_cpp ```go import "fn-registry/functions/infra" const ( issuesDir = "/home/lucas/fn_registry/dev/issues" flowsDir = "/home/lucas/fn_registry/dev/flows" ) // 1. Carga inicial issues, _ := infra.ScanIssuesDir(issuesDir) flows, _ := infra.ScanFlowsDir(flowsDir) fmt.Printf("%d issues, %d flows cargados\n", len(issues), len(flows)) // 2. Actualizar status in-place iss, body, _ := infra.ParseIssueMd(issuesDir + "/0130-kanban-cpp-v2.md") iss.Status = "in-progress" iss.Updated = "2026-05-22" infra.WriteIssueMd(iss.FilePath, iss, body) // 3. Vigilar cambios externos (editor de texto, otro agente) ctx, cancel := context.WithCancel(context.Background()) defer cancel() ch, _ := infra.WatchDirFsnotify(ctx, issuesDir) for ev := range ch { if strings.HasSuffix(ev.Path, ".md") { updated, _, _ := infra.ParseIssueMd(ev.Path) cache.Upsert(updated) // invalidar cache SQLite } } ``` ## Fronteras - NO incluye markdown rendering del body (eso lo hace el frontend). - NO valida campos contra TAXONOMY (existe `fn doctor issues`). - NO crea ni borra archivos de issue (solo lee/escribe los existentes). - NO incluye endpoints HTTP ni SSE (eso es el backend de la app, issue 0130b). ## Notas - `parse_issue_md` + `write_issue_md` son el par CRUD atómico. Siempre usarlos juntos. - `scan_issues_dir` llama a `parse_issue_md` internamente — no reimplementar el walk. - `watch_dir_fsnotify` emite eventos para cualquier archivo, no solo `.md`. Filtrar por extensión en el consumidor. - El watcher y el writer pueden producir loops: el writer dispara un evento `write` que el watcher emite. El backend debe ignorar eventos generados por sus propios writes (comparar path + timestamp).