Agent Squad · plan de implementación

Cadena de custodia: cerrar GAP 1 + GAP 2

Que un auditor pueda rastrear cualquier claim hasta (a) la ejecución exacta del agente que lo generó —con integridad referencial DB-enforced— y (b) el documento fuente, vía un grafo de linaje real y poblado, no teórico.

4 waves 9 tasks TDD · Done when por task Postgres · Bun · Vitest migraciones 0011–0013

Arquitectura — dos ejes independientes

GAP 2 (Wave 1, barato): emitClaim deja de delegar las aristas al caller y las escribe él mismo, transaccionalmente, desde source_refs tipados; + backfill. Desbloquea el eje claim→documento. Ya estaba medio resuelto: publishArtifact escribe edges; faltaba el de claims.

GAP 1 (Wave 2, caro): hoy emitClaim corre antes de que exista la fila step_executions → se reordena el ciclo de vida del step (crearla al inicio con su id) para atar el claim por FK compuesta a la PK de la tabla particionada; + backfill + VALIDATE.

Wave 0
spike / preparación
Task 1 — runner de migraciones + decisión de orden del step
Wave 1 · GAP 2
depende de W0
Tasks 2·3·4 — lineage real de claims + backfill
Wave 2 · GAP 1
depende de W0
Tasks 5·6·7·8 — reorder + FK + backfill+VALIDATE
Wave 3
depende de W1+W2
Task 9 — verificación E2E (consulta del auditor)

01 Spike de preparación Wave 0

📁 lee db/substrate/migrations/README.md · execute-plan.ts · traces.ts · crea doc de decisiones
Done when
  • Comando exacto de migración (del README) y de test (vitest) documentados y verificados.
  • Confirmado que cada emitClaim ocurre antes de su recordStepExecution → justifica el reorder de Task 5.
  • Decisión registrada: FK compuesta a step_executions(id, trace_started_at), no trigger.
Steps
  1. Leer README + aplicar una migración existente en DB drill.
  2. Confirmar cd apps/api && npx vitest run.
  3. Mapear orden emitClaim vs recordStepExecution.
  4. Escribir el doc de decisiones.

02 Tipar source_refs Wave 1 GAP 2

📁 modifica claims.ts (EmitClaimInput) · test claims.test.ts
Done when
  • vitest run src/substrate/claims.test.ts PASS, con caso legacy string[] y caso {id,type}.
  • tsc --noEmit sin errores nuevos.
Steps
  1. Test: normalizeSourceRefs(['abc'])[{id:'abc',type:'claim'}].
  2. Correr → FAIL.
  3. Implementar SourceRef + normalizeSourceRefs + ampliar EmitClaimInput.
  4. PASS + commit.

03 emitClaim escribe lineage_edges atómico Wave 1 GAP 2

📁 modifica claims.ts (emitClaim) · test claims.test.ts
Done when
  • Tras emitClaim con 2 source_refs → 2 filas en lineage_edges (from_type='claim').
  • Si falla un edge, el claim NO persiste (atomicidad — sql.begin).
  • Sin regresión en los tests de claims.
Steps
  1. Test: aseverar 2 edges tras emitir.
  2. FAIL (hoy no escribe).
  3. Envolver en sql.begin; loop sobre source_refs insertando edges; borrar "handled by the caller".
  4. PASS + commit.

04 Migración 0011 — backfill lineage de claims Wave 1 GAP 2

📁 crea 0011_backfill_claim_lineage.sql · test lineage-backfill.test.ts
Done when
  • Idempotente (ON CONFLICT DO NOTHING), re-ejecutable sin duplicar.
  • Tras aplicarla en drill: edges from_type='claim' = suma de source_refs.
  • Documentado que los source_refs legacy sin tipo se asumen claim.
Steps
  1. Test: seed claim con source_refs → 1 edge.
  2. FAIL.
  3. SQL INSERT … SELECT … LATERAL jsonb_array_elements ….
  4. Aplicar en drill + PASS + commit.

