Skip to content

feat: agregar flujo de Recomendaciones (Defensoría del Pueblo CABA) - #93

Draft
jansaldo wants to merge 30 commits into
developfrom
feat/recomendaciones-defensoria
Draft

feat: agregar flujo de Recomendaciones (Defensoría del Pueblo CABA)#93
jansaldo wants to merge 30 commits into
developfrom
feat/recomendaciones-defensoria

Conversation

@jansaldo

@jansaldo jansaldo commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

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 hoja recomendaciones del mismo .xlsx que 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 en src/main ni src/preload.

  • Reutilizado sin cambios: FileProvider, reducer de archivos, useFileParse, fileParser, layout, FileProcessing, FileCheck, services/filesystem/excel.
  • Extendido aditivamente: FileAnnotator (extraAnnotations / activeField opcionales), generateSplits (nuevo tipo extracted), DecisionTabs (label / onRemove), DocFile.recomendacion?, una acción nueva en el reducer.
  • Nuevo: hook de estado controlado del formulario, localizador exact+fuzzy de valores, pantalla de validación, exportación a Excel, Textarea y RadioGroup en 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 generateSplits sobre 9702 entradas generadas de solo tag/search — 0 diferencias.

Highlighting sin offsets

La extracción no viene de NER, así que no hay offsets. utils/recomendaciones/locate-value.ts los 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 resaltan numero_recomendacion, fecha_recomendacion, y nombre/cargo por destinatario; no se resaltan tema, subtema, sector, datos_personales ni contenido_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 nombre y cargo, 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. loadRecomendacion devuelve null ante 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á en docs/superpowers/plans/2026-07-31-recomendaciones-backend-contract.md, copiado del patrón prediction + validation que ya usan datapublic y asr.

Tres requisitos ahí son fáciles de implementar mal, y el segundo es el más importante del contrato:

  1. prediction debe guardar el DataExtractionResult completo, con candidatos_nombre y candidatos_cargo — el frontend recupera de ahí los selectores de organigrama al reabrir.
  2. El payload de validation debe 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.
  3. datos_personales es boolean | null, donde null = sin responder. No coercionar a false.

Verificación

  • pnpm test340 tests, 63 archivos, verde.
  • pnpm typecheck — limpio. pnpm knip — exit 0, sin exports duplicados nuestros.
  • pnpm lint falla, pre-existente: el script es biome check sin 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.
  • Incluye un cherry-pick de fix: migrate pnpm allowBuilds to v11 and pin packageManager #92, necesario para que la suite corra acá (el symlink de @aymurai/ui producí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:

  • generateSplits descartaba las anotaciones nuevas una función más abajo de donde se había mirado, dejando la feature en no-op invisible.
  • Los key de 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).
  • La pantalla de validación guardaba las correcciones solo en estado local, así que el .xlsx recibí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 alimentaba toExcelRow directo. 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 validation sin historial (§9.5) y loadRecomendacion fallando a null, cuando la persistencia exista una falla transitoria del GET va a re-extraer en silencio y puede sobrescribir una validación humana con una inferencia nueva, sin dejar rastro. El frontend ahora loguea un console.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:

  • Introduce the Recomendaciones flow routed through /app/$feature with onboarding, process, validation, and finish steps.
  • Add controlled Recomendaciones validation form with document-side highlighting for extracted values and organigram candidate pickers.
  • Export validated recomendaciones to a new recomendaciones worksheet in the existing Excel workbook, keyed by document UUID.

Enhancements:

  • Extend FileAnnotator and generateSplits to support non-tag/search extracted annotations without affecting current flows.
  • Generalize DecisionTabs with configurable labels and optional per-tab remove control.
  • Add Panda-based Textarea and RadioGroup primitives to complement @aymurai/ui components.
  • Implement robust data-extraction orchestration hook that handles cached backend data, fresh LLM runs, and abort/retry behaviour under React Query and StrictMode.

Documentation:

  • Add detailed implementation and backend-contract plans for the Recomendaciones flow, covering architecture, data model, backend endpoints, and risk considerations.

