feat: agregar flujo de Recomendaciones (Defensoría del Pueblo CABA) - #93
Draft
jansaldo wants to merge 30 commits into
Draft
feat: agregar flujo de Recomendaciones (Defensoría del Pueblo CABA)#93jansaldo wants to merge 30 commits into
jansaldo wants to merge 30 commits into
Conversation
pnpm-workspace.yaml still used the v10-era onlyBuiltDependencies-shaped allowBuilds entry, which broke `pnpm install` on pnpm 11 (the git-hosted @aymurai/ui build script was rejected with ERR_PNPM_GIT_DEP_PREPARE_NOT_ALLOWED). Updates the allowlist entry to the commit hash pnpm 11 actually resolves, drops the now-ignored package.json "pnpm" field, and pins packageManager so every machine installs with the same pnpm version. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Also carries the routeFileIgnorePattern fix so colocated .test.tsx files under routes/ are not treated as routes by the TanStack Router plugin. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- locate-value sketch skipped exact matches in later paragraphs, which its own test (3 matches across 2 paragraphs) would have caught. Restructured as two explicit passes. - support highlights for contenido_para_publicar moved to a separate function so they cannot break Etapa 6's assertion that buildExtractedAnnotations never emits that field. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds extractRecomendacion, loadRecomendacion, saveRecomendacion HTTP client functions and the recomendacionValidationMutation React Query factory. loadRecomendacion re-throws CanceledError but degrades to null on any other error (404/405/501/network/malformed body), mirroring getStoredValidation's fallback contract, since the persistence endpoint does not exist on the backend yet. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds locateValue/normalizeForMatch: a pure locator that finds where an LLM-extracted field value occurs in the original paragraph text, so a later task can highlight it. Two separate passes over all paragraphs (exact first, fuzzy only if zero exact matches anywhere) and an index map from normalized offsets back to original ones, since normalization changes string length. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…, overlapping exact matches
Review reproduced 2 critical and 2 important defects in the locator:
fuzzy windows were reported at their raw step-sampled offset instead of
being refined to the true match boundary (mis-highlighting text by up
to step-1 chars per edge), an empty needle with minLength:0 hung the
exact-match loop via indexOf("", pos) never returning -1, the exact
pass advanced by 1 instead of by the match length (overlapping
ranges), and maxMatches truncation had no test able to distinguish it
from "returns every occurrence". Adds local ±step refinement of the
fuzzy window, an explicit empty-needle guard, a match-length advance
in the exact pass, and tests asserting the actual slice for fuzzy
matches plus maxMatches truncation/expansion.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ator Adds buildExtractedAnnotations(values, paragraphs) which locates the highlightable fields (numero_recomendacion, fecha_recomendacion, and per-destinatario nombre/cargo) via locateValue and groups the resulting ranges into ExtractedValueAnnotation[] keyed by paragraph id. tema, subtema, destinatario.sector, datos_personales and contenido_para_publicar are classifications/LLM syntheses, not quotations, so they are never annotated (see plan §5.3); contenido_para_publicar support-highlights are a separate, later task (buildSupportAnnotations). Finding from reading generateSplits.ts (file-annotator): it does NOT tolerate overlapping ranges for arbitrary annotation types. Its isRightConflicting check silently drops any token whose start falls before the end of the last accepted split; only "search"-over-"tag" overlaps get special merge treatment via mergeSearchIntoTags, and "extracted" gets none. So the overlap dedup in buildExtractedAnnotations (sort by start asc, length desc, drop anything overlapping an already -accepted annotation) is load-bearing, not just defence-in-depth — without it, colliding annotations would silently vanish from the rendered document rather than fail loudly. FileAnnotator gains two optional props, extraAnnotations and activeField, threaded into the memoised Paragraph annotations list with isActive set per field. Both default to undefined/absent, so existing Anonimizador/Set de Datos call sites take the exact same code path as before (no new array/Map identity introduced when the prop isn't passed). New components/file/extracted-annotation.tsx mark uses Panda CSS (bg.secondary-highlight / bg.primary-highlight variants, borders.primary-alt when active) and exposes data-extracted-field for a later scroll-into-view task. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Critical fix: mergeSearchIntoTags (called unconditionally by generateSplits) rebuilt its output from only tag- and search-filtered tokens, so every "extracted" token was silently dropped before the splitting loop ran — the extracted switch case added in the prior commit was unreachable dead code, and buildExtractedAnnotations never reached the DOM. Corrects the "Paso 1" finding from the prior commit: the isRightConflicting overlap-drop is real and load-bearing, but is second-order — extracted tokens never got that far. Fix passes non-tag/non-search tokens through unchanged: tokens.filter(t => t.type !== "tag" && t.type !== "search"), appended after the existing enrichedTags/visibleSearches. Strictly additive to this shared function: tag-only and search-only inputs are computed by the exact same code as before, verified by new regression tests in generateSplits.test.ts (previously this function had zero test coverage, which is how the drop shipped unnoticed). Also: extracted-annotation.tsx now matches search-annotation.tsx's font treatment (Times New Roman + label.md.default) since <mark> is excluded from FileAnnotator.styles.ts's paragraph font rule; and swaps the active-state `border` for an inset boxShadow ring so activating a field no longer reflows the inline mark. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…n overlap Important fix on top of the prior generateSplits fix: an "extracted" token bypasses mergeSearchIntoTags's enrichment and falls straight through to isRightConflicting's first-wins-by-start rule, so an extracted token starting before an overlapping tag or search could win the slot and silently delete it. A lost search hit is especially bad: FileAnnotator always renders SearchBar, so it stays counted in matchesCount and stays selectable via next/prev, but has no [data-search-match-id] element in the DOM, so the active-match scroll effect silently no-ops. Fix: the non-tag/non-search "others" array is now also filtered to drop any token overlapping an existing tag or search, before the sort. This is a deliberate product call: when a highlight and a search hit (or an anonymizer tag) collide, the search/tag wins, since it reflects the user's active intent or an existing annotation, while an extracted-value highlight is a passive suggestion. Non-overlapping extracted tokens are unaffected; tag-only and search-only inputs still go through the exact same enrichedTags/visibleSearches computation as before, so the two shipped flows stay unchanged. Also strengthens generateSplits.test.ts: the extracted-only and mixed tag+search+extracted cases are now full toEqual assertions (were toMatchObject / toContain, which wouldn't catch a reordering regression); added a toEqual test for the mergeSearchIntoTags enrichment path (search overlapping tag hoists identity and suppresses the search); added two regression tests for this fix (extracted dropped when overlapping a tag; extracted dropped when overlapping a search). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds the multiline text field and labelled radio-group primitives the Recomendaciones validation form needs but @aymurai/ui doesn't provide, and makes DecisionTabs' label prefix and per-tab removal opt-in so the existing Set de Datos flow keeps rendering identically by default. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…errors, wire Stop button - Latch extractRecomendacion() behind a documentId-keyed ref (StrictMode double-effect safe) and wire the abort signal through, cancelling any in-flight extraction on unmount/documentId change instead of orphaning it. - Surface query.isError (re-thrown CanceledError) into useDataExtraction's status/error/retry, and fall through to extraction when a stored document has both prediction and validation null — both previously hung forever in "loading" with no error UI or retry. - Add useDataExtraction().abort() and wire it into RecomendacionesProcess's FileProcessing onAbort, so the Stop button actually cancels work. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… abort surfaces as error, latch regression test - Only abort the (already-permanent) parse-stopped state when parsing is still running; stopping mid-extraction no longer leaves parseStatus stuck "stopped" and Siguiente permanently disabled after a successful retry (N1). - queryClient.cancelQueries reverts a data-less query to pending without an error state, so aborting mid-GET used to hang in "loading" forever. Add a manualStop flag so status/retry treat "Stop landed on the GET" and "Stop landed on the mutation" uniformly, and retry() now re-runs whichever side hasn't succeeded yet (N2). - Document the ordering dependency between the abort-on-unmount cleanup effect and the trigger effect's mutate() call (N3). - Add the missing regression test for the extractionStartedForRef latch, using a pre-seeded query cache + renderHook's reactStrictMode option to genuinely reproduce the StrictMode double-invoke hazard it guards against (verified against both the fix and a reverted-latch sanity check). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…iggers a bogus extraction - documentId === undefined -> "idle" now wins over manualStop in the status ternary. Previously, clicking Stop while the parse stage was still running (no documentId yet) flipped status to "error", rendering a Reintentar that could never recover (the parse stage's own abort is already permanent) and, if clicked, called query.refetch() with documentId undefined -- refetch() doesn't honour enabled:false, so it ran loadRecomendacion(undefined) -> 404 -> fails open to null -> fired a real POST with document_id: undefined and an empty paragraph array. - retry() now no-ops when documentId is undefined, and the extraction latch refuses to fire without one, as defense in depth. - Add the N4 regression test, sanity-checked against the reverted ordering. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…est.tsx Disables the "Validar documento" button while the save mutation is pending (matches voice-to-text/validation.tsx precedent), fixes vacuous payload-negative assertions (the local destinatario id, not the top-level payload), and adds coverage for out-of-taxonomy tema/subtema and the cargo-candidate direction of the nombre/cargo independence guarantee. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds a recomendaciones worksheet to the shared .xlsx workbook that Set de Datos already writes, with an upsert-by-DOCUMENT_ID writer so re-validating a document replaces its row instead of duplicating it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…und-trip ensureRecomendacionesSheet re-attaches worksheet.columns on the existing-sheet branch: exceljs column keys are an in-memory alias that never survives serialisation, so every read() after the first write() made key-addressed cell access throw or silently write blank rows, losing the export past the first document. Also guards finish.tsx against StrictMode's concurrent double-submit, makes offline.ts address set_de_datos by name instead of position now that the workbook has a second sheet, and exports an unanswered datos_personales as "" instead of collapsing it into an explicit "no". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s the final pre-merge fix wave
C1 (critical): the validation screen kept edits in local component state and
dispatched only `validate(fileName)`, so `finish.tsx` built the Excel row from
the untouched `normalizeExtraction` output. With the save endpoint still absent,
the only working persistence path exported the raw LLM inference and silently
dropped every human correction. `handleValidate` now dispatches
`setRecomendacion(..., { ...recomendacion, origin: "validation", values })`
first; the spread keeps `inference`/`suggestions`/`candidates` frozen. Covered by
a new integration test that crosses validation -> reducer -> finish -> worksheet
row with the real reducer, form hook and excel writer.
F1: the fuzzy coarse gate ran the real threshold against an unrefined
`step`-sampled window, discarding ~40% of genuine near-matches before edge
refinement. Gate loosely at `threshold - 0.1`, apply the real threshold to the
refined score. Defaults unchanged; all existing tests pass unmodified.
F2: a scanned/image-only PDF parses into zero paragraphs, leaving the process
screen on a perpetual spinner with an inert Stop button. Surface it as a terminal
error in `process.tsx` (the hook's status ternary is untouched), with no dead
Reintentar since `retry()` is a no-op without a documentId.
F3/F4: both `biome-ignore` comments sat inside the effect bodies and suppressed
nothing; dep arrays completed and the remaining suppression moved to a
single-line comment directly above `useEffect(`. 8 warnings -> 0.
Also: drop 6 unused default exports and 6 unused `export` keywords (knip);
`console.warn` on non-404 `loadRecomendacion` failures; `.default()` on the four
fields that hard-failed an otherwise fail-soft schema; `datos_personales` is
`boolean | null` end to end (no more null -> false coercion); keep-and-flag an
out-of-list `sector` like `tema`/`subtema`; `FECHA_VALIDACION` uses the local
calendar day instead of UTC.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ezone Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Sorry @jansaldo, your pull request is larger than the review limit of 150000 diff characters
Reviewer's GuideAdds a new "Recomendaciones" feature flow for Defensoría del Pueblo CABA, including frontend orchestration of LLM data extraction, controlled validation UI with document highlighting, Excel export into a shared workbook, and the supporting types, hooks, and infrastructure, while keeping existing Dataset/Anonymizer/Voice flows behaviorally intact. Sequence diagram for the new Recomendaciones flow (extraction, validation, export)sequenceDiagram
actor Usuario
participant RecomendacionesProcess
participant useDataExtraction
participant loadRecomendacion
participant extractRecomendacion
participant RecomendacionValidation
participant saveRecomendacion
participant submitRecomendacion
participant filesystem_excel
Usuario->>RecomendacionesProcess: Abre /app/RECOMENDACIONES/process
RecomendacionesProcess->>useDataExtraction: useDataExtraction(file)
useDataExtraction->>loadRecomendacion: GET /llm/recomendaciones/validation/document/{documentId}
alt stored document found
loadRecomendacion-->>useDataExtraction: RecomendacionDocument (prediction/validation)
useDataExtraction->>useDataExtraction: normalizeExtraction / validationToValues
useDataExtraction->>RecomendacionesProcess: dispatch(setRecomendacion)
else no stored data
loadRecomendacion-->>useDataExtraction: null
useDataExtraction->>extractRecomendacion: POST /llm/data-extraction
extractRecomendacion-->>useDataExtraction: DataExtractionResult
useDataExtraction->>RecomendacionesProcess: dispatch(setRecomendacion)
end
Usuario->>RecomendacionValidation: Abre /app/RECOMENDACIONES/validation
RecomendacionValidation->>RecomendacionValidation: useRecomendacionForm(recomendacion)
RecomendacionValidation->>FileAnnotator: FileAnnotator(extraAnnotations, activeField)
RecomendacionValidation->>RecomendacionValidation: buildExtractedAnnotations(values, paragraphs)
Usuario->>RecomendacionValidation: Hace clic "validation.validar"
RecomendacionValidation->>saveRecomendacion: saveRecomendacion(documentId, toValidationPayload(values))
alt guardado exitoso
saveRecomendacion-->>RecomendacionValidation: 204
else error de guardado
saveRecomendacion-->>RecomendacionValidation: Error
RecomendacionValidation->>RecomendacionValidation: showToast(validation.saveFailed)
end
RecomendacionValidation->>RecomendacionValidation: dispatch(setRecomendacion(origin="validation"))
Usuario->>submitRecomendacion: Abre RecomendacionFinish
submitRecomendacion->>filesystem_excel: read() / create()
submitRecomendacion->>filesystem_excel: ensureRecomendacionesSheet(workbook)
submitRecomendacion->>submitRecomendacion: toExcelRow(values, documentId, fileName, validatedAt)
submitRecomendacion->>filesystem_excel: write(workbook)
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
Add USE_MOCK_RECOMENDACIONES (mirrors USE_MOCK_STT) so the Recomendaciones flow can be exercised end to end without a backend: fileParser, extractRecomendacion, loadRecomendacion and saveRecomendacion all short-circuit behind the flag, using a realistic Defensoría fixture whose tema/subtema are checked against the taxonomy and whose paragraphs contain the extracted values verbatim (for highlight testing). Off by default, so existing behavior and tests are unaffected. Also add temporary console.info diagnostics (prefixed [recomendaciones], gated behind the mock flag or VITE_DEBUG_RECOMENDACIONES) to useDataExtraction so the process screen's stuck-at-50% hang can be localized to a specific branch without changing any state-machine logic. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…how extraction elapsed time USE_MOCK_RECOMENDACIONES previously dead-ended: APIProtected redirects to "/" whenever api.defaults.baseURL is unset, and only the real connect-to-host flow ever set it, so the mock flow was unreachable without standing up a stub healthcheck server. Extend the existing VITE_DEV_HOST dev-only block in services/api.ts to also pre-set a placeholder baseURL when mocking Recomendaciones and no dev host is configured. Also, a slow (multi-minute) real LLM extraction rendered as a flat "50%" with no signal of progress, indistinguishable from a hang. Add a "Extrayendo datos… mm:ss" elapsed-time indicator under the process screen's subtitle while extraction is loading, reusing the existing formatTime (mm:ss) helper from voice-to-text. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
jansaldo
marked this pull request as draft
August 4, 2026 17:08
…es-defensoria # Conflicts: # src/renderer/src/constants/config.ts # src/renderer/src/constants/i18n/locales/es/index.ts # src/renderer/src/env.d.ts # src/renderer/src/routes/app.$feature/finish.tsx # src/renderer/src/routes/app.$feature/process.tsx # src/renderer/src/routes/app.$feature/route.tsx # src/renderer/src/routes/app.$feature/validation.tsx # src/renderer/src/routes/home/features.tsx # src/renderer/src/services/aymurai/queries.ts # src/renderer/src/store/useLocal.ts # src/renderer/src/types/features.ts
…light color Product owner tried the Recomendaciones validation form and asked for two changes: - Remove the organigram-candidates feature entirely (candidatos_nombre/ candidatos_cargo, OrganigramPicker, and all wiring/tests/docs) — it added no value in the validation form. - Make the extracted-value highlight use the same violet (bg.primary- alternative) as the rest of the app's NER-predicted marks, instead of a flat gray, since there's no NER model behind these LLM-extracted locations but the visual language should still match. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Qué hace
Agrega un cuarto flujo — Recomendaciones — para la Defensoría del Pueblo de CABA. El usuario carga una Recomendación, un LLM local extrae los campos estructurados vía
POST /llm/data-extraction, los valida contra el documento original con los valores resaltados, y el registro se acumula en una hojarecomendacionesdel mismo.xlsxque ya usa el Set de Datos.Implementa el plan en
docs/superpowers/plans/2026-07-31-recomendaciones-defensoria.md(etapas 1–11), con las decisiones de producto ya resueltas en su §9.Arquitectura
Reutiliza el árbol de rutas
/app/$feature/*con el mismo patrón de ramificación que Voz a Texto. Sin cambios ensrc/mainnisrc/preload.FileProvider, reducer de archivos,useFileParse,fileParser, layout,FileProcessing,FileCheck,services/filesystem/excel.FileAnnotator(extraAnnotations/activeFieldopcionales),generateSplits(nuevo tipoextracted),DecisionTabs(label/onRemove),DocFile.recomendacion?, una acción nueva en el reducer.TextareayRadioGroupen Panda.Los tres flujos existentes (Set de Datos, Anonimizador, Voz a Texto) quedan verificadamente intactos: ninguno pasa las props nuevas, y la revisión final diffeó el output de
generateSplitssobre 9702 entradas generadas de solotag/search— 0 diferencias.Highlighting sin offsets
La extracción no viene de NER, así que no hay offsets.
utils/recomendaciones/locate-value.tslos deriva: matching exacto sobre todo el documento (todas las ocurrencias), y solo si no hubo ninguno, un pase fuzzy que devuelve el mejor match con umbral 0.9. Se resaltannumero_recomendacion,fecha_recomendacion, ynombre/cargopor destinatario; no se resaltantema,subtema,sector,datos_personalesnicontenido_para_publicar, porque son clasificaciones o síntesis del modelo y resaltarlos apuntaría a texto arbitrario.Recall medido sobre entradas realistas en español: 99% en
nombreycargo, con precisión 100% sobre 1107 negativos (incluidos cientos construidos en la banda 0.80–0.899).Backend pendiente
La persistencia no existe todavía.
loadRecomendaciondevuelvenullante 404/405/501 y errores de red, así que el flujo degrada a re-extraer siempre — es el camino esperado hoy, no un bug. El contrato que necesita el frontend está endocs/superpowers/plans/2026-07-31-recomendaciones-backend-contract.md, copiado del patrónprediction+validationque ya usandatapublicyasr.Tres requisitos ahí son fáciles de implementar mal, y el segundo es el más importante del contrato:
predictiondebe guardar elDataExtractionResultcompleto, concandidatos_nombreycandidatos_cargo— el frontend recupera de ahí los selectores de organigrama al reabrir.validationdebe round-trippear un id estable por destinatario. Hoy el frontend re-empareja por posición, lo que se desalinea si el usuario borró un destinatario antes de guardar.datos_personalesesboolean | null, dondenull= sin responder. No coercionar afalse.Verificación
pnpm test— 340 tests, 63 archivos, verde.pnpm typecheck— limpio.pnpm knip— exit 0, sin exports duplicados nuestros.pnpm lintfalla, pre-existente: el script esbiome checksin path, procesa 0 archivos y sale con error. Idéntico en el punto de fork.npx biome check src/está limpio en todo el código nuevo.@aymurai/uiproducía dos copias de React).Cada etapa pasó por revisión propia con rondas de corrección. Varios bugs solo aparecieron al probar los módulos empíricamente en vez de leerlos — vale mencionar tres, porque los tests pasaban en verde en los tres casos:
generateSplitsdescartaba las anotaciones nuevas una función más abajo de donde se había mirado, dejando la feature en no-op invisible.keyde columna de exceljs no sobreviven la serialización, así que la exportación fallaba en el segundo documento (silenciosamente: el throw se comía en la lista de errores)..xlsxrecibía la inferencia cruda del LLM y descartaba todo lo que el humano había corregido. Ningún test por etapa podía verlo: el de la etapa 9 verificaba el payload de la mutación y el de la 10 alimentabatoExcelRowdirecto. Nada cruzaba la costura del reducer.Pendiente para el reviewer
La pasada manual no se corrió — necesita el backend LLM local y un PDF real de Recomendación. Es la etapa 11 paso 2 del plan, y es el paso que habría detectado el bug de exceljs en segundos. Recomiendo correrla antes de mergear.
Riesgo a decidir
Con
validationsin historial (§9.5) yloadRecomendacionfallando anull, cuando la persistencia exista una falla transitoria delGETva a re-extraer en silencio y puede sobrescribir una validación humana con una inferencia nueva, sin dejar rastro. El frontend ahora loguea unconsole.warn, que es el piso; una mitigación real necesita una señal visible al usuario o historial en el backend.🤖 Generated with Claude Code
Summary by Sourcery
Add a new Recomendaciones feature flow for Defensoría del Pueblo CABA, integrating LLM-based data extraction, validation UI, and Excel export into the existing app.$feature architecture while keeping existing Dataset, Anonymizer, and VoiceToText flows intact.
New Features:
Enhancements:
Documentation:
Tests: