docs(flows): DoD obligatorio con user-facing surface + abrir issues 0100-0103 (taxonomia, frontmatter migration, dev_console, work dashboard)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-17 00:07:03 +02:00
parent 212875ed0d
commit 5d2a14e50a
77 changed files with 4062 additions and 311 deletions
+2
View File
@@ -190,6 +190,8 @@ Ventajas vs `command: ./fn run ...`:
- API: `GET /api/functions/{id}` devuelve `{id, name, description, signature, purity, domain, lang, uses_functions[], uses_types[]}` leyendo `registry.db` read-only. La UI consume este endpoint al expandir un step.
- Validator regex en `dag_validate`: `^[a-z0-9_]+_[a-z]+_[a-z]+$`. ID invalido = error.
- Variables de entorno: `FN_REGISTRY_ROOT` (default `/home/lucas/fn_registry`) localiza el binario `fn`. Override con `FN_BIN=/path/al/fn`.
- **`FN_REGISTRY_ROOT` obligatorio cuando el servicio corre via systemd** con `WorkingDirectory` fuera del root del registry. El binario `fn` resuelve `registry.db` por (1) env var, (2) walk-up buscando `go.mod`, (3) exe dir. Si (1) no esta y (2) encuentra el `go.mod` del propio servicio (ej. `apps/dag_engine/go.mod`), devuelve un dir donde `registry.db` no existe o esta stale, fallando con `error: function "<id>" not found`. Bug historico: `apps/dag_engine/registry.db` stale (May 15) tumbo 3 noches `fn_backup` + `daily-registry-audit`. Defensa en profundidad: el executor exporta `FN_REGISTRY_ROOT` y hace `cd $FN_REGISTRY_ROOT` antes del spawn de steps `function:` (executor.go), pero el `Environment=FN_REGISTRY_ROOT=...` del systemd unit sigue siendo la fuente de verdad.
- **`PATH` en el systemd unit**: si steps `function:` invocan funciones Go sin tests (`go vet`) o Python (`python3`), el `PATH` del entorno systemd debe incluir esos binarios — declarar `Environment=PATH=/usr/local/go/bin:/home/lucas/go/bin:/home/lucas/.local/bin:/usr/local/bin:/usr/bin:/bin`.
Ejemplo completo: `~/.dagu/dags/example-fn-call.yaml`.
+31
View File
@@ -84,6 +84,37 @@ cd .. && CGO_ENABLED=1 go build -tags fts5 -o dag-engine .
Compatible con el formato YAML de Dagu. Lee DAGs existentes de `~/dagu/dags/` sin modificaciones.
Puerto por defecto 8090 (mismo que Dagu).
### 2026-05-16 — Fix function-not-found en steps `function:` + panel Logs en RunDetail `[done]`
Sintoma: `fn_backup` y `daily-registry-audit` fallaron 3 noches seguidas con `error: function "<id>" not found (tried as ID and name)` aunque las funciones existen en `registry.db` raiz.
Raiz: servicio systemd `dag_engine.service` tiene `WorkingDirectory=/home/lucas/fn_registry/apps/dag_engine`. Binario `fn` resuelve `registry.db` por (1) `FN_REGISTRY_ROOT`, (2) `root()` walk-up buscando `go.mod`, (3) exe dir (`cmd/fn/ops.go:1597-1628`). Sin `FN_REGISTRY_ROOT` seteado, (2) encuentra el `go.mod` de `apps/dag_engine/` y devuelve ese dir — donde habia una copia stale `apps/dag_engine/registry.db` (262 KB, May 15) sin las funciones recien creadas. Viola regla `.claude/rules/db_locations.md` (registry.db SOLO en raiz).
Fix:
- Borrado `apps/dag_engine/registry.db` stale.
- `~/.config/systemd/user/dag_engine.service`: anadido `Environment=FN_REGISTRY_ROOT=/home/lucas/fn_registry`, `FN_BIN=/home/lucas/fn_registry/fn`, `PATH=/usr/local/go/bin:/home/lucas/go/bin:...`, `HOME=/home/lucas`. Sin PATH el step `go vet` fallaba con `exec: "go": executable file not found in $PATH`.
- `apps/dag_engine/executor.go`: para steps `function:` el spawn exporta `FN_REGISTRY_ROOT=<root>` en env y, si `step.dir`/`working_dir` vacios, fija `dir = fnRegistryRoot`. Belt-and-suspenders: aunque alguien lance el binario sin systemd, los `function:` steps usan el root canonico.
Verificacion: `POST /api/dags/daily-registry-audit/run` -> step `audit_capabilities` pasa (387 ms) en vez de fallar con not-found. Restantes failures (`audit_artefacts` exit 1, `fn_backup` exit 4 sin respetar `continue_on.exit_code`) son bugs reales independientes — fuera de scope.
### 2026-05-16 — Panel Logs en RunDetail (frontend) `[done]`
- `apps/dag_engine/frontend/src/pages/RunDetail.tsx`: nuevo `<Paper>` "Logs" al final con `<Code block>` scrollable (max-h 480) + `CopyButton` de Mantine (icono toggle copy/check teal).
- Helper `buildLogText(run, steps)` compone texto plano: metadata del run (dag, path, status, trigger, started/finished ISO, duration ms, error) + por step (`[status] name exit=N Nms`, started, finished, error, stdout, stderr indentado 4 espacios).
- Permite pegar log entero al LLM para debugging sin abrir N collapses del `StepTimeline`.
- Build frontend pendiente: `pnpm build` rompe por errores preexistentes (`StepTimeline.tsx:49` usa API legacy `<Collapse in={opened}>`; `main.tsx:1` importa `@mantine/core/styles.css` sin tipos). Edit de RunDetail type-checkea limpio.
### 2026-05-16 — BBDDs canonicas (referencia rapida)
- `dag_engine.db`: `apps/dag_engine/dag_engine.db` (+ WAL sidecars). Migrations en `apps/dag_engine/store/migrations/` (`001_init.sql`, `002_step_function_id.sql`). Tablas `dag_runs`, `dag_step_results`.
- NO debe coexistir copia de `registry.db` en este dir (viola `db_locations.md`). Si reaparece: borrarla.
## Lo siguiente que pega
- `audit_artefacts` falla con exit 1 en `daily-registry-audit` — investigar stderr real (probablemente artefacto huerfano o git drift). Step independiente, no bloquea el resto del DAG.
- `fn_backup` step `run_backup_all` sale con exit 4 y el DAG no respeta `continue_on.exit_code: [4]`. Bug en executor: parsear `step.ContinueOn.ExitCode []int` y comparar con `result.ExitCode`. Hoy solo se mira `step.ContinueOn.Failure` (bool).
- Frontend `pnpm build` roto por API drift de Mantine en `StepTimeline.tsx` (`<Collapse in={opened}>`) y CSS type import en `main.tsx`. Fix junto con un refresh general de tipos.
## Documentacion de usuario
Guia completa (formato YAML, anadir DAGs, troubleshooting, endpoints HTTP):
+18 -6
View File
@@ -160,15 +160,16 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
// Resolve command source: function (registry) takes precedence over command/script.
var command string
var stepFunctionID string
var fnRegistryRoot string
if step.Function != "" {
stepFunctionID = step.Function
fnRegistryRoot = os.Getenv("FN_REGISTRY_ROOT")
if fnRegistryRoot == "" {
fnRegistryRoot = "/home/lucas/fn_registry"
}
fnBin := os.Getenv("FN_BIN")
if fnBin == "" {
root := os.Getenv("FN_REGISTRY_ROOT")
if root == "" {
root = "/home/lucas/fn_registry"
}
fnBin = root + "/fn"
fnBin = fnRegistryRoot + "/fn"
}
parts := []string{fnBin, "run", step.Function}
parts = append(parts, step.Args...)
@@ -191,6 +192,13 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
// Build environment.
env := buildStepEnv(dag, step, daguEnvPath, outputs)
// For function: steps, force FN_REGISTRY_ROOT into env so `fn run`
// resolves the canonical registry.db (not whatever lives at the spawn cwd).
// Prevents the apps/dag_engine/registry.db stale-shadow bug (2026-05-16).
if stepFunctionID != "" {
env = append(env, "FN_REGISTRY_ROOT="+fnRegistryRoot)
}
if command == "" {
e.store.UpdateStepResult(stepID, "skipped", 0, "", "", nil, 0, "no command or script")
return nil
@@ -201,11 +209,15 @@ func (e *Executor) executeStep(ctx context.Context, runID string, dag core.DagDe
command = resolveStepRefs(command, outputs)
mu.Unlock()
// Determine working directory.
// Determine working directory. function: steps default to FN_REGISTRY_ROOT
// so `fn` resolves registry.db correctly via go.mod walk-up.
dir := step.Dir
if dir == "" {
dir = dag.WorkingDir
}
if dir == "" && stepFunctionID != "" {
dir = fnRegistryRoot
}
shell := step.Shell
if shell == "" {
@@ -9,12 +9,63 @@ import {
Paper,
Alert,
Loader,
CopyButton,
Tooltip,
ActionIcon,
Code,
} from "@mantine/core";
import { IconArrowLeft } from "@tabler/icons-react";
import { IconArrowLeft, IconCopy, IconCheck } from "@tabler/icons-react";
import { getRun } from "../api";
import { StatusBadge } from "../components/StatusBadge";
import { StepTimeline } from "../components/StepTimeline";
import type { RunDetail as RunDetailType } from "../types";
import type { RunDetail as RunDetailType, DagStepResult, DagRun } from "../types";
function buildLogText(run: DagRun, steps: DagStepResult[]): string {
const lines: string[] = [];
const started = run.StartedAt ? new Date(run.StartedAt) : null;
const finished = run.FinishedAt ? new Date(run.FinishedAt) : null;
const durationMs =
started && finished ? finished.getTime() - started.getTime() : null;
lines.push(`=== DAG run ${run.ID} ===`);
lines.push(`dag: ${run.DagName}`);
lines.push(`path: ${run.DagPath}`);
lines.push(`status: ${run.Status}`);
lines.push(`trigger: ${run.Trigger}`);
lines.push(`started: ${started ? started.toISOString() : "-"}`);
lines.push(`finished: ${finished ? finished.toISOString() : "-"}`);
lines.push(
`duration: ${durationMs !== null ? `${durationMs} ms` : "running..."}`
);
if (run.Error) {
lines.push("");
lines.push("run error:");
lines.push(run.Error);
}
lines.push("");
lines.push(`--- steps (${steps.length}) ---`);
for (const s of steps) {
lines.push("");
lines.push(
`[${s.Status}] ${s.StepName} exit=${s.ExitCode} ${s.DurationMs}ms`
);
if (s.StartedAt) lines.push(` started: ${s.StartedAt}`);
if (s.FinishedAt) lines.push(` finished: ${s.FinishedAt}`);
if (s.Error) {
lines.push(" error:");
lines.push(s.Error.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stdout) {
lines.push(" stdout:");
lines.push(s.Stdout.split("\n").map((l) => " " + l).join("\n"));
}
if (s.Stderr) {
lines.push(" stderr:");
lines.push(s.Stderr.split("\n").map((l) => " " + l).join("\n"));
}
}
return lines.join("\n");
}
export function RunDetail() {
const { id } = useParams<{ id: string }>();
@@ -100,6 +151,41 @@ export function RunDetail() {
</Text>
)}
</Paper>
<Paper p="md" withBorder>
<Group justify="space-between" mb="sm">
<Title order={4}>Logs</Title>
<CopyButton value={buildLogText(run, steps || [])} timeout={1500}>
{({ copied, copy }) => (
<Tooltip
label={copied ? "Copiado" : "Copiar log completo"}
withArrow
position="left"
>
<ActionIcon
variant={copied ? "filled" : "light"}
color={copied ? "teal" : "blue"}
onClick={copy}
aria-label="Copiar logs"
>
{copied ? <IconCheck size={16} /> : <IconCopy size={16} />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
</Group>
<Code
block
style={{
maxHeight: 480,
overflow: "auto",
whiteSpace: "pre",
fontSize: 12,
}}
>
{buildLogText(run, steps || [])}
</Code>
</Paper>
</Stack>
);
}