Tests:

  • Add comprehensive unit and integration tests for Recomendaciones form state, value locator, annotations, data-extraction hook, Excel export, and validation screen, including StrictMode and Exceljs edge cases.

jansaldo and others added 26 commits August 2, 2026 17:53
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>

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @jansaldo, your pull request is larger than the review limit of 150000 diff characters

@sourcery-ai

sourcery-ai Bot commented Aug 3, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds 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)
Loading

File-Level Changes

Change Details Files
Introduce the Recomendaciones feature flow into the existing /app/$feature route tree, with dedicated process, validation, and finish screens wired to the file reducer and React Query.
  • Extend FeatureFlowEnum, featureNamespace, home feature cards, and feature icons to include Recomendaciones
  • Add branching in app.$feature onboarding/preview/process/validation/finish routes to render Recomendaciones-specific components
  • Implement RecomendacionesProcess and RecomendacionValidation/Finish components that reuse FileProvider, layout, and RequireFile while orchestrating parsing, extraction, validation, and Excel export
src/renderer/src/types/features.ts
src/renderer/src/constants/config.ts
src/renderer/src/routes/home/features.tsx
src/renderer/src/routes/app.$feature/route.tsx
src/renderer/src/routes/app.$feature/onboarding.tsx
src/renderer/src/routes/app.$feature/preview.tsx
src/renderer/src/routes/app.$feature/process.tsx
src/renderer/src/routes/app.$feature/validation.tsx
src/renderer/src/routes/app.$feature/finish.tsx
src/renderer/src/components/recomendaciones/process.tsx
src/renderer/src/components/recomendaciones/validation.tsx
src/renderer/src/components/recomendaciones/finish.tsx
Add domain modeling, schemas, hooks, and HTTP clients for Recomendaciones, including controlled form state and data-extraction orchestration with backend persistence fallback.
  • Define Recomendaciones types (DataExtractionResult, DestinatarioValue, RecomendacionState) and zod schemas for extraction and validation payloads
  • Implement useRecomendacionForm for controlled form state, normalization of extraction results, and pristine/suggestion tracking
  • Implement useDataExtraction hook that loads stored prediction/validation or triggers LLM extraction, attaching RecomendacionState to DocFile via a new reducer action
  • Add servicios/aymurai/recomendaciones client (extractRecomendacion/loadRecomendacion/saveRecomendacion) and recomendacionValidationMutation in queries.ts
  • Extend DocFile with optional recomendacion and reducer/actions with SET_RECOMENDACION
src/renderer/src/types/recomendaciones.ts
src/renderer/src/schema/recomendaciones.ts
src/renderer/src/hooks/useRecomendacionForm.ts
src/renderer/src/hooks/useDataExtraction.ts
src/renderer/src/services/aymurai/recomendaciones.ts
src/renderer/src/services/aymurai/queries.ts
src/renderer/src/reducers/file/actions.ts
src/renderer/src/reducers/file/index.ts
src/renderer/src/types/file.ts
Implement frontend value location and annotation building for highlighting extracted fields in the existing FileAnnotator, including support for a new "extracted" annotation type.
  • Add ExtractedValueAnnotation and extend Annotation union in file-annotator/types.ts
  • Update FileAnnotator to accept extraAnnotations and activeField props, merging extracted annotations into the existing tag/search annotations and rendering them via ExtractedAnnotation
  • Fix generateSplits token merge to preserve non-tag/search tokens (e.g. extracted) and drop them only when overlapping tag/search
  • Implement locateValue (exact+fuzzy matching over paragraphs using fastest-levenshtein) and buildExtractedAnnotations to convert RecomendacionValues into per-paragraph extracted annotations