05 Reorder del step — start/finish Wave 2 GAP 1

📁 modifica traces.ts · execute-plan.ts · test traces.test.ts
Done when
  • startStepExecution devuelve {step_execution_id, trace_started_at} y crea la fila running.
  • finishStepExecution la cierra a succeeded por PK.
  • execute-plan.ts llama start antes del primer emitClaim; sin regresión global.
Steps
  1. Test start→running con id, finish→succeeded.
  2. FAIL.
  3. Dividir el INSERT actual: start con RETURNING id, trace_started_at; finish con UPDATE … WHERE id=$1 AND trace_started_at=$2.
  4. Reconectar call-sites + PASS + commit.

06 Propagar step_execution_id a emitClaim Wave 2 GAP 1

📁 modifica claims.ts · execute-plan.ts · artifact-publish.ts
Done when
  • emitClaim con step_execution_id lo persiste en claims.step_execution_id.
  • Todos los call-sites pasan el id de startStepExecution.
  • vitest run global PASS.
Steps
  1. Test persistencia del id.
  2. FAIL (requiere Task 7 para la columna — mismo PR).
  3. Ampliar EmitClaimInput.provenance + INSERT + call-sites.
  4. PASS + commit.

07 Migración 0012 — columnas + FK NOT VALID Wave 2 GAP 1

📁 crea 0012_claims_step_exec_fk.sql · test claims-fk.test.ts
Done when
  • claims con step_execution_id + step_exec_started_at (nullable) y FK compuesta a step_executions(id, trace_started_at) ON DELETE RESTRICT NOT VALID.
  • Claim con step_execution_id inexistente → falla (FK activa en filas nuevas aun NOT VALID).
  • Claim con ambas NULL → permitido (compat).
Steps
  1. Test insert con id inexistente → error FK.
  2. FAIL.
  3. ALTER TABLE claims ADD COLUMN … ADD CONSTRAINT … NOT VALID.
  4. Aplicar drill + PASS + commit.

08 Backfill 0013 + VALIDATE Wave 2 GAP 1

📁 crea 0013_backfill_claim_step_exec.sql · scripts/backfill-claim-step-exec.ts
Done when
  • Resuelve (trace_id, step_id) → step_executions eligiendo la succeeded más cercana a asserted_at; 0 o >1 candidatos → NULL + reporte.
  • VALIDATE CONSTRAINT claims_step_exec_fk corre sin error.
  • Reportado el % resuelto vs NULL (legacy ambiguos = limitación documentada).
Steps
  1. Test: 1 resoluble + 1 ambiguo (2 step_execs) → resoluble con id, ambiguo NULL.
  2. FAIL.
  3. Script TS (DISTINCT ON + cercanía a asserted_at) + migración con VALIDATE.
  4. Drill + PASS + commit.

09 Verificación E2E — la consulta del auditor Wave 3

📁 test custody-lineage.test.ts · documenta la consulta en CONCEPTS.md / disaster-recovery.md
Done when
  • Test integración: doc fuente → claim derivado (con source_refs + step_execution_id) por el flujo real; correr ambos ejes.
  • Eje 1 devuelve actor_resolved + step exacto vía FK; Eje 2 (recursivo) llega al artifact doc con su content_addr.
  • La consulta SQL canónica de auditoría queda documentada en el repo.
Steps
  1. Test del flujo E2E + ambas consultas.
  2. FAIL.
  3. Implementar con helpers reales; copiar las consultas al doc.
  4. PASS + commit.

⚠ Riesgo registrado (decisión de alcance)

Si tras la Task 1 el reorder del step (Task 5) resulta más invasivo de lo estimado, la Wave 2 (GAP 1) puede diferirse y entregar solo la Wave 1 (GAP 2) — que ya cierra la trazabilidad-al-documento, el 80% del valor para el auditor.

El GAP 1 (integridad DB-enforced del salto claim→step) es el 20% restante y se justifica recién cuando exista auditoría contractual que lo exija.