src/renderer/src/components/file-annotator/types.ts
src/renderer/src/components/file-annotator/index.tsx
src/renderer/src/components/file-annotator/generateSplits.ts
src/renderer/src/components/file/extracted-annotation.tsx
src/renderer/src/utils/recomendaciones/locate-value.ts
src/renderer/src/utils/recomendaciones/build-annotations.ts
Extend UI primitives and decision tabs to support the Recomendaciones validation UX, including a Panda Textarea, RadioGroup wrapper, and removable, relabelable tabs for destinatarios.
  • Add Panda-based Textarea component mirroring @aymurai/ui TextField styling for multiline content
  • Add Panda-based RadioGroup wrapper around @aymurai/ui Radio for grouped labelled radios
  • Parameterize DecisionTabs to accept a label prop and optional onRemove handler, adding an accessible remove control per tab when applicable
  • Implement RecomendacionForm, DestinatarioFields, and OrganigramPicker to render the right-hand validation form with suggestions, organigram candidates, and out-of-taxonomy/list warnings
src/renderer/src/components/ui/textarea.tsx
src/renderer/src/components/ui/radio-group.tsx
src/renderer/src/components/decision-tabs/index.tsx
src/renderer/src/components/recomendaciones/recomendacion-form.tsx
src/renderer/src/components/recomendaciones/destinatario-fields.tsx
src/renderer/src/components/recomendaciones/organigram-picker.tsx
Add Excel export support for Recomendaciones in the shared workbook, using a new recomendaciones worksheet and an upsert writer keyed by DOCUMENT_ID.
  • Define RECOMENDACIONES_COLUMNS and toExcelRow to flatten RecomendacionValues into a single recomendaciones row with identity and destinatario summary columns
  • Add ensureRecomendacionesSheet to create or re-key a recomendaciones worksheet in the shared workbook, handling exceljs column key aliasing after read/write round-trips
  • Implement submitRecomendacion to read or create the workbook, upsert recomendaciones rows by DOCUMENT_ID, and write back to disk
  • Wire RecomendacionFinish to call submitRecomendacion per file and expose a "Ver recomendaciones" button via filesystem.excel.open; adjust offline dataset writer to target set_de_datos by name
src/renderer/src/utils/recomendaciones/to-excel-rows.ts
src/renderer/src/services/filesystem/excel/recomendaciones-sheet.ts
src/renderer/src/utils/recomendaciones/submit-recomendacion.ts
src/renderer/src/components/recomendaciones/finish.tsx
src/renderer/src/utils/file/submitValidations/offline.ts
Add Recomendaciones-specific constants, taxonomy, i18n strings, and planning/contract docs to support the flow and its backend integration.
  • Introduce TAXONOMY, TEMA_OPTIONS, subtemaOptions, and SECTOR_OPTIONS for tema/subtema/sector selects
  • Add Spanish i18n namespace for recomendaciones covering titles, process/validation/finish text, and validation error messages
  • Add long-form implementation plan and backend contract docs under docs/superpowers/plans for Recomendaciones and its persistence API
src/renderer/src/constants/recomendaciones/taxonomy.ts
src/renderer/src/constants/recomendaciones/sectores.ts
src/renderer/src/constants/i18n/locales/es/recomendaciones.ts
src/renderer/src/constants/i18n/locales/es/index.ts
docs/superpowers/plans/2026-07-31-recomendaciones-defensoria.md
docs/superpowers/plans/2026-07-31-recomendaciones-backend-contract.md
Adjust infrastructure and config to support the new code, dependencies, and test behavior without breaking existing flows.
  • Add fastest-levenshtein dependency and bump pnpm packageManager version; remove pnpm.onlyBuiltDependencies block
  • Update pnpm-workspace allowBuilds for @aymurai/ui tarball URL hash
  • Configure tanstack-router codegen to ignore .test.tsx route files and vitest to run with Argentina time zone
  • Fix offline dataset writer to select set_de_datos worksheet by name instead of index so it can coexist with recomendaciones
package.json
pnpm-workspace.yaml
vite.config.ts
vitest.config.ts
src/renderer/src/utils/file/submitValidations/offline.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

jansaldo and others added 2 commits August 3, 2026 16:08
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
jansaldo marked this pull request as draft August 4, 2026 17:08
jansaldo and others added 2 commits August 5, 2026 12:25
…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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant