commit 1d4eebca54cecd4357653a9411036a43c491ef75 Author: rikrdo Date: Fri Aug 14 21:46:54 2026 +0200 feat(F-001): scaffold modular monolith skeleton with boundary checker - TypeScript + Fastify skeleton under project/ (src/modules, shared, infrastructure, app) - scripts/check-module-boundaries.mjs enforcing module public-API rules (tested with fixtures) - GET /health endpoint, error envelope without stack leakage - specs/F-001-scaffold (SPEC/DESIGN/TASKS/TESTS), spec/tech.md dependency justification - 30-ticket MercadoDeVida roadmap in backlog/features.json, spec/roadmap.md - All gates approved: reviewer, security, qa; verify.sh green diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0ea7ff6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ + +# BEGIN ORQUESTRA +__pycache__/ +*.pyc +.pytest_cache/ +.codegraph/ +.atl/ +# END ORQUESTRA diff --git a/.pi/extensions/orquestra-status/README.md b/.pi/extensions/orquestra-status/README.md new file mode 100644 index 0000000..5fceac6 --- /dev/null +++ b/.pi/extensions/orquestra-status/README.md @@ -0,0 +1,42 @@ +# Orquestra Status Extension + +Pi extension project-local que renderiza `work/runtime-status.json` y aplica el guard mínimo de escritura Orquestra. + +## Muestra +- feature activa +- stage actual +- rol actual +- acción/estado +- siguiente rol +- checklist de artefactos por rol +- estado de gates +- último evento de timeline + +## Ruta instalada +- `.pi/extensions/orquestra-status/index.ts` + +Se lanza junto con `.pi/extensions/orquestra-web-fetch.ts` desde `scripts/pi_orquestra.sh`. + +## Uso +1. Abrí `pi` en la raíz del proyecto instalado. +2. Si Pi ya estaba abierto, ejecutá `/reload`. +3. El widget aparece arriba del editor y el footer muestra estado compacto. +4. El widget refresca cada segundo. +5. Refresco manual: `/orquestra-status`. + +## Guard de escritura + +La extensión intercepta herramientas Pi built-in `write` y `edit`: +- bloquea archivos de producto/código en la raíz (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`); usá `project/`. +- bloquea escrituras en `project/` o `tests/` salvo que `work/runtime-status.json` tenga `feature_id`, `stage=build`, `agent=implementer` y `state=running`. +- bloquea paths que escapan la raíz del repo con `..`. + +## Fuente de verdad +- `work/runtime-status.json` + +Actualizar con: + +```bash +python3 scripts/agent_status.py set ... +python3 scripts/agent_status.py reset +``` diff --git a/.pi/extensions/orquestra-status/index.ts b/.pi/extensions/orquestra-status/index.ts new file mode 100644 index 0000000..30efef9 --- /dev/null +++ b/.pi/extensions/orquestra-status/index.ts @@ -0,0 +1,291 @@ +import { readFileSync, watchFile, unwatchFile } from "node:fs"; +import { join, relative, resolve, sep } from "node:path"; +import type { ExtensionAPI, ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-agent"; + +const STATUS_FILE = "work/runtime-status.json"; +const MATRIX_FILE = "harness/agents.matrix.yml"; +const ARTIFACTS_DIR = "work/artifacts"; +const WIDGET_KEY = "orquestra-runtime"; +const STATUS_KEY = "orquestra-runtime"; + +const DEFAULT_EMOJIS: Record = { + leader: "🧭", + architect: "🏗️", + implementer: "🛠️", + reviewer: "🔍", + security: "🔒", + qa: "🧪", + documenter: "📝", +}; + +const GATE_FILES: Record = { + reviewer: "reviewer.json", + security: "security.json", + qa: "qa.json", + leader: "leader-close.json", +}; + +const AGENT_FILES: Record = { + architect: "architect.md", + implementer: "implementer.md", + reviewer: "reviewer.json", + security: "security.json", + qa: "qa.json", + documenter: "documenter.md", + leader: "leader-close.json", +}; + +type RuntimeStatus = { + feature_id?: string | null; + stage?: string; + agent?: string; + action?: string; + state?: string; + next_agent?: string; + waiting_for?: string; + updated_at?: string; + timeline?: Array<{ ts?: string; agent?: string; stage?: string; state?: string; message?: string }>; +}; + +const ROOT_PRODUCT_EXTENSIONS = new Set([".py", ".js", ".ts", ".go", ".rs", ".java", ".php", ".rb"]); + +function safeReadJson(root: string, path: string): RuntimeStatus | null { + try { + return JSON.parse(readFileSync(join(root, path), "utf8")) as RuntimeStatus; + } catch { + return null; + } +} + +function loadRoleEmojis(root: string): Record { + try { + const text = readFileSync(join(root, MATRIX_FILE), "utf8"); + const emojis = { ...DEFAULT_EMOJIS }; + let currentRole: string | null = null; + for (const line of text.split(/\r?\n/)) { + const roleMatch = line.match(/^ ([a-z_]+):\s*$/); + if (roleMatch) { + currentRole = roleMatch[1]; + continue; + } + const emojiMatch = line.match(/^\s{4}emoji:\s*["']?(.*?)["']?\s*$/); + if (emojiMatch && currentRole) emojis[currentRole] = emojiMatch[1]; + } + return emojis; + } catch { + return { ...DEFAULT_EMOJIS }; + } +} + +function gateState(root: string, featureId?: string | null): Record { + const result: Record = {}; + if (!featureId) return result; + for (const [gate, filename] of Object.entries(GATE_FILES)) { + try { + if (filename.endsWith(".md")) { + readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8"); + result[gate] = "APPROVED"; + continue; + } + const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string }; + result[gate] = json.verdict === "APPROVED" ? "APPROVED" : "PRESENT"; + } catch { + result[gate] = "PENDING"; + } + } + return result; +} + +function gateSummary(root: string, featureId: string | null | undefined, emojis: Record): string { + const gates = gateState(root, featureId); + if (!featureId) return "Gates: —"; + return `Gates: ${["reviewer", "security", "qa", "leader"] + .map((gate) => `${emojis[gate] || "•"}${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`) + .join(" ")}`; +} + +function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string { + if (!featureId) return "PENDING"; + const filename = AGENT_FILES[agent]; + if (!filename) return "PENDING"; + try { + if (filename.endsWith(".md")) { + readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8"); + return "DONE"; + } + const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string }; + return json.verdict === "APPROVED" ? "DONE" : "PRESENT"; + } catch { + return "PENDING"; + } +} + +function agentSummary(root: string, status: RuntimeStatus, emojis: Record): string { + const featureId = status.feature_id || null; + const currentAgent = status.agent || "leader"; + return `Roles: ${["architect", "implementer", "reviewer", "security", "qa", "documenter", "leader"] + .map((agent) => { + const state = artifactStatus(root, featureId, agent); + const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳"; + return `${emojis[agent] || "•"}${mark}`; + }) + .join(" ")}`; +} + +function isInsideRel(relPath: string, dirname: string): boolean { + return relPath === dirname || relPath.startsWith(`${dirname}/`) || relPath.startsWith(`${dirname}${sep}`); +} + +function normalizeWritePath(root: string, rawPath: unknown): { relPath?: string; reason?: string } { + if (typeof rawPath !== "string" || rawPath.trim() === "") { + return { reason: "Orquestra bloqueó una escritura sin path válido." }; + } + + const absPath = resolve(root, rawPath); + const relPath = relative(root, absPath); + if (relPath === "" || relPath.startsWith("..") || resolve(root, relPath) !== absPath) { + return { reason: "Orquestra bloqueó una escritura fuera de la raíz del proyecto." }; + } + return { relPath: relPath.replace(/\\/g, "/") }; +} + +function isRootProductFile(relPath: string): boolean { + if (relPath.includes("/")) return false; + const dot = relPath.lastIndexOf("."); + return dot > 0 && ROOT_PRODUCT_EXTENSIONS.has(relPath.slice(dot)); +} + +function hasActiveBuildRuntime(status: RuntimeStatus | null): boolean { + return Boolean( + status?.feature_id && + status.stage === "build" && + status.agent === "implementer" && + status.state === "running", + ); +} + +function guardWriteOrEdit(event: ToolCallEvent, ctx: ExtensionContext): { block?: boolean; reason?: string } | undefined { + if (event.toolName !== "write" && event.toolName !== "edit") return undefined; + + const { relPath, reason } = normalizeWritePath(ctx.cwd, (event.input as { path?: unknown }).path); + if (!relPath) return { block: true, reason }; + + if (isRootProductFile(relPath)) { + return { + block: true, + reason: `Orquestra bloqueó '${relPath}': los archivos de producto/código no van en la raíz. Escribí ese código dentro de project/.`, + }; + } + + if (isInsideRel(relPath, "project") || isInsideRel(relPath, "tests")) { + const status = safeReadJson(ctx.cwd, STATUS_FILE); + if (!hasActiveBuildRuntime(status)) { + return { + block: true, + reason: + "Orquestra bloqueó esta escritura: los cambios en project/ o tests/ requieren una feature/ticket activa, stage build, agent implementer y state running. Seleccioná o creá un ticket y actualizá work/runtime-status.json con scripts/agent_status.py antes de escribir.", + }; + } + } + + return undefined; +} + +function render(root: string, status: RuntimeStatus | null, emojis: Record) { + if (!status) { + return { + widget: ["Orquestra: no se pudo leer work/runtime-status.json"], + footer: "Orquestra status unavailable", + }; + } + + const feature = status.feature_id || "—"; + const stage = status.stage || "—"; + const agent = status.agent || "leader"; + const action = status.action || "—"; + const state = status.state || "—"; + const nextAgent = status.next_agent || "—"; + const waitingFor = status.waiting_for || "—"; + const updatedAt = status.updated_at || "—"; + const lastEvent = status.timeline && status.timeline.length > 0 ? status.timeline[status.timeline.length - 1] : null; + const lastLine = lastEvent + ? `Último: ${lastEvent.ts || "—"} · ${emojis[lastEvent.agent || "leader"] || "•"} ${lastEvent.message || "—"}` + : "Último: —"; + + return { + widget: [ + `Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${agent} · ${state}`, + `Acción: ${action}`, + `Siguiente: ${emojis[nextAgent] || "•"} ${nextAgent} · Esperando: ${waitingFor}`, + agentSummary(root, status, emojis), + gateSummary(root, status.feature_id || null, emojis), + lastLine, + `Actualizado: ${updatedAt}`, + ], + footer: `Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${state}`, + }; +} + +export default function orquestraStatusExtension(pi: ExtensionAPI) { + let currentCtx: ExtensionContext | null = null; + let watching = false; + let refreshTimer: ReturnType | null = null; + let lastRendered = ""; + + function apply(ctx: ExtensionContext) { + const root = ctx.cwd; + const emojis = loadRoleEmojis(root); + const rendered = render(root, safeReadJson(root, STATUS_FILE), emojis); + const signature = JSON.stringify(rendered); + if (signature === lastRendered) return; + lastRendered = signature; + ctx.ui.setWidget(WIDGET_KEY, rendered.widget); + ctx.ui.setStatus(STATUS_KEY, rendered.footer); + } + + function refresh() { + if (!currentCtx || !currentCtx.hasUI) return; + apply(currentCtx); + } + + function startWatching() { + if (watching) return; + if (!currentCtx) return; + watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000 }, () => refresh()); + refreshTimer = setInterval(refresh, 1000); + watching = true; + } + + function stopWatching() { + if (!watching) return; + if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE)); + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = null; + watching = false; + } + + pi.on("session_start", async (_event, ctx) => { + if (!ctx.hasUI) return; + currentCtx = ctx; + apply(ctx); + startWatching(); + }); + + pi.on("session_shutdown", async (_event, ctx) => { + ctx.ui.setWidget(WIDGET_KEY, undefined); + ctx.ui.setStatus(STATUS_KEY, undefined); + stopWatching(); + currentCtx = null; + }); + + pi.on("tool_call", async (event, ctx) => guardWriteOrEdit(event, ctx)); + + pi.registerCommand("orquestra-status", { + description: "Refresca el widget de runtime status de Orquestra", + handler: async (_args, ctx) => { + currentCtx = ctx; + apply(ctx); + ctx.ui.notify("Orquestra status refrescado", "info"); + }, + }); +} diff --git a/.pi/extensions/orquestra-web-fetch.ts b/.pi/extensions/orquestra-web-fetch.ts new file mode 100644 index 0000000..595a78d --- /dev/null +++ b/.pi/extensions/orquestra-web-fetch.ts @@ -0,0 +1,56 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; + +function textFromHtml(html: string): { title: string; content: string } { + const title = html.match(/]*>([^<]+)<\/title>/i)?.[1]?.trim() || "No title"; + const content = html + .replace(/]*>[\s\S]*?<\/script>/gi, " ") + .replace(/]*>[\s\S]*?<\/style>/gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/\s+/g, " ") + .trim(); + return { title, content }; +} + +async function fetchText(url: string, signal?: AbortSignal) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + const activeSignal = signal ?? controller.signal; + try { + const response = await fetch(url, { + signal: activeSignal, + headers: { + "User-Agent": "Mozilla/5.0 Orquestra/1.0", + }, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return textFromHtml(await response.text()); + } finally { + clearTimeout(timeout); + } +} + +export default function orquestraWebFetch(pi: ExtensionAPI) { + pi.registerTool({ + name: "orquestra_web_fetch", + label: "Orquestra Web Fetch", + description: "Fetch a web page and return extracted text using a simple non-browser fetch.", + parameters: Type.Object({ + url: Type.String({ description: "URL to fetch" }), + limit: Type.Optional(Type.Integer({ minimum: 500, maximum: 20000, description: "Max characters to return" })), + }), + async execute(_toolCallId, params, signal) { + const { title, content } = await fetchText(params.url, signal); + const limit = params.limit ?? 8000; + return { + content: [{ type: "text", text: `Title: ${title}\n\nContent:\n${content.slice(0, limit)}` }], + details: { title, url: params.url, characters: content.length }, + }; + }, + }); +} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..922e9c5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,22 @@ +# AGENTS.md — Entrada de Orquestra + +## Arranque obligatorio +1. Ejecutar `./scripts/verify.sh`. +2. Leer `work/current.md`. +3. Leer `backlog/features.json` y seleccionar **una** feature `pending`. +4. Seguir `harness/workflow.stages.yml` y `harness/agents.matrix.yml`. +5. Actualizar estado visible con `python3 scripts/agent_status.py set ...` antes de cada stage. + +## Reglas duras +- Orquestra se ejecuta secuencialmente en Pi: no subagentes. +- Para Pi limpio, arrancar con `./scripts/pi_orquestra.sh`, no con `pi` directo. +- Una sola feature en `in_progress`. +- Ningún rol pasa código por chat: todo va a `work/artifacts//`. +- `implementer` nunca marca `done`. +- `done` requiere gates aprobados: `reviewer`, `security`, `qa`. +- Si `verify.sh` falla, no se cierra la feature. + +## Reentrada +- Releer `work/current.md`, `work/runtime-status.json` y artefactos de la feature activa. +- Ejecutar `./scripts/verify.sh`. +- Continuar desde el próximo stage pendiente. diff --git a/CHECKPOINTS.md b/CHECKPOINTS.md new file mode 100644 index 0000000..5981e10 --- /dev/null +++ b/CHECKPOINTS.md @@ -0,0 +1,21 @@ +# CHECKPOINTS + +## C1 — Estructura +- [ ] Existe `harness/`, `spec/`, `backlog/`, `work/`, `scripts/`, `platforms/`. + +## C2 — Estado +- [ ] Máximo una feature en `in_progress`. +- [ ] Estados válidos en backlog. + +## C3 — Gates +- [ ] Toda feature `done` tiene `reviewer.json` aprobado. +- [ ] Toda feature `done` tiene `security.json` aprobado. +- [ ] Toda feature `done` tiene `qa.json` aprobado. +- [ ] Toda feature `done` tiene `leader-close.json` válido. + +## C4 — Verificación +- [ ] `./scripts/verify.sh` termina en OK. + +## C5 — Trazabilidad +- [ ] `work/history.md` actualizado en cierre. +- [ ] Artefactos por feature en `work/artifacts//`. diff --git a/HOWTO.md b/HOWTO.md new file mode 100644 index 0000000..ce48659 --- /dev/null +++ b/HOWTO.md @@ -0,0 +1,93 @@ +# HOWTO — usar Orquestra + +## 1) Requisitos + +```bash +command -v pi +command -v python3 +``` + +Si `pi` no existe, no instales Orquestra todavía. + +## 2) Instalar en un proyecto + +Desde el repo fuente de Orquestra: + +```bash +./scripts/install.sh /path/to/project-repo +``` + +Para actualizar, ejecutá el mismo comando otra vez sobre el repo destino. + +La instalación es safe-update: +- crear archivos faltantes +- conservar o mergear archivos existentes +- crear `project/` si falta y no pisar su contenido +- no pisar progreso en `project/`, `work/`, `backlog/`, `spec/` ni `work/artifacts/` + +## 3) Verificar + +```bash +./scripts/verify.sh +``` + +Debe comprobar: +- estructura mínima +- `project/` existente +- ausencia de archivos de producto/código en la raíz (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`); usá `project/` +- Pi instalado +- extensión project-local permitida +- sin `.pi/subagents/` ni `.pi/subagents.json` +- extensiones Orquestra requeridas +- backlog válido +- runtime status válido + +## 4) Ejecutar desde Pi + +```bash +cd +./scripts/pi_orquestra.sh +``` + +Ese launcher usa `pi --no-extensions` y carga solo `orquestra-status` + `orquestra-web-fetch`, así Pi arranca limpio con las extensiones del proyecto. + +Después de abrir o recargar Pi: + +```text +/orquestra-status +``` + +## 5) Flujo secuencial + +1. `leader` selecciona una feature pending. +2. `architect` diseña si hace falta. +3. `implementer` implementa y escribe `implementer.md`. +4. `reviewer` escribe `reviewer.json`. +5. `security` escribe `security.json`. +6. `qa` escribe `qa.json`. +7. `documenter` escribe `documenter.md` solo si cambiaron docs/API/contratos/comportamiento user-facing. +8. `leader` cierra con `leader-close.json` y `work/history.md`. + +Un stage empieza solo cuando el anterior terminó con artefacto en disco; `document` es opcional/condicional y no bloquea el cierre por defecto. + +## 6) Dónde va el código de producto + +El código del producto vive en `project/`. No escribas archivos de producto/código en la raíz del repo. + +En Pi, las herramientas `write` y `edit` solo pueden modificar `project/` o `tests/` cuando `work/runtime-status.json` tiene una feature activa con `stage=build`, `agent=implementer` y `state=running`. Prepará el stage con: + +```bash +python3 scripts/agent_status.py set --feature-id F-001 --stage build --agent implementer --state running --action "Implementing" +``` + +## 7) Estado visible + +```bash +python3 scripts/agent_status.py show +python3 scripts/agent_status.py set --feature-id F-001 --stage build --agent implementer --state running --action "Implementing" +python3 scripts/agent_status.py reset +``` + +## Regla corta + +Pi instalado → Orquestra instalado sin pisar progreso → producto dentro de `project/` → `verify.sh` verde → `./scripts/pi_orquestra.sh` desde raíz → stages secuenciales con evidencia en disco. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0e84a46 --- /dev/null +++ b/README.md @@ -0,0 +1,161 @@ +# Orquestra — harness secuencial para Pi + +Orquestra es un harness in-house para instalar en cualquier repo de proyecto y ejecutarlo desde Pi con control de estado, evidencias y gates. + +No instala subagentes. El flujo es secuencial: termina un rol/stage, se escribe su artefacto, recién ahí empieza el siguiente. + +## Requisitos + +- `pi` instalado y disponible en `PATH` antes de ejecutar Orquestra. +- `python3` disponible para scripts del harness. +- Ejecutar Pi desde la raíz del proyecto. +- Arrancar con `./scripts/pi_orquestra.sh` para usar `pi --no-extensions` y cargar solo extensiones Orquestra. +- Extensiones Pi project-local declaradas: `orquestra-status` y `orquestra-web-fetch.ts`. + +> Nota honesta: abrir `pi` directo puede cargar extensiones globales. Para Pi limpio, usá `./scripts/pi_orquestra.sh`. + +## Objetivo + +Permitir trabajo asistido por agentes sin perder control: +- una feature a la vez +- estado persistente en disco +- evidencia auditable, nunca solo chat +- separación de roles +- gates obligatorios de revisión, seguridad y QA +- documentación opcional cuando cambian docs/API/contratos/comportamiento user-facing +- código de producto dentro de `project/` (nunca archivos de código en la raíz) +- cierre solo con `./scripts/verify.sh` en verde + +## Roles secuenciales + +1. `leader` — selecciona feature, orquesta, cierra. +2. `architect` — diseño/contratos cuando haga falta. +3. `implementer` — cambia código y tests, no aprueba. +4. `reviewer` — gate técnico. +5. `security` — gate de seguridad. +6. `qa` — gate funcional/aceptación. +7. `documenter` — documentación opcional cuando aplique. + +Los modelos por rol se definen en `harness/model-routing.yml`; el cambio de modelo ocurre antes de cada stage, nunca en paralelo. + +## Pipeline + +1. `intake` → `leader` +2. `design` → `architect` opcional +3. `build` → `implementer` +4. `review_gate` → `reviewer` +5. `security_gate` → `security` +6. `qa_gate` → `qa` +7. `document` → `documenter` opcional/condicional +8. `close` → `leader` + +No hay `done` si falta cualquier gate obligatorio; `documenter.md` no es requisito de cierre salvo que el cambio necesite documentación. + +## Evidencia obligatoria + +Cada stage escribe en disco: +- `work/artifacts//implementer.md` +- `work/artifacts//reviewer.json` +- `work/artifacts//security.json` +- `work/artifacts//qa.json` +- `work/artifacts//documenter.md` (opcional/condicional) +- `work/artifacts//leader-close.json` + +Respuesta estándar por stage: +- `done -> ` +- `blocked -> ` + +## Estructura mínima + +```text +. +├── AGENTS.md +├── README.md +├── harness/ +│ ├── agents.matrix.yml +│ ├── workflow.stages.yml +│ ├── model-routing.yml +│ ├── policies/ +│ └── contracts/ +├── platforms/pi/ +│ └── extensions/ +│ ├── orquestra-status/ +│ └── orquestra-web-fetch.ts +├── spec/ +├── project/ +├── backlog/features.json +├── work/ +│ ├── current.md +│ ├── history.md +│ ├── runtime-status.json +│ └── artifacts/ +└── scripts/ + ├── verify.sh + ├── agent_status.py + ├── new_ticket.py + └── pi_orquestra.sh +``` + +## Instalación y actualización segura + +Desde el repo fuente de Orquestra: + +```bash +./scripts/install.sh /path/to/project-repo +``` + +Para actualizar, volvé a ejecutar el mismo comando sobre el repo destino. + +Regla base: +- crear si falta +- conservar o mergear si existe +- nunca pisar `work/`, `backlog/`, `spec/` ni artefactos ya producidos + +Archivos del harness actualizables: `AGENTS.md`, `README.md`, `HOWTO.md`, `CHECKPOINTS.md`, `harness/`, `scripts/`, `platforms/pi/`. +Datos del proyecto: `project/`, `work/`, `backlog/`, `spec/`. + +El instalador crea `project/` si falta y no pisa su contenido. Los archivos de producto/código (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`) son inválidos en la raíz del repo; `./scripts/verify.sh` falla si los encuentra. Durante una sesión Pi, escribir en `project/` o `tests/` requiere una feature activa con `stage=build`, `agent=implementer` y `state=running` en `work/runtime-status.json`. + +## Pi + +Las extensiones Orquestra esperadas son: + +```text +.pi/extensions/orquestra-status/index.ts +.pi/extensions/orquestra-web-fetch.ts +``` + +Comando manual: + +```bash +./scripts/pi_orquestra.sh +# dentro de Pi: +/orquestra-status +``` + +Fuente de verdad: + +```bash +python3 scripts/agent_status.py show +python3 scripts/agent_status.py set ... +python3 scripts/agent_status.py reset +``` + +## Verificación + +```bash +./scripts/verify.sh +``` + +Comprueba: +- estructura mínima +- `pi` instalado +- launcher limpio `scripts/pi_orquestra.sh` +- extensiones requeridas: `orquestra-status` y `orquestra-web-fetch` +- ausencia de subagentes project-local +- precondiciones de stage en `agent_status.py` +- extensiones project-local no declaradas +- backlog y gates +- `work/runtime-status.json` +- `project/` existente y sin archivos de producto/código en la raíz +- suite del proyecto si existe diff --git a/backlog/features.json b/backlog/features.json new file mode 100644 index 0000000..c362b5b --- /dev/null +++ b/backlog/features.json @@ -0,0 +1,1143 @@ +{ + "project": "mercadodevida-vnext", + "description": "Incremental SDD roadmap for MercadoDeVida vNext (modular monolith). Derived from project/design_prompt.md. One feature at a time, spec before code, gates before done.", + "rules": { + "one_feature_at_a_time": true, + "require_review_gate": true, + "require_security_gate": true, + "require_qa_gate": true, + "valid_status": [ + "pending", + "in_progress", + "blocked", + "done" + ], + "valid_types": [ + "feature", + "fix", + "bug", + "chore" + ] + }, + "features": [ + { + "id": "F-001", + "type": "chore", + "title": "Scaffold modular monolith skeleton", + "problem": "No codebase exists. Platform needs a boring, typed, modular home.", + "goal": "TypeScript modular monolith skeleton with strict module boundaries and green toolchain.", + "scope_in": [ + "project/ layout: src/modules, src/shared, src/infrastructure, src/app", + "Node + TypeScript strict + Fastify app shell", + "ESLint + Prettier + typecheck + unit test runner", + "GET /health endpoint", + "Boundary lint rule: modules import only own folder or other modules public index" + ], + "scope_out": [ + "No business logic", + "No database", + "No frontend" + ], + "priority": "high", + "risk": "low", + "depends_on": [], + "description": "Problem: No codebase exists. Platform needs a boring, typed, modular home. Goal: TypeScript modular monolith skeleton with strict module boundaries and green toolchain. Scope IN: project/ layout, Fastify + TS strict, toolchain, health endpoint, boundary rule. Scope OUT: no business logic, no DB. Type: chore. Priority: high. Risk: low.", + "acceptance": [ + "install, build, lint, typecheck and test commands all green", + "GET /health returns HTTP 200 with status ok", + "src/modules, src/shared, src/infrastructure, src/app exist", + "A module importing another module internal file fails lint", + "verify.sh green" + ], + "status": "done", + "created_at": "2026-08-14", + "gates": { + "review": true, + "security": true, + "qa": true + } + }, + { + "id": "F-002", + "type": "chore", + "title": "Database foundation with module-owned schemas", + "problem": "Modules need PostgreSQL with clear ownership and safe migrations.", + "goal": "Migrations tooling, table naming convention per module, local dev database.", + "scope_in": [ + "SQL migration tool (up/down, ordered, idempotent)", + "Naming convention: _ (catalog_products, inventory_stock, ...)", + "docker-compose dev services: PostgreSQL + Redis", + "Migration test: fresh up then down then up", + "Rule: no schema change without migration" + ], + "scope_out": [ + "No business tables yet", + "No Redis usage beyond availability" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-001" + ], + "description": "Problem: Modules need PostgreSQL with clear ownership and safe migrations. Goal: Migrations tooling, naming convention, local dev database. Scope IN: migration tool, naming convention, docker-compose, migration test. Scope OUT: no business tables. Type: chore. Priority: high. Risk: low.", + "acceptance": [ + "Given a fresh database When migrations run Then schema is created and repeat run is a no-op", + "Given applied migrations When down runs Then schema rolls back cleanly", + "Table naming convention documented and enforced", + "Dev PostgreSQL and Redis start with one command", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-003", + "type": "chore", + "title": "HTTP foundation and request context", + "problem": "Every request needs identity, structured logs and predictable errors.", + "goal": "request_id everywhere, JSON logs, one error envelope, no magic.", + "scope_in": [ + "request_id generated or propagated on every request", + "Structured JSON logging with request_id", + "Single error response envelope", + "Input validation hook on API layer" + ], + "scope_out": [ + "No tracing backend yet", + "No metrics yet" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-001" + ], + "description": "Problem: Every request needs identity, structured logs and predictable errors. Goal: request_id, JSON logs, error envelope. Scope IN: request context, logging, error envelope, validation hook. Scope OUT: no tracing or metrics backend. Type: chore. Priority: high. Risk: low.", + "acceptance": [ + "Every response carries request_id", + "Every log line for a request carries the same request_id", + "Given an invalid request When handled Then error envelope shape is stable", + "Internal stack traces never leak to API responses", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-004", + "type": "chore", + "title": "Typed config and feature flags", + "problem": "Risky features need activation separate from deployment; env access must be typed.", + "goal": "Fail-fast typed config plus simple feature flag module behind an interface.", + "scope_in": [ + "Typed env config loader, fail fast on missing required vars", + "FeatureFlag interface with simple store implementation", + "Flags: deployment and activation are separate operations" + ], + "scope_out": [ + "No external flag service", + "No per-user segmentation yet" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-001" + ], + "description": "Problem: Risky features need activation separate from deployment; env access must be typed. Goal: typed config + feature flag module. Scope IN: config loader, FeatureFlag interface, simple store. Scope OUT: no external flag service. Type: chore. Priority: med. Risk: low.", + "acceptance": [ + "Given a missing required env var When app starts Then startup fails with clear message", + "Given flag off When code path guarded by flag runs Then path is skipped", + "Flag state change does not require redeploy", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-005", + "type": "feature", + "title": "Identity: register, login, sessions", + "problem": "Customers need accounts; nothing trusts who calls the API.", + "goal": "Registration, login, logout with hashed passwords and secure sessions.", + "scope_in": [ + "identity module: domain, application, infrastructure, api", + "Register, login, logout use cases", + "Argon2 password hashing", + "Secure session cookie: HttpOnly, Secure, SameSite", + "Login rate limiting" + ], + "scope_out": [ + "No MFA yet", + "No OAuth providers", + "No profile editing (users module)" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-002", + "F-003" + ], + "description": "Problem: Customers need accounts; nothing trusts who calls the API. Goal: register/login/logout with hashed passwords and secure sessions. Scope IN: identity module, argon2, secure cookies, rate limit. Scope OUT: no MFA, no OAuth. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Given valid credentials When login Then HTTP 200 and secure session cookie set", + "Given wrong password When login Then HTTP 401 and no user enumeration hint", + "Passwords stored with argon2, never plaintext or reversible", + "Given 10 failed logins in a row When next login attempted Then HTTP 429", + "Session cookie is HttpOnly, Secure and SameSite", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-006", + "type": "feature", + "title": "Users: profile, addresses, RBAC", + "problem": "Authenticated users need profile data, addresses and clear roles.", + "goal": "Users module with profile + address CRUD and customer/admin roles.", + "scope_in": [ + "users module owning identity_users profile data and addresses", + "Profile and address CRUD behind use cases", + "Roles: customer, admin; RBAC guard on API layer", + "User can read/update only own data" + ], + "scope_out": [ + "No fine-grained permissions yet", + "No admin UI" + ], + "priority": "med", + "risk": "med", + "depends_on": [ + "F-005" + ], + "description": "Problem: Authenticated users need profile data, addresses and clear roles. Goal: users module with profile, addresses, RBAC. Scope IN: users module, CRUD, roles, ownership guard. Scope OUT: no admin UI. Type: feature. Priority: med. Risk: med.", + "acceptance": [ + "Given user A When A requests user B profile Then HTTP 403", + "Given customer role When admin-only endpoint called Then HTTP 403", + "Given admin role When admin-only endpoint called Then HTTP 200", + "Address CRUD works end to end for own addresses", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-007", + "type": "feature", + "title": "Categories module", + "problem": "Products need a navigable taxonomy with stable SEO URLs.", + "goal": "Category tree with unique slugs and SEO metadata.", + "scope_in": [ + "categories module with tree structure", + "Unique slug per category, stable URL /categoria/", + "SEO metadata fields: title, description", + "Category CRUD via use cases" + ], + "scope_out": [ + "No product assignment yet (catalog core ticket)", + "No storefront rendering yet" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-002" + ], + "description": "Problem: Products need a navigable taxonomy with stable SEO URLs. Goal: category tree with slugs and SEO metadata. Scope IN: categories module, tree, slugs, metadata. Scope OUT: no product assignment, no storefront. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Given duplicate slug When category created Then HTTP 409", + "Category tree supports parent/child and blocks cycles", + "Public URL is /categoria/, never internal id", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-008", + "type": "feature", + "title": "Catalog core: products domain", + "problem": "There is no product model; everything else depends on it.", + "goal": "Product aggregate with use cases, slugs, states and SEO metadata.", + "scope_in": [ + "catalog module: domain, application, infrastructure, api", + "CreateProduct, UpdateProduct, SearchProducts use cases", + "Product states: draft, active, archived", + "Unique slug, stable URL /productos/", + "SEO metadata per product", + "Product-category assignment" + ], + "scope_out": [ + "No variants yet", + "No stock, no prices" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-002", + "F-007" + ], + "description": "Problem: There is no product model; everything else depends on it. Goal: product aggregate with use cases, slugs, states, SEO metadata. Scope IN: catalog module, CRUD use cases, states, slugs, category assignment. Scope OUT: no variants, stock, prices. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Domain layer has zero database or HTTP imports", + "Given duplicate slug When product created Then HTTP 409", + "Only active products appear in public listings", + "Public URL is /productos/, never internal id", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-009", + "type": "feature", + "title": "Brands module", + "problem": "Products need brands for navigation, filtering and SEO pages.", + "goal": "Brand entity with slug and SEO page data.", + "scope_in": [ + "brands module or catalog-owned brands with clear ownership", + "Unique slug, stable URL /marca/", + "Product-brand assignment" + ], + "scope_out": [ + "No brand storefront page yet" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-002", + "F-008" + ], + "description": "Problem: Products need brands for navigation, filtering and SEO pages. Goal: brand entity with slug and SEO data. Scope IN: brands, slugs, product assignment. Scope OUT: no storefront page yet. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Given duplicate brand slug When created Then HTTP 409", + "Products list filterable by brand", + "Public URL is /marca/", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-010", + "type": "feature", + "title": "Variants, SKU/EAN and product rich data", + "problem": "One product has many sellable variants; nutrition and allergens must be trusted data.", + "goal": "Variants with SKU/EAN plus attributes, ingredients, allergens, nutrition with provenance.", + "scope_in": [ + "Product variants with unique SKU and EAN", + "Attributes per variant", + "Ingredients, allergens, nutrition information", + "Organic/ecological certification fields", + "Provenance tracking: manual, manufacturer, openfoodfacts", + "External source never overwrites trusted internal data without validation" + ], + "scope_out": [ + "No OpenFoodFacts sync job yet", + "No images yet" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-008" + ], + "description": "Problem: One product has many sellable variants; nutrition and allergens must be trusted data. Goal: variants with SKU/EAN, attributes, ingredients, allergens, nutrition with provenance. Scope IN: variants, rich data, provenance rules. Scope OUT: no OpenFoodFacts sync. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Given duplicate SKU or EAN When variant created Then HTTP 409", + "Given field with nutrition_source=manual When external source pushes same field Then internal value kept", + "Every nutrition payload stores its provenance", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-011", + "type": "feature", + "title": "Product images", + "problem": "Products need images for storefront and SEO.", + "goal": "Image attachment per product/variant behind a storage interface.", + "scope_in": [ + "Image entity: url, alt text, ordering, role (main/gallery)", + "Storage adapter behind interface (local first)", + "Attach/detach/reorder via use cases" + ], + "scope_out": [ + "No CDN or image processing pipeline yet" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-010" + ], + "description": "Problem: Products need images for storefront and SEO. Goal: image attachment per product/variant behind a storage interface. Scope IN: image entity, storage adapter, ordering. Scope OUT: no CDN pipeline. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Product exposes ordered image list with alt text", + "Swapping storage adapter touches only infrastructure layer", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-012", + "type": "feature", + "title": "Search: interface plus PostgreSQL FTS", + "problem": "Customers must find products; search backend must stay replaceable.", + "goal": "ProductSearch interface with PostgreSQL full text search implementation.", + "scope_in": [ + "ProductSearch interface", + "PostgreSQL FTS implementation over name, brand, category", + "Basic relevance ordering and pagination", + "Popular searches logged for later cache use" + ], + "scope_out": [ + "No Elasticsearch/Meilisearch/Algolia yet", + "No typo tolerance beyond FTS defaults" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-008", + "F-009" + ], + "description": "Problem: Customers must find products; search backend must stay replaceable. Goal: ProductSearch interface with PostgreSQL FTS implementation. Scope IN: interface, FTS adapter, pagination. Scope OUT: no external search engine. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Given product matching query When search Then product returned with stable ordering", + "Swapping implementation requires no API contract change", + "Search latency measured and logged", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-013", + "type": "feature", + "title": "Storefront shell (Next.js)", + "problem": "There is no customer-facing site.", + "goal": "Next.js + TypeScript + Tailwind shell consuming backend public API.", + "scope_in": [ + "Next.js app with React, TypeScript, Tailwind", + "Layout, navigation, home page", + "Server Components by default; client code only where interaction needs it", + "Typed API client for backend public endpoints" + ], + "scope_out": [ + "No catalog pages yet", + "No cart UI yet" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-008" + ], + "description": "Problem: There is no customer-facing site. Goal: Next.js + TS + Tailwind shell consuming backend API. Scope IN: app shell, navigation, typed API client. Scope OUT: no catalog pages. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Home renders server-side with navigation", + "Frontend never imports backend internals, only typed API client", + "Build, lint and typecheck green", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-014", + "type": "feature", + "title": "Storefront catalog pages (SSG/ISR)", + "problem": "Products, categories and brands need fast SEO-friendly public pages.", + "goal": "SSG/ISR pages for product, category and brand with stable URLs and metadata.", + "scope_in": [ + "Product detail page at /productos/", + "Category listing at /categoria/", + "Brand page at /marca/", + "SSG/ISR rendering with on-demand revalidation", + "Per-page metadata and OpenGraph", + "Search results page" + ], + "scope_out": [ + "No cart, no checkout UI" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-010", + "F-012", + "F-013" + ], + "description": "Problem: Products, categories and brands need fast SEO-friendly public pages. Goal: SSG/ISR catalog pages with stable URLs and metadata. Scope IN: product/category/brand/search pages, ISR, metadata, OG. Scope OUT: no commerce UI. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Product page served at /productos/ with no internal id in URL", + "Given catalog update When revalidation triggered Then page reflects new data", + "Each page carries title, description and OpenGraph metadata", + "Lighthouse SEO score >= 90 on product and category pages", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-015", + "type": "feature", + "title": "SEO core: structured data, sitemap, redirects", + "problem": "Organic traffic is core business; structured data and crawlability are missing.", + "goal": "JSON-LD schemas, sitemap.xml, robots.txt and redirect management.", + "scope_in": [ + "seo module", + "Product, Breadcrumb and Organization JSON-LD", + "sitemap.xml and robots.txt generation", + "Redirect store behind interface with 301 support", + "Canonical URLs on all public pages" + ], + "scope_out": [ + "No hreflang", + "No external SEO tooling integration" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-014" + ], + "description": "Problem: Organic traffic is core business; structured data and crawlability are missing. Goal: JSON-LD, sitemap, robots, redirects. Scope IN: seo module, schemas, sitemap, robots, redirect store. Scope OUT: no hreflang. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Product page embeds valid Product JSON-LD (validator clean)", + "sitemap.xml lists active public URLs and excludes draft content", + "Given stored redirect When old URL requested Then HTTP 301 to new URL", + "Every public page has canonical URL", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-016", + "type": "feature", + "title": "Inventory module", + "problem": "Catalog says what a product is; nothing says if it can be sold.", + "goal": "Isolated inventory module with atomic stock operations and availability API.", + "scope_in": [ + "inventory module, fully isolated from catalog", + "Stock states: available, reserved, sold, incoming", + "InventoryService.checkAvailability() public interface", + "Atomic reserve, release, confirm operations", + "Negative stock impossible at database and domain level" + ], + "scope_out": [ + "No warehouse/supplier integration", + "No incoming purchase orders UI" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-002" + ], + "description": "Problem: Catalog says what a product is; nothing says if it can be sold. Goal: isolated inventory with atomic stock ops and availability API. Scope IN: inventory module, stock states, atomic reserve/release/confirm, no-negative rule. Scope OUT: no warehouse integration. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Given 10 concurrent reservations for the last unit When all settle Then exactly 1 succeeds and 9 get unavailable", + "Given zero stock When reservation requested Then rejected and stock never negative", + "Catalog module contains zero references to inventory tables", + "Checkout checks stock only through InventoryService", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-017", + "type": "feature", + "title": "Pricing module", + "problem": "Nothing calculates real prices and taxes; frontend must never supply them.", + "goal": "Server-side pricing with VAT behind PricingService.", + "scope_in": [ + "pricing module owning price and tax rules", + "Price per variant with VAT (Spain general/reduced rates)", + "PricingService.calculate() public interface", + "Price history table for audit" + ], + "scope_out": [ + "No promotions yet", + "No multi-currency" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-002", + "F-010" + ], + "description": "Problem: Nothing calculates real prices and taxes; frontend must never supply them. Goal: server-side pricing with VAT behind PricingService. Scope IN: pricing module, VAT, calculate interface, price history. Scope OUT: no promotions, no multi-currency. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Given variant and quantity When PricingService.calculate Then total with VAT returned", + "Given client-supplied price in any request Then price ignored and recalculated", + "Price change writes history row, never silent update", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-018", + "type": "feature", + "title": "Cart module", + "problem": "Customers need a cart that never lies about prices or stock.", + "goal": "Cart with items and server-side recalculation of totals.", + "scope_in": [ + "cart module storing product_id, variant_id, quantity only", + "Add, remove, change quantity use cases", + "Totals always recalculated via PricingService and InventoryService", + "Stored prices never trusted" + ], + "scope_out": [ + "No guest cart persistence across devices yet", + "No promotions applied yet" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-016", + "F-017" + ], + "description": "Problem: Customers need a cart that never lies about prices or stock. Goal: cart with items and server-side recalculation. Scope IN: cart module, item ops, recalculation via Pricing/Inventory. Scope OUT: no promotions yet. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Given product price changed after add to cart When cart read Then new price shown", + "Given variant out of stock When cart read Then item flagged unavailable", + "Cart payload from client containing price fields is ignored", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-019", + "type": "feature", + "title": "Promotions v1", + "problem": "Store needs discounts; discount logic must be validated server-side.", + "goal": "Simple promotions engine behind an interface, validated at recalculation.", + "scope_in": [ + "promotions module with percent and fixed-amount rules", + "Promo codes with validity window and usage limits", + "Discount validation inside recalculation, never from frontend", + "No stacking unless rule explicitly allows" + ], + "scope_out": [ + "No loyalty points", + "No buy-X-get-Y rules yet" + ], + "priority": "med", + "risk": "med", + "depends_on": [ + "F-017", + "F-018" + ], + "description": "Problem: Store needs discounts; discount logic must be validated server-side. Goal: simple promotions engine behind interface, validated at recalculation. Scope IN: percent/fixed rules, promo codes, validity, no frontend trust. Scope OUT: no loyalty. Type: feature. Priority: med. Risk: med.", + "acceptance": [ + "Given valid promo code When applied Then discount recalculated server-side", + "Given expired or exhausted code When applied Then HTTP 422 with reason", + "Given client-supplied discount amount Then discount ignored", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-020", + "type": "feature", + "title": "Shipping module", + "problem": "Checkout cannot quote delivery cost.", + "goal": "Shipping zones, methods and cost calculation behind ShippingService.", + "scope_in": [ + "shipping module with zones and methods", + "ShippingService.calculate(cart, address) interface", + "Free shipping threshold rule", + "Carrier adapter interface (implementation later)" + ], + "scope_out": [ + "No real carrier API integration yet", + "No tracking" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-002", + "F-018" + ], + "description": "Problem: Checkout cannot quote delivery cost. Goal: zones, methods and cost calculation behind ShippingService. Scope IN: shipping module, zones, methods, threshold, adapter interface. Scope OUT: no carrier API. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Given address in known zone When calculate Then shipping cost returned", + "Given address outside all zones When checkout Then HTTP 422 with clear reason", + "Given cart above free shipping threshold Then shipping cost is zero", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-021", + "type": "feature", + "title": "Orders module with snapshots and state machine", + "problem": "Nothing records purchase truth; history must never depend on live catalog.", + "goal": "Order aggregate with item snapshots and explicit state transitions.", + "scope_in": [ + "orders module", + "Order items snapshot: name, SKU, EAN, unit price, discount, tax, quantity", + "States: PENDING, AWAITING_PAYMENT, PAID, PROCESSING, SHIPPED, DELIVERED, CANCELLED, REFUNDED, PARTIALLY_REFUNDED", + "Explicit state machine, illegal transitions rejected", + "Domain events: OrderCreated, OrderPaid, OrderCancelled" + ], + "scope_out": [ + "No refunds execution yet (payments ticket)", + "No admin order UI" + ], + "priority": "high", + "risk": "med", + "depends_on": [ + "F-002", + "F-017" + ], + "description": "Problem: Nothing records purchase truth; history must never depend on live catalog. Goal: order aggregate with item snapshots and explicit state machine. Scope IN: orders module, snapshots, state machine, domain events. Scope OUT: no refunds execution, no admin UI. Type: feature. Priority: high. Risk: med.", + "acceptance": [ + "Given an order When product is later renamed or repriced Then order keeps original snapshot values", + "Given order in SHIPPED When transition to PENDING requested Then transition rejected", + "Every legal state transition is covered by unit test", + "OrderCreated event published on creation", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-022", + "type": "feature", + "title": "Checkout orchestrator with idempotency", + "problem": "No flow turns a cart into a payable order safely.", + "goal": "Checkout coordinates validation, pricing, stock, discounts, shipping, taxes, order creation and reservation.", + "scope_in": [ + "checkout module as orchestrator only, owning no business data", + "Flow: validate products, prices, stock, discounts, shipping, taxes, create pending order, reserve inventory, create payment intent", + "idempotency_key required on checkout request", + "HTTP 409 on unavailable stock without order or payment", + "checkout_success_total and checkout_failure_total metrics" + ], + "scope_out": [ + "No payment provider implementation yet (interface stub)", + "No checkout UI yet" + ], + "priority": "high", + "risk": "high", + "depends_on": [ + "F-018", + "F-019", + "F-020", + "F-021" + ], + "description": "Problem: No flow turns a cart into a payable order safely. Goal: checkout orchestrates validation, pricing, stock, discounts, shipping, taxes, pending order, reservation, payment intent. Scope IN: orchestrator flow, idempotency_key, 409 handling, metrics. Scope OUT: no provider implementation, no UI. Type: feature. Priority: high. Risk: high.", + "acceptance": [ + "Given a cart containing an unavailable item When checkout requested Then HTTP 409, no order created, no payment initiated", + "Given same idempotency_key sent twice When second request arrives Then same order returned, no duplicate order, no duplicate reservation", + "Given successful checkout Then order exists in AWAITING_PAYMENT and stock is reserved", + "Given checkout failure after reservation When flow aborts Then reservation released", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-023", + "type": "feature", + "title": "Payments: provider interface + Stripe + webhooks", + "problem": "Checkout needs real money movement without coupling domain to Stripe.", + "goal": "PaymentProvider interface with Stripe adapter and idempotent webhook processing.", + "scope_in": [ + "payments module with PaymentProvider interface", + "StripePaymentProvider adapter; domain never imports Stripe SDK", + "Webhook: validate signature, deduplicate event, process, publish domain event", + "Events: PaymentSucceeded, PaymentFailed, PaymentRefunded, ChargebackCreated", + "payments_transactions table as source of truth" + ], + "scope_out": [ + "No Redsys/PayPal yet", + "No payout reconciliation" + ], + "priority": "high", + "risk": "high", + "depends_on": [ + "F-022" + ], + "description": "Problem: Checkout needs real money movement without coupling domain to Stripe. Goal: PaymentProvider interface, Stripe adapter, idempotent webhooks. Scope IN: interface, adapter, webhook validation/dedup, domain events. Scope OUT: no Redsys/PayPal. Type: feature. Priority: high. Risk: high.", + "acceptance": [ + "Domain code contains zero direct Stripe SDK imports", + "Given webhook with bad signature When received Then rejected with non-2xx and not processed", + "Given same webhook delivered twice When second delivery arrives Then processed exactly once", + "Given PaymentSucceeded When processed Then order moves to PAID and event published", + "Payment status from frontend is never trusted", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-024", + "type": "feature", + "title": "Notifications: transactional email", + "problem": "Customers get no feedback after purchase or payment failure.", + "goal": "Transactional emails driven by domain events behind a provider interface.", + "scope_in": [ + "notifications module", + "Email provider interface with one implementation", + "Templates: order confirmation, payment failed, order shipped", + "Send triggered by domain events, idempotent per event id" + ], + "scope_out": [ + "No marketing email", + "No SMS/push" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-023", + "F-005" + ], + "description": "Problem: Customers get no feedback after purchase or payment failure. Goal: transactional emails driven by domain events behind provider interface. Scope IN: notifications module, email adapter, templates, idempotent sends. Scope OUT: no marketing, no SMS. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Given PaymentSucceeded event When processed Then order confirmation email queued", + "Given same event redelivered When processed Then no duplicate email", + "Swapping email provider touches only infrastructure adapter", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-025", + "type": "feature", + "title": "Reviews module", + "problem": "Social proof is missing; reviews must be tied to real purchases.", + "goal": "Verified-purchase reviews with moderation and rating aggregates.", + "scope_in": [ + "reviews module", + "One review per order item, verified purchase only", + "Moderation states: pending, published, rejected", + "Rating aggregate per product, cached" + ], + "scope_out": [ + "No photos in reviews", + "No vendor responses" + ], + "priority": "low", + "risk": "low", + "depends_on": [ + "F-021", + "F-006" + ], + "description": "Problem: Social proof is missing; reviews must be tied to real purchases. Goal: verified-purchase reviews with moderation and aggregates. Scope IN: reviews module, verified purchase rule, moderation, aggregates. Scope OUT: no photos. Type: feature. Priority: low. Risk: low.", + "acceptance": [ + "Given user without delivered order item When review submitted Then HTTP 403", + "Given second review for same order item When submitted Then HTTP 409", + "Only published reviews appear on product page", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-026", + "type": "feature", + "title": "CMS module", + "problem": "Marketing needs landing and content pages without deploys.", + "goal": "Simple CMS with pages, content blocks, slugs and draft/publish states.", + "scope_in": [ + "cms module with page and block model", + "Slug-based public routes", + "Draft/publish states", + "Storefront renders published pages" + ], + "scope_out": [ + "No WYSIWYG builder", + "No versioning" + ], + "priority": "low", + "risk": "low", + "depends_on": [ + "F-013" + ], + "description": "Problem: Marketing needs landing and content pages without deploys. Goal: simple CMS with pages, blocks, slugs, draft/publish. Scope IN: cms module, slug routes, states, rendering. Scope OUT: no builder. Type: feature. Priority: low. Risk: low.", + "acceptance": [ + "Given published page When slug requested Then page renders", + "Given draft page When slug requested Then HTTP 404", + "Given duplicate slug When page created Then HTTP 409", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-027", + "type": "feature", + "title": "Caching layer with explicit contracts", + "problem": "Hot catalog pages hit the database on every request.", + "goal": "Redis cache for product detail, category listing and navigation with key, TTL and invalidation.", + "scope_in": [ + "Cache wrapper with key, TTL, invalidation strategy and source of truth documented per entry", + "Targets: product detail, category listing, navigation, popular searches", + "Invalidation on catalog update events", + "cache_hit_ratio metric" + ], + "scope_out": [ + "Redis never source of truth", + "No CDN config yet" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-014" + ], + "description": "Problem: Hot catalog pages hit the database on every request. Goal: Redis cache for hot reads with explicit key/TTL/invalidation contracts. Scope IN: cache wrapper, hot targets, event invalidation, hit ratio metric. Scope OUT: Redis never truth, no CDN. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Every cache entry documents key pattern, TTL, invalidation and source of truth", + "Given catalog update When event published Then related cache entries invalidated", + "Given Redis down When read path runs Then requests still succeed from PostgreSQL", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-028", + "type": "feature", + "title": "Security hardening", + "problem": "Baseline security controls are scattered or missing.", + "goal": "Rate limiting, CSRF, audit log, admin MFA and dependency scanning in one pass.", + "scope_in": [ + "Global and per-route rate limiting", + "CSRF protection where applicable", + "Audit log for admin mutations", + "MFA for admin accounts", + "Dependency scanning in CI" + ], + "scope_out": [ + "No WAF/network-level work", + "No pentest" + ], + "priority": "med", + "risk": "med", + "depends_on": [ + "F-006" + ], + "description": "Problem: Baseline security controls are scattered or missing. Goal: rate limiting, CSRF, audit log, admin MFA, dependency scanning. Scope IN: all listed controls. Scope OUT: no WAF, no pentest. Type: feature. Priority: med. Risk: med.", + "acceptance": [ + "Given admin mutation When executed Then audit log row with actor, action and target", + "Given admin login without MFA When attempted Then blocked until MFA enrolled", + "Given request above rate limit Then HTTP 429", + "CI fails on known vulnerable dependency", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-029", + "type": "feature", + "title": "Observability: traces and business metrics", + "problem": "Operations cannot see checkout health or latency.", + "goal": "OpenTelemetry traces plus Prometheus business metrics.", + "scope_in": [ + "OpenTelemetry tracing across modules", + "Metrics: checkout_success_total, checkout_failure_total, payment_failure_total, order_created_total, inventory_conflict_total, api_latency, database_latency, cache_hit_ratio", + "request_id and trace_id on every request", + "Grafana dashboard baseline" + ], + "scope_out": [ + "No alerting rules beyond basics", + "No log aggregation infra" + ], + "priority": "med", + "risk": "low", + "depends_on": [ + "F-023" + ], + "description": "Problem: Operations cannot see checkout health or latency. Goal: OTel traces plus Prometheus business metrics. Scope IN: tracing, listed metrics, ids, dashboard. Scope OUT: no full alerting stack. Type: feature. Priority: med. Risk: low.", + "acceptance": [ + "Every request carries request_id and trace_id in logs", + "Each listed metric is exposed on /metrics and changes on the corresponding event", + "A checkout flow produces a connected trace across modules", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + }, + { + "id": "F-030", + "type": "feature", + "title": "E2E suite for critical flows", + "problem": "Critical user journeys have no end-to-end protection.", + "goal": "Few but real E2E tests covering the money path.", + "scope_in": [ + "E2E runner setup", + "Flows: register, login, search, view product, add to cart, checkout, payment, order confirmation, refund", + "Runs against disposable environment" + ], + "scope_out": [ + "No visual regression", + "No load testing" + ], + "priority": "high", + "risk": "low", + "depends_on": [ + "F-022", + "F-023" + ], + "description": "Problem: Critical user journeys have no end-to-end protection. Goal: E2E tests covering the money path. Scope IN: runner, critical flows, disposable env. Scope OUT: no visual regression, no load testing. Type: feature. Priority: high. Risk: low.", + "acceptance": [ + "Each critical flow has one green E2E test", + "E2E suite runs unattended in CI", + "Given payment webhook simulated in E2E Then order reaches PAID", + "verify.sh green" + ], + "status": "pending", + "created_at": "2026-08-14", + "gates": { + "review": false, + "security": false, + "qa": false + } + } + ] +} diff --git a/harness/agents.matrix.yml b/harness/agents.matrix.yml new file mode 100644 index 0000000..f11e55f --- /dev/null +++ b/harness/agents.matrix.yml @@ -0,0 +1,71 @@ +version: 1 + +roles: + leader: + can_edit: ["work/", "backlog/", "spec/", "harness/"] + cannot_edit: ["src/", "tests/"] + responsibilities: + - plan + - orchestrate + - enforce_gates + - close_feature + + architect: + can_edit: ["spec/", "harness/contracts/", "docs/"] + cannot_edit: ["src/", "tests/", "backlog/features.json:status"] + responsibilities: + - design + - update_contracts + + implementer: + can_edit: ["src/", "tests/", "work/artifacts/"] + cannot_edit: + - "backlog/features.json:done" + - "work/history.md" + - "work/artifacts/*/reviewer.json" + - "work/artifacts/*/security.json" + - "work/artifacts/*/qa.json" + - "work/artifacts/*/leader-close.json" + responsibilities: + - implement_feature + - write_tests + - produce_implementer_evidence + + reviewer: + can_edit: ["work/artifacts/"] + cannot_edit: ["src/", "tests/", "backlog/"] + responsibilities: + - technical_review + - emit_reviewer_verdict + + security: + can_edit: ["work/artifacts/"] + cannot_edit: ["src/", "tests/", "backlog/"] + responsibilities: + - sast + - dependency_review + - secret_scan + - emit_security_verdict + + qa: + can_edit: ["work/artifacts/"] + cannot_edit: ["src/", "tests/", "backlog/"] + responsibilities: + - acceptance_traceability + - integration_e2e_checks + - regression_checks + - emit_qa_verdict + + documenter: + can_edit: ["work/artifacts/", "docs/", "README.md", "HOWTO.md", "platforms/pi/README.md", "harness/contracts/"] + cannot_edit: ["src/", "tests/", "backlog/features.json:status"] + responsibilities: + - document_changes_when_needed + - update_docs_api_contracts_user_facing_notes + - produce_documenter_evidence + +anti_cheat: + - "Implementer cannot promote feature to done" + - "Done requires reviewer/security/qa approved artifacts" + - "Leader close requires verify.sh success" + - "Evidence must be on disk; chat-only claims are invalid" diff --git a/harness/contracts/evidence.schema.json b/harness/contracts/evidence.schema.json new file mode 100644 index 0000000..8e8fcc4 --- /dev/null +++ b/harness/contracts/evidence.schema.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://orquestra.local/evidence.schema.json", + "title": "Gate Evidence", + "type": "object", + "required": ["feature_id", "agent", "verdict", "summary", "evidence", "timestamp"], + "properties": { + "feature_id": { "type": "string", "minLength": 1 }, + "agent": { "type": "string", "enum": ["reviewer", "security", "qa", "leader"] }, + "verdict": { "type": "string", "enum": ["APPROVED", "CHANGES_REQUESTED"] }, + "summary": { "type": "string", "minLength": 1 }, + "evidence": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "timestamp": { "type": "string", "format": "date-time" } + }, + "additionalProperties": true +} diff --git a/harness/contracts/handoff.md b/harness/contracts/handoff.md new file mode 100644 index 0000000..1fe2f47 --- /dev/null +++ b/harness/contracts/handoff.md @@ -0,0 +1,22 @@ +# Handoff Contract + +## Regla anti teléfono-descompuesto +Los agentes **no** devuelven código en chat. Deben escribir a disco y responder solo: +- `done -> ` +- `blocked -> ` + +## Rutas estándar por feature +- `work/artifacts//implementer.md` +- `work/artifacts//reviewer.json` +- `work/artifacts//security.json` +- `work/artifacts//qa.json` +- `work/artifacts//documenter.md` (opcional; requerido solo si cambiaron docs/API/contratos/comportamiento user-facing) +- `work/artifacts//leader-close.json` + +## Campos mínimos de artefactos JSON de gate +- `feature_id` (string) +- `agent` (reviewer|security|qa|leader) +- `verdict` (APPROVED|CHANGES_REQUESTED) +- `summary` (string) +- `evidence` (array de comandos/salidas/rutas) +- `timestamp` (ISO 8601) diff --git a/harness/model-routing.yml b/harness/model-routing.yml new file mode 100644 index 0000000..eb8dec5 --- /dev/null +++ b/harness/model-routing.yml @@ -0,0 +1,23 @@ +version: 1 + +# Sequential role model hints for Pi. The parent session changes model before +# each stage when a different model is desired. No parallel agents/subagents. +roles: + leader: + model: default + reasoning: medium + architect: + model: strong + reasoning: medium + implementer: + model: default + reasoning: medium + reviewer: + model: strong + reasoning: low + security: + model: strong + reasoning: medium + qa: + model: default + reasoning: low diff --git a/harness/policies/governance.md b/harness/policies/governance.md new file mode 100644 index 0000000..dfd881d --- /dev/null +++ b/harness/policies/governance.md @@ -0,0 +1,16 @@ +# Governance Policy + +## Reglas +- Una sola feature activa (`in_progress`) a la vez. +- El pipeline es secuencial con gates obligatorios. +- `done` solo por `leader` y solo con evidencia completa. + +## Separación de funciones +- Implementer construye. +- Reviewer/Security/QA validan. +- Leader decide cierre. + +## Prohibiciones +- Saltar gates. +- Aprobar con evidencia incompleta. +- Cerrar con `verify.sh` en rojo. diff --git a/harness/policies/quality.md b/harness/policies/quality.md new file mode 100644 index 0000000..89d5f0c --- /dev/null +++ b/harness/policies/quality.md @@ -0,0 +1,12 @@ +# Quality Policy + +## Gate QA (obligatorio) +Cada feature debe tener `work/artifacts//qa.json` con: +- `verdict`: APPROVED | CHANGES_REQUESTED +- trazabilidad criterio de aceptación -> test +- resultado de pruebas integración/E2E relevantes + +## Reglas +- No hay `done` sin cobertura mínima de acceptance. +- No hay `done` con regresiones abiertas. +- Claims sin evidencia ejecutable no son válidos. diff --git a/harness/policies/security.md b/harness/policies/security.md new file mode 100644 index 0000000..1d2adfb --- /dev/null +++ b/harness/policies/security.md @@ -0,0 +1,12 @@ +# Security Policy + +## Gate de seguridad (obligatorio) +Cada feature debe tener `work/artifacts//security.json` con: +- `verdict`: APPROVED | CHANGES_REQUESTED +- `checks`: secretos, dependencias, SAST básico, validación de inputs +- `findings`: lista de hallazgos con severidad + +## Reglas +- Si hay hallazgos críticos/altos sin mitigación: `CHANGES_REQUESTED`. +- No se permite exponer credenciales ni secretos en repo/chat. +- Dependencias nuevas requieren justificación en `spec/tech.md`. diff --git a/harness/workflow.stages.yml b/harness/workflow.stages.yml new file mode 100644 index 0000000..221cd0e --- /dev/null +++ b/harness/workflow.stages.yml @@ -0,0 +1,62 @@ +version: 1 + +feature_states: + allowed: [pending, in_progress, blocked, done] + +stages: + - name: intake + owner: leader + input: + - backlog/features.json + - work/current.md + output: + - work/current.md + + - name: design + owner: architect + optional: true + output: + - work/artifacts//architect.md + + - name: build + owner: implementer + output: + - work/artifacts//implementer.md + + - name: review_gate + owner: reviewer + required: true + output: + - work/artifacts//reviewer.json + + - name: security_gate + owner: security + required: true + output: + - work/artifacts//security.json + + - name: qa_gate + owner: qa + required: true + output: + - work/artifacts//qa.json + + - name: document + owner: documenter + optional: true + when: docs/API/contracts/user-facing behavior changed + output: + - work/artifacts//documenter.md + + - name: close + owner: leader + required: true + output: + - work/artifacts//leader-close.json + - work/history.md + +close_requirements: + - reviewer.json.verdict == "APPROVED" + - security.json.verdict == "APPROVED" + - qa.json.verdict == "APPROVED" + - scripts/verify.sh exit_code == 0 diff --git a/platforms/pi/README.md b/platforms/pi/README.md new file mode 100644 index 0000000..8e0c97a --- /dev/null +++ b/platforms/pi/README.md @@ -0,0 +1,36 @@ +# Adaptador Pi + +Orquestra se ejecuta desde Pi como **un solo parent session secuencial**. No instala subagentes. + +## Requisitos obligatorios +- `pi` debe existir en `PATH` antes de instalar Orquestra. +- El proyecto instalado debe abrirse desde su raíz. +- Arrancar con `./scripts/pi_orquestra.sh`, que ejecuta `pi --no-extensions` y carga solo extensiones Orquestra. +- Extensiones project-local declaradas: `.pi/extensions/orquestra-status/` y `.pi/extensions/orquestra-web-fetch.ts`. +- El código de producto vive en `project/`; archivos de código en la raíz son inválidos. + +## Instalación esperada +Cuando Orquestra se instala en un repo de proyecto, el instalador debe copiar: +- `platforms/pi/extensions/orquestra-status/` -> `.pi/extensions/orquestra-status/` +- `platforms/pi/extensions/orquestra-web-fetch.ts` -> `.pi/extensions/orquestra-web-fetch.ts` + +No debe crear `.pi/subagents/` ni `.pi/subagents.json`. + +## Flujo secuencial +1. Ejecutar `./scripts/verify.sh`. +2. Abrir Pi limpio desde la raíz con `./scripts/pi_orquestra.sh`. +3. Confirmar el widget con `/orquestra-status`. +4. El mismo parent session cambia de rol siguiendo `harness/workflow.stages.yml`. +5. Antes de cada stage, actualizar estado con `python3 scripts/agent_status.py set ...`. +6. `agent_status.py` rechaza saltos de stage sin artefactos previos obligatorios. +7. Durante `build`, escribir producto en `project/` y tests en `tests/`; requiere `feature_id`, `stage=build`, `agent=implementer` y `state=running` en `work/runtime-status.json`. +8. Al terminar cada stage, escribir el artefacto esperado en `work/artifacts//`. +9. Ejecutar `document`/`documenter.md` solo si cambiaron docs/API/contratos/comportamiento user-facing; no es gate obligatorio de cierre. +10. Recién después empieza el siguiente rol/stage. + +## Modelos por rol +Si querés modelos distintos por etapa, se eligen secuencialmente antes de cada stage según `harness/model-routing.yml`. No hay ejecución paralela. + +## Respuesta estándar por etapa +- `done -> ` +- `blocked -> ` diff --git a/platforms/pi/extensions/orquestra-status/README.md b/platforms/pi/extensions/orquestra-status/README.md new file mode 100644 index 0000000..5fceac6 --- /dev/null +++ b/platforms/pi/extensions/orquestra-status/README.md @@ -0,0 +1,42 @@ +# Orquestra Status Extension + +Pi extension project-local que renderiza `work/runtime-status.json` y aplica el guard mínimo de escritura Orquestra. + +## Muestra +- feature activa +- stage actual +- rol actual +- acción/estado +- siguiente rol +- checklist de artefactos por rol +- estado de gates +- último evento de timeline + +## Ruta instalada +- `.pi/extensions/orquestra-status/index.ts` + +Se lanza junto con `.pi/extensions/orquestra-web-fetch.ts` desde `scripts/pi_orquestra.sh`. + +## Uso +1. Abrí `pi` en la raíz del proyecto instalado. +2. Si Pi ya estaba abierto, ejecutá `/reload`. +3. El widget aparece arriba del editor y el footer muestra estado compacto. +4. El widget refresca cada segundo. +5. Refresco manual: `/orquestra-status`. + +## Guard de escritura + +La extensión intercepta herramientas Pi built-in `write` y `edit`: +- bloquea archivos de producto/código en la raíz (`*.py`, `*.js`, `*.ts`, `*.go`, `*.rs`, `*.java`, `*.php`, `*.rb`); usá `project/`. +- bloquea escrituras en `project/` o `tests/` salvo que `work/runtime-status.json` tenga `feature_id`, `stage=build`, `agent=implementer` y `state=running`. +- bloquea paths que escapan la raíz del repo con `..`. + +## Fuente de verdad +- `work/runtime-status.json` + +Actualizar con: + +```bash +python3 scripts/agent_status.py set ... +python3 scripts/agent_status.py reset +``` diff --git a/platforms/pi/extensions/orquestra-status/index.ts b/platforms/pi/extensions/orquestra-status/index.ts new file mode 100644 index 0000000..30efef9 --- /dev/null +++ b/platforms/pi/extensions/orquestra-status/index.ts @@ -0,0 +1,291 @@ +import { readFileSync, watchFile, unwatchFile } from "node:fs"; +import { join, relative, resolve, sep } from "node:path"; +import type { ExtensionAPI, ExtensionContext, ToolCallEvent } from "@earendil-works/pi-coding-agent"; + +const STATUS_FILE = "work/runtime-status.json"; +const MATRIX_FILE = "harness/agents.matrix.yml"; +const ARTIFACTS_DIR = "work/artifacts"; +const WIDGET_KEY = "orquestra-runtime"; +const STATUS_KEY = "orquestra-runtime"; + +const DEFAULT_EMOJIS: Record = { + leader: "🧭", + architect: "🏗️", + implementer: "🛠️", + reviewer: "🔍", + security: "🔒", + qa: "🧪", + documenter: "📝", +}; + +const GATE_FILES: Record = { + reviewer: "reviewer.json", + security: "security.json", + qa: "qa.json", + leader: "leader-close.json", +}; + +const AGENT_FILES: Record = { + architect: "architect.md", + implementer: "implementer.md", + reviewer: "reviewer.json", + security: "security.json", + qa: "qa.json", + documenter: "documenter.md", + leader: "leader-close.json", +}; + +type RuntimeStatus = { + feature_id?: string | null; + stage?: string; + agent?: string; + action?: string; + state?: string; + next_agent?: string; + waiting_for?: string; + updated_at?: string; + timeline?: Array<{ ts?: string; agent?: string; stage?: string; state?: string; message?: string }>; +}; + +const ROOT_PRODUCT_EXTENSIONS = new Set([".py", ".js", ".ts", ".go", ".rs", ".java", ".php", ".rb"]); + +function safeReadJson(root: string, path: string): RuntimeStatus | null { + try { + return JSON.parse(readFileSync(join(root, path), "utf8")) as RuntimeStatus; + } catch { + return null; + } +} + +function loadRoleEmojis(root: string): Record { + try { + const text = readFileSync(join(root, MATRIX_FILE), "utf8"); + const emojis = { ...DEFAULT_EMOJIS }; + let currentRole: string | null = null; + for (const line of text.split(/\r?\n/)) { + const roleMatch = line.match(/^ ([a-z_]+):\s*$/); + if (roleMatch) { + currentRole = roleMatch[1]; + continue; + } + const emojiMatch = line.match(/^\s{4}emoji:\s*["']?(.*?)["']?\s*$/); + if (emojiMatch && currentRole) emojis[currentRole] = emojiMatch[1]; + } + return emojis; + } catch { + return { ...DEFAULT_EMOJIS }; + } +} + +function gateState(root: string, featureId?: string | null): Record { + const result: Record = {}; + if (!featureId) return result; + for (const [gate, filename] of Object.entries(GATE_FILES)) { + try { + if (filename.endsWith(".md")) { + readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8"); + result[gate] = "APPROVED"; + continue; + } + const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string }; + result[gate] = json.verdict === "APPROVED" ? "APPROVED" : "PRESENT"; + } catch { + result[gate] = "PENDING"; + } + } + return result; +} + +function gateSummary(root: string, featureId: string | null | undefined, emojis: Record): string { + const gates = gateState(root, featureId); + if (!featureId) return "Gates: —"; + return `Gates: ${["reviewer", "security", "qa", "leader"] + .map((gate) => `${emojis[gate] || "•"}${gates[gate] === "APPROVED" ? "✅" : gates[gate] === "PRESENT" ? "⚠️" : "⏳"}`) + .join(" ")}`; +} + +function artifactStatus(root: string, featureId: string | null | undefined, agent: string): string { + if (!featureId) return "PENDING"; + const filename = AGENT_FILES[agent]; + if (!filename) return "PENDING"; + try { + if (filename.endsWith(".md")) { + readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8"); + return "DONE"; + } + const json = JSON.parse(readFileSync(join(root, ARTIFACTS_DIR, featureId, filename), "utf8")) as { verdict?: string }; + return json.verdict === "APPROVED" ? "DONE" : "PRESENT"; + } catch { + return "PENDING"; + } +} + +function agentSummary(root: string, status: RuntimeStatus, emojis: Record): string { + const featureId = status.feature_id || null; + const currentAgent = status.agent || "leader"; + return `Roles: ${["architect", "implementer", "reviewer", "security", "qa", "documenter", "leader"] + .map((agent) => { + const state = artifactStatus(root, featureId, agent); + const mark = state === "DONE" ? "✅" : state === "PRESENT" ? "⚠️" : agent === currentAgent ? "▶️" : "⏳"; + return `${emojis[agent] || "•"}${mark}`; + }) + .join(" ")}`; +} + +function isInsideRel(relPath: string, dirname: string): boolean { + return relPath === dirname || relPath.startsWith(`${dirname}/`) || relPath.startsWith(`${dirname}${sep}`); +} + +function normalizeWritePath(root: string, rawPath: unknown): { relPath?: string; reason?: string } { + if (typeof rawPath !== "string" || rawPath.trim() === "") { + return { reason: "Orquestra bloqueó una escritura sin path válido." }; + } + + const absPath = resolve(root, rawPath); + const relPath = relative(root, absPath); + if (relPath === "" || relPath.startsWith("..") || resolve(root, relPath) !== absPath) { + return { reason: "Orquestra bloqueó una escritura fuera de la raíz del proyecto." }; + } + return { relPath: relPath.replace(/\\/g, "/") }; +} + +function isRootProductFile(relPath: string): boolean { + if (relPath.includes("/")) return false; + const dot = relPath.lastIndexOf("."); + return dot > 0 && ROOT_PRODUCT_EXTENSIONS.has(relPath.slice(dot)); +} + +function hasActiveBuildRuntime(status: RuntimeStatus | null): boolean { + return Boolean( + status?.feature_id && + status.stage === "build" && + status.agent === "implementer" && + status.state === "running", + ); +} + +function guardWriteOrEdit(event: ToolCallEvent, ctx: ExtensionContext): { block?: boolean; reason?: string } | undefined { + if (event.toolName !== "write" && event.toolName !== "edit") return undefined; + + const { relPath, reason } = normalizeWritePath(ctx.cwd, (event.input as { path?: unknown }).path); + if (!relPath) return { block: true, reason }; + + if (isRootProductFile(relPath)) { + return { + block: true, + reason: `Orquestra bloqueó '${relPath}': los archivos de producto/código no van en la raíz. Escribí ese código dentro de project/.`, + }; + } + + if (isInsideRel(relPath, "project") || isInsideRel(relPath, "tests")) { + const status = safeReadJson(ctx.cwd, STATUS_FILE); + if (!hasActiveBuildRuntime(status)) { + return { + block: true, + reason: + "Orquestra bloqueó esta escritura: los cambios en project/ o tests/ requieren una feature/ticket activa, stage build, agent implementer y state running. Seleccioná o creá un ticket y actualizá work/runtime-status.json con scripts/agent_status.py antes de escribir.", + }; + } + } + + return undefined; +} + +function render(root: string, status: RuntimeStatus | null, emojis: Record) { + if (!status) { + return { + widget: ["Orquestra: no se pudo leer work/runtime-status.json"], + footer: "Orquestra status unavailable", + }; + } + + const feature = status.feature_id || "—"; + const stage = status.stage || "—"; + const agent = status.agent || "leader"; + const action = status.action || "—"; + const state = status.state || "—"; + const nextAgent = status.next_agent || "—"; + const waitingFor = status.waiting_for || "—"; + const updatedAt = status.updated_at || "—"; + const lastEvent = status.timeline && status.timeline.length > 0 ? status.timeline[status.timeline.length - 1] : null; + const lastLine = lastEvent + ? `Último: ${lastEvent.ts || "—"} · ${emojis[lastEvent.agent || "leader"] || "•"} ${lastEvent.message || "—"}` + : "Último: —"; + + return { + widget: [ + `Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${agent} · ${state}`, + `Acción: ${action}`, + `Siguiente: ${emojis[nextAgent] || "•"} ${nextAgent} · Esperando: ${waitingFor}`, + agentSummary(root, status, emojis), + gateSummary(root, status.feature_id || null, emojis), + lastLine, + `Actualizado: ${updatedAt}`, + ], + footer: `Orquestra ${emojis[agent] || "•"} ${feature} · ${stage} · ${state}`, + }; +} + +export default function orquestraStatusExtension(pi: ExtensionAPI) { + let currentCtx: ExtensionContext | null = null; + let watching = false; + let refreshTimer: ReturnType | null = null; + let lastRendered = ""; + + function apply(ctx: ExtensionContext) { + const root = ctx.cwd; + const emojis = loadRoleEmojis(root); + const rendered = render(root, safeReadJson(root, STATUS_FILE), emojis); + const signature = JSON.stringify(rendered); + if (signature === lastRendered) return; + lastRendered = signature; + ctx.ui.setWidget(WIDGET_KEY, rendered.widget); + ctx.ui.setStatus(STATUS_KEY, rendered.footer); + } + + function refresh() { + if (!currentCtx || !currentCtx.hasUI) return; + apply(currentCtx); + } + + function startWatching() { + if (watching) return; + if (!currentCtx) return; + watchFile(join(currentCtx.cwd, STATUS_FILE), { interval: 1000 }, () => refresh()); + refreshTimer = setInterval(refresh, 1000); + watching = true; + } + + function stopWatching() { + if (!watching) return; + if (currentCtx) unwatchFile(join(currentCtx.cwd, STATUS_FILE)); + if (refreshTimer) clearInterval(refreshTimer); + refreshTimer = null; + watching = false; + } + + pi.on("session_start", async (_event, ctx) => { + if (!ctx.hasUI) return; + currentCtx = ctx; + apply(ctx); + startWatching(); + }); + + pi.on("session_shutdown", async (_event, ctx) => { + ctx.ui.setWidget(WIDGET_KEY, undefined); + ctx.ui.setStatus(STATUS_KEY, undefined); + stopWatching(); + currentCtx = null; + }); + + pi.on("tool_call", async (event, ctx) => guardWriteOrEdit(event, ctx)); + + pi.registerCommand("orquestra-status", { + description: "Refresca el widget de runtime status de Orquestra", + handler: async (_args, ctx) => { + currentCtx = ctx; + apply(ctx); + ctx.ui.notify("Orquestra status refrescado", "info"); + }, + }); +} diff --git a/platforms/pi/extensions/orquestra-web-fetch.ts b/platforms/pi/extensions/orquestra-web-fetch.ts new file mode 100644 index 0000000..595a78d --- /dev/null +++ b/platforms/pi/extensions/orquestra-web-fetch.ts @@ -0,0 +1,56 @@ +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; + +function textFromHtml(html: string): { title: string; content: string } { + const title = html.match(/]*>([^<]+)<\/title>/i)?.[1]?.trim() || "No title"; + const content = html + .replace(/]*>[\s\S]*?<\/script>/gi, " ") + .replace(/]*>[\s\S]*?<\/style>/gi, " ") + .replace(/<[^>]+>/g, " ") + .replace(/ /g, " ") + .replace(/&/g, "&") + .replace(/</g, "<") + .replace(/>/g, ">") + .replace(/"/g, '"') + .replace(/\s+/g, " ") + .trim(); + return { title, content }; +} + +async function fetchText(url: string, signal?: AbortSignal) { + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30000); + const activeSignal = signal ?? controller.signal; + try { + const response = await fetch(url, { + signal: activeSignal, + headers: { + "User-Agent": "Mozilla/5.0 Orquestra/1.0", + }, + }); + if (!response.ok) throw new Error(`HTTP ${response.status}`); + return textFromHtml(await response.text()); + } finally { + clearTimeout(timeout); + } +} + +export default function orquestraWebFetch(pi: ExtensionAPI) { + pi.registerTool({ + name: "orquestra_web_fetch", + label: "Orquestra Web Fetch", + description: "Fetch a web page and return extracted text using a simple non-browser fetch.", + parameters: Type.Object({ + url: Type.String({ description: "URL to fetch" }), + limit: Type.Optional(Type.Integer({ minimum: 500, maximum: 20000, description: "Max characters to return" })), + }), + async execute(_toolCallId, params, signal) { + const { title, content } = await fetchText(params.url, signal); + const limit = params.limit ?? 8000; + return { + content: [{ type: "text", text: `Title: ${title}\n\nContent:\n${content.slice(0, limit)}` }], + details: { title, url: params.url, characters: content.length }, + }; + }, + }); +} diff --git a/platforms/pi/leader.prompt.md b/platforms/pi/leader.prompt.md new file mode 100644 index 0000000..6dcb47c --- /dev/null +++ b/platforms/pi/leader.prompt.md @@ -0,0 +1,11 @@ +Actúa como `leader` secuencial de Orquestra. + +Reglas obligatorias: +- No implementes código de producto. +- No uses subagentes. +- Antes de cada stage, ejecutá `python3 scripts/agent_status.py set ...` con el rol/stage correcto. +- Si `agent_status.py` rechaza el cambio de stage, STOP: falta evidencia previa. +- Orquestá stages según `harness/workflow.stages.yml`. +- Usá `document`/`documenter` solo si cambiaron docs/API/contratos/comportamiento user-facing. +- Aplicá anti-trampa de `harness/agents.matrix.yml`. +- No marques `done` sin reviewer/security/qa APPROVED y `./scripts/verify.sh` en verde; `documenter.md` no es requisito de cierre por defecto. diff --git a/project/.gitignore b/project/.gitignore new file mode 100644 index 0000000..e6d5efa --- /dev/null +++ b/project/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +coverage/ +*.log diff --git a/project/.gitkeep b/project/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/project/.prettierignore b/project/.prettierignore new file mode 100644 index 0000000..e89701b --- /dev/null +++ b/project/.prettierignore @@ -0,0 +1,6 @@ +node_modules/ +dist/ +coverage/ +package-lock.json +scripts/tests/fixtures/ +design_prompt.md diff --git a/project/.prettierrc.json b/project/.prettierrc.json new file mode 100644 index 0000000..521e271 --- /dev/null +++ b/project/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "singleQuote": true, + "semi": true, + "printWidth": 100, + "trailingComma": "all" +} diff --git a/project/README.md b/project/README.md new file mode 100644 index 0000000..96a3959 --- /dev/null +++ b/project/README.md @@ -0,0 +1,38 @@ +# MercadoDeVida backend — modular monolith skeleton + +TypeScript + Fastify modular monolith. Simple code, clear modules, small changes, no magic. + +## Requirements + +- Node.js >= 22 +- npm + +## Commands + +```bash +npm install # install dependencies +npm run build # compile to dist/ +npm start # run compiled server (PORT, HOST env vars) +npm test # vitest unit/integration tests +npm run typecheck # tsc --noEmit +npm run lint # eslint + prettier check +npm run lint:boundaries # module boundary check +``` + +## Layout + +```text +src/ +├── app/ # composition root (only place that wires modules) +├── infrastructure/ # http server entrypoint (later: db, redis, providers) +├── modules/ # business modules, one folder each +│ └── health/ # exemplar module: public API only via index.ts +└── shared/ # cross-cutting helpers (error envelope) +``` + +## Module rules + +- A module exposes its public API only through its `index.ts`. +- Files inside a module may import: own subtree, `src/shared`, Node builtins, npm packages. +- Code outside modules (app/infrastructure) may import a module only via its `index.ts`. +- `npm run lint:boundaries` enforces these rules. diff --git a/project/design_prompt.md b/project/design_prompt.md new file mode 100644 index 0000000..8ddf456 --- /dev/null +++ b/project/design_prompt.md @@ -0,0 +1,1315 @@ +# MercadoDeVida vNext — Caveman Architecture / SDD Master Prompt + +You are a **Principal Software Architect + Senior Full-Stack Engineer**. + +You are building the new version of: + +**mercadodevida.es** + +B2C e-commerce for natural, organic and healthy products. + +This project will be developed incrementally by an **AI coding harness using SDD — Spec-Driven Development**. + +Main rule: + +> SIMPLE CODE. CLEAR MODULES. SMALL CHANGES. NO MAGIC. + +--- + +# 0. CORE PHILOSOPHY + +Build boring software. + +Boring = good. + +Prefer: + +* simple code +* explicit dependencies +* small modules +* clear APIs +* strong typing +* predictable behavior +* easy testing +* easy replacement +* easy debugging + +Avoid: + +* clever abstractions +* unnecessary microservices +* circular dependencies +* global state +* giant service classes +* giant controllers +* shared business logic everywhere +* framework magic +* premature optimization +* premature distributed systems + +The system must be easy for humans AND AI agents to understand. + +A developer must be able to modify one feature without understanding the entire application. + +--- + +# 1. ARCHITECTURE + +Start with a: + +**Modular Monolith** + +NOT microservices. + +Each business domain is an isolated module. + +Example: + +```text +src/ +├── modules/ +│ ├── auth/ +│ ├── users/ +│ ├── catalog/ +│ ├── categories/ +│ ├── pricing/ +│ ├── inventory/ +│ ├── cart/ +│ ├── checkout/ +│ ├── orders/ +│ ├── payments/ +│ ├── shipping/ +│ ├── promotions/ +│ ├── reviews/ +│ ├── seo/ +│ ├── cms/ +│ └── notifications/ +│ +├── shared/ +├── infrastructure/ +└── app/ +``` + +Modules communicate through: + +1. explicit public interfaces +2. domain/application events +3. typed contracts + +Never access another module's internal implementation. + +BAD: + +```text +checkout → directly query inventory tables +``` + +GOOD: + +```text +checkout → InventoryService.checkAvailability() +``` + +Even better when appropriate: + +```text +OrderPaid + ↓ +Inventory + ↓ +reserve/remove stock +``` + +--- + +# 2. MODULE RULE + +Every module owns its logic. + +Example: + +```text +modules/catalog/ +├── domain/ +├── application/ +├── infrastructure/ +├── api/ +├── tests/ +└── index.ts +``` + +Responsibilities: + +### domain/ + +Pure business rules. + +No database. + +No HTTP. + +No framework. + +### application/ + +Use cases. + +Example: + +```text +CreateProduct +UpdateProduct +SearchProducts +ChangePrice +ReserveStock +CreateOrder +CancelOrder +``` + +### infrastructure/ + +External systems: + +```text +PostgreSQL +Redis +Stripe +Email +Search engine +Storage +External APIs +``` + +### api/ + +HTTP/API layer. + +Controllers must be thin. + +Controller: + +```text +request +↓ +validate +↓ +use case +↓ +response +``` + +NO business logic in controllers. + +--- + +# 3. SDD IS MANDATORY + +NO FEATURE STARTS WITH CODE. + +Every change starts with a specification. + +Directory: + +```text +specs/ +``` + +Each feature gets: + +```text +specs// +├── SPEC.md +├── DESIGN.md +├── TASKS.md +└── TESTS.md +``` + +Optional: + +```text +ADR.md +MIGRATION.md +ROLLBACK.md +``` + +--- + +# 4. SPEC.md + +Before touching code define: + +```text +Problem +Goal +Non-goals +User story +Functional requirements +Business rules +Inputs +Outputs +Edge cases +Acceptance criteria +Dependencies +Security implications +SEO implications +Performance implications +``` + +Acceptance criteria MUST be testable. + +BAD: + +```text +Checkout should work correctly. +``` + +GOOD: + +```text +Given a cart containing an unavailable item +When checkout is requested +Then checkout returns HTTP 409 +And no order is created +And no payment is initiated +``` + +--- + +# 5. DESIGN.md + +Describe BEFORE implementation: + +```text +affected modules +new interfaces +API changes +database changes +events +external integrations +cache changes +security considerations +migration strategy +rollback strategy +``` + +Always include: + +```text +Modules touched: +Modules NOT touched: +``` + +If a simple feature requires touching many unrelated modules: + +STOP. + +Architecture is wrong. + +Reconsider design. + +--- + +# 6. TASKS.md + +Break implementation into small atomic tasks. + +Example: + +```text +TASK-001 Add inventory availability interface +TASK-002 Implement PostgreSQL inventory adapter +TASK-003 Add cart stock validation +TASK-004 Add checkout integration +TASK-005 Add unit tests +TASK-006 Add integration tests +TASK-007 Add API test +TASK-008 Add observability metrics +``` + +Each task must be independently understandable. + +--- + +# 7. FEATURE CHANGE RULE + +When implementing a new feature: + +FIRST inspect: + +```text +SPEC +existing domain +public module APIs +tests +database schema +events +``` + +THEN propose the smallest possible change. + +Do NOT refactor unrelated code. + +Do NOT redesign the whole application. + +Do NOT introduce a new abstraction unless at least two real use cases require it. + +--- + +# 8. TECH STACK + +Prefer a modern, boring, production-ready stack. + +Recommended baseline: + +## Frontend + +```text +Next.js +React +TypeScript +Server Components where useful +Tailwind CSS +``` + +Use SSR/SSG for SEO-sensitive content. + +Examples: + +```text +products +categories +brands +landing pages +blog +``` + +Use client-side code only where interaction requires it. + +--- + +## Backend + +Preferred: + +```text +TypeScript +Node.js +Fastify or NestJS with strict module boundaries +``` + +Alternative acceptable: + +```text +Python + FastAPI +``` + +Choose ONE. + +Do not mix backend languages without a strong reason. + +--- + +## Database + +Primary: + +```text +PostgreSQL +``` + +Use PostgreSQL for: + +```text +users +products +pricing +inventory +orders +payments +promotions +reviews +``` + +Cache: + +```text +Redis +``` + +Use Redis ONLY for: + +```text +cache +sessions +rate limiting +locks +short-lived state +``` + +Redis is NOT the source of truth. + +--- + +# 9. DATABASE RULES + +Database belongs to modules. + +Logical ownership must remain clear even if modules share one PostgreSQL instance. + +Example: + +```text +catalog_products +catalog_categories + +inventory_stock + +orders_orders +orders_items + +payments_transactions +``` + +Never let random modules query arbitrary tables. + +Access data through module interfaces. + +Use migrations. + +Never manually modify production schema. + +--- + +# 10. CORE BUSINESS MODULES + +Initial modules: + +```text +Identity +Users + +Catalog +Categories +Brands +Product Attributes + +Pricing +Promotions + +Inventory + +Cart + +Checkout + +Orders + +Payments + +Shipping + +SEO + +CMS + +Reviews + +Notifications +``` + +Each module has ONE primary responsibility. + +--- + +# 11. CATALOG + +Catalog must support: + +```text +products +variants +EAN +SKU +brands +categories +ingredients +allergens +nutrition information +images +attributes +organic/ecological certifications +product descriptions +SEO metadata +``` + +Product information may come from: + +```text +manual admin data +manufacturer information +supplier data +OpenFoodFacts +``` + +External sources NEVER overwrite trusted internal data without validation. + +Store provenance when useful. + +Example: + +```text +nutrition_source = manufacturer +nutrition_source = openfoodfacts +nutrition_source = manual +``` + +--- + +# 12. INVENTORY + +Inventory must remain isolated from catalog. + +Catalog answers: + +```text +What is this product? +``` + +Inventory answers: + +```text +Can I sell this product? +``` + +Never mix these responsibilities. + +Support: + +```text +available +reserved +sold +incoming +``` + +Stock operations must be atomic. + +Never allow negative stock. + +--- + +# 13. CART + +Cart must be independent. + +Cart contains: + +```text +product_id +variant_id +quantity +``` + +Do NOT trust stored prices. + +At checkout: + +```text +recalculate price +validate promotion +validate stock +validate taxes +validate shipping +``` + +--- + +# 14. CHECKOUT + +Checkout is an orchestrator. + +Checkout does NOT own: + +```text +products +inventory +payments +shipping +orders +``` + +Checkout coordinates them. + +Flow: + +```text +Cart + ↓ +Validate products + ↓ +Validate current prices + ↓ +Validate stock + ↓ +Calculate discounts + ↓ +Calculate shipping + ↓ +Calculate taxes + ↓ +Create Pending Order + ↓ +Reserve inventory + ↓ +Create Payment Intent + ↓ +Return payment session +``` + +Use idempotency. + +Checkout request MUST support: + +```text +idempotency_key +``` + +Duplicate requests must never create duplicate orders. + +--- + +# 15. ORDERS + +Order is historical truth. + +Order items must snapshot: + +```text +product name +SKU +EAN +unit price +discount +tax +quantity +``` + +Never calculate historical orders from current product information. + +States: + +```text +PENDING +AWAITING_PAYMENT +PAID +PROCESSING +SHIPPED +DELIVERED +CANCELLED +REFUNDED +PARTIALLY_REFUNDED +``` + +State transitions must be explicit. + +--- + +# 16. PAYMENTS + +Payment provider must be behind an interface. + +Example: + +```text +PaymentProvider +``` + +Implementation: + +```text +StripePaymentProvider +``` + +Future: + +```text +RedsysPaymentProvider +PayPalPaymentProvider +``` + +Domain code must NOT depend directly on Stripe SDK. + +Flow: + +```text +Checkout + ↓ +PaymentProvider.createPayment() + ↓ +Stripe +``` + +Webhook: + +```text +Stripe + ↓ +Webhook + ↓ +validate signature + ↓ +deduplicate event + ↓ +process event + ↓ +publish domain event +``` + +Examples: + +```text +PaymentSucceeded +PaymentFailed +PaymentRefunded +ChargebackCreated +``` + +Webhook processing MUST be idempotent. + +--- + +# 17. SEARCH + +Search must be replaceable. + +Interface: + +```text +ProductSearch +``` + +Possible implementations: + +```text +PostgreSQL Full Text Search +Meilisearch +Typesense +Elasticsearch +Algolia +``` + +Start simple. + +Do not introduce Elasticsearch unless actual scale/search requirements justify it. + +--- + +# 18. SEO IS CORE BUSINESS LOGIC + +MercadoDeVida depends heavily on organic traffic. + +SEO is NOT an afterthought. + +Support: + +```text +canonical URLs +structured data +Product schema +Breadcrumb schema +Organization schema +sitemap.xml +robots.txt +OpenGraph +metadata +category metadata +product metadata +brand metadata +redirect management +``` + +URLs must remain stable. + +Example: + +```text +/productos/ +/categoria/ +/marca/ +``` + +Never expose internal IDs in public URLs unless required. + +--- + +# 19. PERFORMANCE + +Use cache ONLY where measurable. + +Priority: + +```text +CDN +↓ +Next.js cache +↓ +Redis +↓ +PostgreSQL +``` + +Good cache targets: + +```text +product detail +category listing +navigation +SEO metadata +popular searches +``` + +Never cache blindly. + +Every cache needs: + +```text +key +TTL +invalidation strategy +source of truth +``` + +--- + +# 20. SECURITY + +Mandatory: + +```text +HTTPS +secure cookies +CSRF protection where applicable +input validation +output encoding +rate limiting +RBAC +password hashing +MFA for administrators +audit log +secrets management +dependency scanning +``` + +Never trust: + +```text +user_id from frontend +price from frontend +discount from frontend +stock from frontend +order total from frontend +payment status from frontend +``` + +Backend calculates and verifies everything. + +--- + +# 21. OBSERVABILITY + +Every important operation must expose: + +```text +structured logs +metrics +traces +errors +``` + +Use: + +```text +OpenTelemetry +Prometheus +Grafana +``` + +Important metrics: + +```text +checkout_success_total +checkout_failure_total +payment_failure_total +order_created_total +inventory_conflict_total +api_latency +database_latency +cache_hit_ratio +``` + +Every request gets: + +```text +request_id +trace_id +``` + +--- + +# 22. TESTING + +Testing pyramid: + +```text +many unit tests +some integration tests +few E2E tests +``` + +Business rules MUST have unit tests. + +Repositories MUST have integration tests. + +Critical user flows MUST have E2E tests. + +Critical E2E: + +```text +register +login +search +view product +add cart +checkout +payment +order confirmation +refund +``` + +--- + +# 23. HARNESS RULES + +The coding harness must NEVER blindly modify the repository. + +For every task: + +### STEP 1 + +Read: + +```text +SPEC.md +DESIGN.md +TASKS.md +``` + +### STEP 2 + +Inspect relevant modules only. + +### STEP 3 + +Create implementation plan. + +### STEP 4 + +Implement smallest possible change. + +### STEP 5 + +Run: + +```text +lint +typecheck +unit tests +integration tests +relevant E2E +``` + +### STEP 6 + +Compare implementation against acceptance criteria. + +### STEP 7 + +Report: + +```text +Files created +Files modified +Database migrations +API changes +Tests added +Tests passed +Known limitations +Follow-up work +``` + +--- + +# 24. BLAST RADIUS RULE + +Every change must minimize blast radius. + +Before coding report: + +```text +EXPECTED BLAST RADIUS + +Modules modified: +Modules indirectly affected: +Database changes: +API changes: +Events added/changed: +Risk level: +``` + +If blast radius is unexpectedly large: + +STOP. + +Explain why. + +Propose a better boundary. + +--- + +# 25. BACKWARD COMPATIBILITY + +Never silently break: + +```text +API contracts +database contracts +public URLs +SEO URLs +events +external integrations +``` + +Breaking changes require: + +```text +migration plan +compatibility period +rollback plan +``` + +--- + +# 26. FEATURE FLAGS + +Risky features should support feature flags. + +Example: + +```text +new_checkout +new_search +new_promotions_engine +``` + +Deployment and activation must be separate operations. + +--- + +# 27. NO BIG BANG REWRITES + +Never propose: + +```text +"rewrite everything" +``` + +Prefer: + +```text +incremental migration +module by module +feature by feature +``` + +Every stage must leave the application deployable. + +--- + +# 28. DEFINITION OF DONE + +A feature is NOT done because code exists. + +DONE means: + +```text +spec complete +design reviewed +implementation complete +tests passing +security checked +observability added +documentation updated +migration tested +rollback possible +acceptance criteria verified +``` + +--- + +# 29. RESPONSE FORMAT + +When I ask you to implement a feature, respond FIRST with: + +## 1. Understanding + +What needs to be built. + +## 2. Existing Impact + +Modules affected. + +## 3. Proposed Specification + +Business behavior. + +## 4. Architecture + +Interfaces and boundaries. + +## 5. Data Changes + +Schema/migrations. + +## 6. API Contract + +Endpoints/events. + +## 7. Implementation Tasks + +Small atomic tasks. + +## 8. Tests + +Required tests. + +## 9. Risks + +Potential failures. + +## 10. Blast Radius + +What changes and what does NOT change. + +DO NOT CODE UNTIL THE SPEC IS CLEAR. + +--- + +# 30. FINAL CAVEMAN RULES + +```text +ONE MODULE = ONE JOB + +ONE USE CASE = ONE PURPOSE + +DATABASE = SOURCE OF TRUTH + +REDIS = CACHE, NOT TRUTH + +CONTROLLER = THIN + +BUSINESS LOGIC = DOMAIN + +EXTERNAL API = ADAPTER + +NO CROSS-MODULE TABLE QUERIES + +NO GLOBAL STATE + +NO HIDDEN MAGIC + +NO COPY-PASTE BUSINESS LOGIC + +NO FEATURE WITHOUT SPEC + +NO DATABASE CHANGE WITHOUT MIGRATION + +NO CRITICAL LOGIC WITHOUT TEST + +NO EXTERNAL EVENT WITHOUT IDEMPOTENCY + +NO PAYMENT TRUST FROM FRONTEND + +NO PRICE TRUST FROM FRONTEND + +NO BIG REWRITE + +SMALL CHANGE + +TEST CHANGE + +SHIP CHANGE +``` + +--- + +# GOAL + +Build MercadoDeVida as a system where adding: + +```text +subscriptions +loyalty points +new payment provider +marketplace products +product recommendations +AI product descriptions +warehouse integration +ERP integration +supplier synchronization +new shipping provider +mobile app +B2B channel +``` + +does NOT require rewriting: + +```text +catalog +checkout +orders +payments +inventory +``` + +New functionality should normally mean: + +```text +new module ++ +small explicit integration ++ +tests +``` + +Not: + +```text +modify 30 files ++ +break 4 unrelated modules ++ +pray +``` + +Architecture must optimize for: + +**changeability > cleverness** + +**modularity > abstraction** + +**explicitness > magic** + +**maintainability > premature scalability** + +**specification before implementation** + diff --git a/project/eslint.config.mjs b/project/eslint.config.mjs new file mode 100644 index 0000000..79eafb7 --- /dev/null +++ b/project/eslint.config.mjs @@ -0,0 +1,34 @@ +// @ts-check +import eslint from '@eslint/js'; +import tseslint from 'typescript-eslint'; +import prettier from 'eslint-config-prettier'; + +export default tseslint.config( + { + ignores: ['dist/**', 'node_modules/**', 'coverage/**', 'scripts/tests/fixtures/**'], + }, + eslint.configs.recommended, + ...tseslint.configs.recommended, + prettier, + { + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }, + ], + 'no-console': ['error', { allow: ['error'] }], + }, + }, + { + files: ['**/*.mjs'], + languageOptions: { + globals: { + console: 'readonly', + process: 'readonly', + }, + }, + rules: { + 'no-console': ['error', { allow: ['error', 'log'] }], + }, + }, +); diff --git a/project/package-lock.json b/project/package-lock.json new file mode 100644 index 0000000..cfea9cd --- /dev/null +++ b/project/package-lock.json @@ -0,0 +1,3732 @@ +{ + "name": "mercadodevida-backend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mercadodevida-backend", + "version": "0.1.0", + "dependencies": { + "fastify": "^5.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "eslint": "^9.17.0", + "eslint-config-prettier": "^10.0.0", + "prettier": "^3.4.0", + "typescript": "^5.7.0", + "typescript-eslint": "^8.18.0", + "vitest": "^3.0.0" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", + "integrity": "sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.6.tgz", + "integrity": "sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.3.0", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.5.tgz", + "integrity": "sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.6.tgz", + "integrity": "sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@fastify/ajv-compiler/node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/@fastify/ajv-compiler/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.1.0.tgz", + "integrity": "sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^7.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.2.tgz", + "integrity": "sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/lzma-linux-x64-gnu": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@napi-rs/lzma-linux-x64-gnu/-/lzma-linux-x64-gnu-1.5.1.tgz", + "integrity": "sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^22.20 || ^24.12 || >=25" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.4.tgz", + "integrity": "sha512-RrPokAb7dmbxFoeO3TloqHyOjgye8RkBhSqmp4aJMIex4c9r46ZstPnleDQOq1t46VOVjwIuwNogIqbodV1Vvg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.4.tgz", + "integrity": "sha512-JKuJc+pnpks2pjy7L/N3v/cAkZxYlnmuZoD840ldbMI5KDbC4iO9NKwPKYdjYFCMAIIlBzYSFHxIJVYzRo2/8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.4.tgz", + "integrity": "sha512-krw5uS2STmvJ02x0uTXHbqQNuz+9eZ1iw+qXk9dmW2gvV4jV7O2hEoOnuhFrpOPiel1mBFtqbxYZZtC46hXLOw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.4.tgz", + "integrity": "sha512-wsTxtgApb4PrOsNJIm0FZ1h3WvCC+k9uxLJ4ad75hgoS4NiRes2SoJFlDAyMwiUY8IssDqGcHbXuN0sx1tfF1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.4.tgz", + "integrity": "sha512-GUOnQlyZe3yAXhWOtOMsn5Qkrv5E5mZXa0thbARWi5Ei2szlVXJFQhddZ4HbAzh8q92w5twp+CQvs/eFanz9YQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.4.tgz", + "integrity": "sha512-/Y7f3QuxjzPKsjA/rfEDa3+0vXqyjmJ50Ln8dPpCmWkKTrUoWHG1cWhTqaAMLob2m2nESWuC7yGrREz019Ztqg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.4.tgz", + "integrity": "sha512-81wiiX3v7aqy+T+bT61TJ78yJjRquqFFTTbAPt08imfQQzkPIW8t6aJbkTagtCCrXMNc9D66+geqlK7ydLPNqA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.4.tgz", + "integrity": "sha512-9kmDIvNZqdoHOBZgNtpTBeLWYO/LVipM3H/j62P8848/l/VPEQL6N3uxU9pvP1oZAsXyC2MEnFP3ovRjo7WYNQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.4.tgz", + "integrity": "sha512-CcnXHWnXg69g+DX5VWL3FHts3qMRN2uVEHX+BZvGLdd07/gXkn3ePjYtO1LDJvxkGKVHMclKBRa1QUTH+6toYQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.4.tgz", + "integrity": "sha512-iFOibiHnTRuhrWLlRsOQFdZJJIa7S8OwkneJr4ocALP16u5yk6lWLINFwhHaEqBFMsKDUZofLkGos7+CPzGB3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.4.tgz", + "integrity": "sha512-XnWYMI7euHlb5a871xPja+Gm7DRCFU+FGRrtS2sMq9N8FvqtpagUy6gD4YOemC5MRk9xbh8+jYMEJbigFQwsgA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.4.tgz", + "integrity": "sha512-qGDAlO0U8xedCcsdRm9oaoQY8DAx/QT7uIxJWhCdx0ceIWX783UC9QSYkdpzAe29wNiVfp24+bZdQmn49o45SQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.4.tgz", + "integrity": "sha512-ru4H6ezD7ysA5EiEK6qkkaEb4modH8CTej6kUy/gQi20u3kB3G7Zn8snXXkeJSCOFKG/rbPPtM/+9Wgas1961w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.4.tgz", + "integrity": "sha512-2W4MO5WQVJnbJaZdvDb9rhBDuFU1nKIepPFpJUBsTh2k1YY2g+ODViaWuyOAjQ5cOP7NvrvLzt3wvHOoiAvc7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.4.tgz", + "integrity": "sha512-+fxjfuoAmVMCYV5QyjoIpu0cp5DOiOTeqYFk1AVaxGr+/ravWLX89XfQmptsoWcaVy/TGf2hexzbUOrCQIL1CQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.4.tgz", + "integrity": "sha512-jTn8JfHGL4djjFxPuM06LmNUJDsst2jeVlsd9OmIH6zc5sC9K6rIuO4YajXatLUpBmBKl6b35ro1QZocLi+tcA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.4.tgz", + "integrity": "sha512-oCJCJL4pXsoDcP2QZ+JVlPTIRc6266zsIaeJJsWImmF7HO0W8nb6HuSgZlMWxJwaPf8ehbSw8yo0EUw925hKsA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.4.tgz", + "integrity": "sha512-W69hukhZ3KKNRCaMIEzKvcFye42hh0FE1+YoYaf5+Ikacuftoco6yO/xouz0hc5d5W/s3yBro5jRiuEE/Q5vUw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.4.tgz", + "integrity": "sha512-qiXbGG2jkjXhzXpsFZSR2Xpb8DN/UaxYsbb/STbuR/6fpaDgRmmaq1B/LmtF2wQFOFOSsK2jdE0RZ3a0zHn4QA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.4.tgz", + "integrity": "sha512-nWeM//hxv8mIo6jD7Hu4o48DVmV9pbV6gsKaWU+4NFyqHoPKwrkRiZGLKUhOBk8qNmDmpwFtPKg80Bo/Tn4xiQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.4.tgz", + "integrity": "sha512-s62SQ/vgsRSvMwDkOEfTqfgASF0f26ZNaQuTA6Aok5lrikf89yI2W0gFHvZb2Jpgc6N8JnOKZgCK2iciO3CsxQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.4.tgz", + "integrity": "sha512-J6wGf8TVGbXJq+HH+ttTvrcfNKPbuZecV6KT1B8I18BC5IURUh5kl4Yl5OEP5eFIUoI5BWxCsyYMhFsDx8kekw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.4.tgz", + "integrity": "sha512-zmfrQd/0wu6oJs8Vq8KwY/YtsKSsLtKe/HwAP4Wqy8LhWjeT55fHRAkOhYQ12wI3ayS4Tt12d5CDRD7N96SAYQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.4.tgz", + "integrity": "sha512-qPzHqdj9rfUD+w79dtE07zi/kFwKyCJqplp5K5ygeLTp7jLpAoc16OAH39HSmRC9UpozaecsleI8uAdEj6v2yw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.4.tgz", + "integrity": "sha512-zD6NdeWEByGE9QF9vCrlJ5YQB4oq9q91kPZS37Jwj5hOkvR1lTBSpsKhKDw4IJtbQ35LsTS1HD9DZYGKIshU1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.67.0.tgz", + "integrity": "sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/type-utils": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.67.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.6.tgz", + "integrity": "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.67.0.tgz", + "integrity": "sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.67.0.tgz", + "integrity": "sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.67.0", + "@typescript-eslint/types": "^8.67.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.67.0.tgz", + "integrity": "sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.67.0.tgz", + "integrity": "sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.67.0.tgz", + "integrity": "sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.67.0.tgz", + "integrity": "sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.67.0.tgz", + "integrity": "sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.67.0", + "@typescript-eslint/tsconfig-utils": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/visitor-keys": "8.67.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz", + "integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.8" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.67.0.tgz", + "integrity": "sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.67.0", + "@typescript-eslint/types": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.67.0.tgz", + "integrity": "sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.67.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.7.tgz", + "integrity": "sha512-E8eBXaKibuvH2pSZErOjdVb5vF4PbKYcrnluBTYxEk1l/VhhwZg1kZQsdtjq+CsF5CFydf2Rdkz7jDHKSisi3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.7.tgz", + "integrity": "sha512-Trr0hYO9CM3Wj6ksWHRhK9IZpIY6wTMO5u/MqXurMxT57sWBaOPEtP3Oq60ihZuh5JsiagKfz95OcxdEP6dBrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.7.tgz", + "integrity": "sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.7.tgz", + "integrity": "sha512-sB9y4ovltoQP+WaUPwmSxO9WIg9Ig694Di5PalVPsYHklAdE027mehpWF2SQSVq+k6sFgaivbTjTJwZLSHbedA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.7", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.7.tgz", + "integrity": "sha512-7C+MwShwtBSI5Buwoyg3s/iY1eHL9PKAf+O1wVh/TdnjXUtkoL/9YQtre90i4MtNXM6edP1wJ2zOBpfCyhIS7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.7.tgz", + "integrity": "sha512-Q2eQGI6d2L/hBtZ0qNuKcAGid68XK6cv1xsoaIma6PaJhHPoqcEJhYpXZ/5myCMqkNgtP6UKuBhbc0nHKnrkuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.7.tgz", + "integrity": "sha512-x6BDOd7dyo3PFLY3I9/HJ25X/6OurhGXk2/B9gOZNPF7XDVjeBK4k01lQE5uvDpbuheErh91qYuE1E2OEjK3Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.7", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.18.0.tgz", + "integrity": "sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats/node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ajv-formats/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.3.0.tgz", + "integrity": "sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.5", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.5.tgz", + "integrity": "sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.6", + "@eslint/js": "9.39.5", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.4.0.tgz", + "integrity": "sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-7.0.1.tgz", + "integrity": "sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^4.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/fast-json-stringify/node_modules/ajv/node_modules/fast-uri": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-json-stringify/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-4.1.2.tgz", + "integrity": "sha512-TyGmBcbDTZXcb2cj5MV89DrF42DKvb3y5DDUNh95iO+IMeAzMkVSxK1PZRrRIpc9yg8U2GhGdbofNa0LS/a4Bw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.12.0", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.12.0.tgz", + "integrity": "sha512-A3RNEaDIHWaxFW8n8rNJaW1wQ+XAXuoU71llfUQJjuh5WaYLmKfRhhanaJBOx8m2EBPQkR6sDBovYdHNe9F6rA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^7.0.0", + "find-my-way": "^9.6.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.1.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-my-way": { + "version": "9.7.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.7.0.tgz", + "integrity": "sha512-f2JHn75x2JlwUwLenZypgczR7YWMb/uO9BvUXtus+JMgkbIkLADd38cI4EiV+OQqrGo1Zlq6V8wnqMJ8e62wUQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/ipaddr.js": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.5.0.tgz", + "integrity": "sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", + "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.18", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz", + "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", + "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.26", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz", + "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.17", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/process-warning": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.1.0.tgz", + "integrity": "sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rollup": { + "version": "4.62.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.4.tgz", + "integrity": "sha512-RXOqwaPsBGjMNMa4sQjDjHieHEZDFoj/Rdr46l2MU5DfEs16wHJPC2RPTPHWhNl+M3aI472LLqFkFKut4SblOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@napi-rs/lzma-linux-x64-gnu": "1.5.1", + "@rollup/rollup-android-arm-eabi": "4.62.4", + "@rollup/rollup-android-arm64": "4.62.4", + "@rollup/rollup-darwin-arm64": "4.62.4", + "@rollup/rollup-darwin-x64": "4.62.4", + "@rollup/rollup-freebsd-arm64": "4.62.4", + "@rollup/rollup-freebsd-x64": "4.62.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.4", + "@rollup/rollup-linux-arm-musleabihf": "4.62.4", + "@rollup/rollup-linux-arm64-gnu": "4.62.4", + "@rollup/rollup-linux-arm64-musl": "4.62.4", + "@rollup/rollup-linux-loong64-gnu": "4.62.4", + "@rollup/rollup-linux-loong64-musl": "4.62.4", + "@rollup/rollup-linux-ppc64-gnu": "4.62.4", + "@rollup/rollup-linux-ppc64-musl": "4.62.4", + "@rollup/rollup-linux-riscv64-gnu": "4.62.4", + "@rollup/rollup-linux-riscv64-musl": "4.62.4", + "@rollup/rollup-linux-s390x-gnu": "4.62.4", + "@rollup/rollup-linux-x64-gnu": "4.62.4", + "@rollup/rollup-linux-x64-musl": "4.62.4", + "@rollup/rollup-openbsd-x64": "4.62.4", + "@rollup/rollup-openharmony-arm64": "4.62.4", + "@rollup/rollup-win32-arm64-msvc": "4.62.4", + "@rollup/rollup-win32-ia32-msvc": "4.62.4", + "@rollup/rollup-win32-x64-gnu": "4.62.4", + "@rollup/rollup-win32-x64-msvc": "4.62.4", + "fsevents": "~2.3.2" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thread-stream": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.2.0.tgz", + "integrity": "sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==", + "license": "MIT", + "dependencies": { + "real-require": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/thread-stream/node_modules/real-require": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-1.0.0.tgz", + "integrity": "sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.4.tgz", + "integrity": "sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==", + "license": "MIT", + "engines": { + "node": ">=20" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.67.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.67.0.tgz", + "integrity": "sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.67.0", + "@typescript-eslint/parser": "8.67.0", + "@typescript-eslint/typescript-estree": "8.67.0", + "@typescript-eslint/utils": "8.67.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "7.3.6", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.6.tgz", + "integrity": "sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0 || ^0.28.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.7.tgz", + "integrity": "sha512-KrxIJ62Fd89gfysR4WotlgZABiz2dqFPgqGzX7s+CwsqLFomRH7777ZcrOD6+WVAh7khPQP41A+BKbpcJFrdEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.7", + "@vitest/mocker": "3.2.7", + "@vitest/pretty-format": "^3.2.7", + "@vitest/runner": "3.2.7", + "@vitest/snapshot": "3.2.7", + "@vitest/spy": "3.2.7", + "@vitest/utils": "3.2.7", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.7", + "@vitest/ui": "3.2.7", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/project/package.json b/project/package.json new file mode 100644 index 0000000..dce0cd0 --- /dev/null +++ b/project/package.json @@ -0,0 +1,30 @@ +{ + "name": "mercadodevida-backend", + "version": "0.1.0", + "private": true, + "type": "module", + "description": "MercadoDeVida vNext backend - modular monolith skeleton", + "engines": { + "node": ">=22" + }, + "scripts": { + "build": "tsc -p tsconfig.build.json", + "start": "node dist/infrastructure/http/server.js", + "lint": "eslint . && prettier --check .", + "lint:boundaries": "node scripts/check-module-boundaries.mjs src", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test": "vitest run" + }, + "dependencies": { + "fastify": "^5.2.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "eslint": "^9.17.0", + "eslint-config-prettier": "^10.0.0", + "prettier": "^3.4.0", + "typescript": "^5.7.0", + "typescript-eslint": "^8.18.0", + "vitest": "^3.0.0" + } +} diff --git a/project/scripts/check-module-boundaries.mjs b/project/scripts/check-module-boundaries.mjs new file mode 100644 index 0000000..94c0987 --- /dev/null +++ b/project/scripts/check-module-boundaries.mjs @@ -0,0 +1,112 @@ +#!/usr/bin/env node +/** + * Module boundary checker. + * + * Rules (see specs/F-001-scaffold/DESIGN.md): + * R1: Files inside /modules// may only import their own module + * subtree, /shared/, Node builtins, or npm packages. + * R2: Files outside modules may import a module only through its index.ts. + * Deep imports into /modules//... are violations. + * + * Usage: node scripts/check-module-boundaries.mjs + * Exit codes: 0 = clean, 1 = violations found, 2 = usage error. + */ + +import { readdirSync, readFileSync, statSync } from 'node:fs'; +import path from 'node:path'; +import process from 'node:process'; + +function isBareSpecifier(specifier) { + return !specifier.startsWith('.') && !specifier.startsWith('/'); +} + +function walk(dir, files = []) { + for (const entry of readdirSync(dir)) { + const full = path.join(dir, entry); + const stat = statSync(full); + if (stat.isDirectory()) { + if (entry === 'node_modules' || entry === 'dist' || entry === 'fixtures') continue; + walk(full, files); + } else if (entry.endsWith('.ts') && !entry.endsWith('.d.ts')) { + files.push(full); + } + } + return files; +} + +function extractSpecifiers(source) { + const specifiers = []; + const fromRegex = /(?:import|export)\s+[^'"]*?from\s*['"]([^'"]+)['"]/g; + const sideEffectRegex = /^\s*import\s*['"]([^'"]+)['"]/gm; + const dynamicRegex = /import\(\s*['"]([^'"]+)['"]\s*\)/g; + let match; + while ((match = fromRegex.exec(source)) !== null) specifiers.push(match[1]); + while ((match = sideEffectRegex.exec(source)) !== null) specifiers.push(match[1]); + while ((match = dynamicRegex.exec(source)) !== null) specifiers.push(match[1]); + return specifiers; +} + +/** Strip a .js/.ts extension so we can compare logical paths. */ +function stripExtension(p) { + return p.replace(/\.(js|ts|mjs|cjs)$/, ''); +} + +function checkFile(file, rootAbs, violations) { + const source = readFileSync(file, 'utf8'); + const relFile = path.relative(rootAbs, file); + const fileDir = path.dirname(file); + const relFileParts = relFile.split(path.sep); + const sourceInModule = + relFileParts[0] === 'modules' && relFileParts.length >= 2 ? relFileParts[1] : null; + + for (const specifier of extractSpecifiers(source)) { + if (isBareSpecifier(specifier)) continue; // npm package or node builtin + + const targetAbs = stripExtension(path.resolve(fileDir, specifier)); + const relTarget = path.relative(rootAbs, targetAbs); + const targetParts = relTarget.split(path.sep); + const targetInModule = + targetParts[0] === 'modules' && targetParts.length >= 2 ? targetParts[1] : null; + + if (sourceInModule !== null) { + // R1: stay inside own module or go to shared + const ownModule = relTarget.startsWith(path.join('modules', sourceInModule) + path.sep); + const toShared = targetParts[0] === 'shared'; + if (!ownModule && !toShared) { + violations.push( + `R1 violation: ${relFile} imports "${specifier}" (escapes module "${sourceInModule}")`, + ); + } + } else if (targetInModule !== null) { + // R2: outside code may only use a module's public index + const isIndex = targetParts.length === 3 && targetParts[2] === 'index'; + if (!isIndex) { + violations.push( + `R2 violation: ${relFile} imports "${specifier}" (deep import into module "${targetInModule}", use its index)`, + ); + } + } + } +} + +async function main() { + const rootArg = process.argv[2]; + if (!rootArg) { + console.error('Usage: node scripts/check-module-boundaries.mjs '); + process.exit(2); + } + const rootAbs = path.resolve(rootArg); + const files = walk(rootAbs); + const violations = []; + for (const file of files) { + checkFile(file, rootAbs, violations); + } + if (violations.length > 0) { + for (const violation of violations) console.error(violation); + console.error(`Boundary check FAILED: ${violations.length} violation(s)`); + process.exit(1); + } + console.log(`Boundary check OK: ${files.length} file(s) checked`); +} + +await main(); diff --git a/project/scripts/tests/boundary-checker.test.ts b/project/scripts/tests/boundary-checker.test.ts new file mode 100644 index 0000000..9e9ef63 --- /dev/null +++ b/project/scripts/tests/boundary-checker.test.ts @@ -0,0 +1,49 @@ +import { execFile } from 'node:child_process'; +import path from 'node:path'; +import process from 'node:process'; +import { fileURLToPath } from 'node:url'; +import { describe, expect, it } from 'vitest'; + +const here = path.dirname(fileURLToPath(import.meta.url)); +const scriptPath = path.resolve(here, '..', 'check-module-boundaries.mjs'); +const fixturesDir = path.resolve(here, 'fixtures'); + +function runChecker(rootDir: string): Promise<{ code: number; output: string }> { + return new Promise((resolve) => { + execFile( + process.execPath, + [scriptPath, rootDir], + { encoding: 'utf8' }, + (error, stdout, stderr) => { + const code = error !== null && 'code' in error ? (error.code as number) : 0; + resolve({ code, output: `${stdout}\n${stderr}` }); + }, + ); + }); +} + +describe('check-module-boundaries', () => { + it('passes on a clean tree', async () => { + const { code, output } = await runChecker(path.join(fixturesDir, 'ok')); + expect(output).toContain('Boundary check OK'); + expect(code).toBe(0); + }); + + it('fails when a module imports another module internal file (R1)', async () => { + const { code, output } = await runChecker(path.join(fixturesDir, 'cross-module-internal')); + expect(output).toContain('R1 violation'); + expect(code).toBe(1); + }); + + it('fails when outside code deep-imports a module internal file (R2)', async () => { + const { code, output } = await runChecker(path.join(fixturesDir, 'deep-from-app')); + expect(output).toContain('R2 violation'); + expect(code).toBe(1); + }); + + it('allows outside code to import a module index (R2 ok)', async () => { + const { code, output } = await runChecker(path.join(fixturesDir, 'ok')); + expect(output).toContain('Boundary check OK'); + expect(code).toBe(0); + }); +}); diff --git a/project/scripts/tests/fixtures/cross-module-internal/modules/alpha/api/routes.ts b/project/scripts/tests/fixtures/cross-module-internal/modules/alpha/api/routes.ts new file mode 100644 index 0000000..d91bc3d --- /dev/null +++ b/project/scripts/tests/fixtures/cross-module-internal/modules/alpha/api/routes.ts @@ -0,0 +1,2 @@ +import { secret } from '../../beta/domain/secret.js'; +export const routes = [secret]; diff --git a/project/scripts/tests/fixtures/cross-module-internal/modules/beta/domain/secret.ts b/project/scripts/tests/fixtures/cross-module-internal/modules/beta/domain/secret.ts new file mode 100644 index 0000000..69cbbfa --- /dev/null +++ b/project/scripts/tests/fixtures/cross-module-internal/modules/beta/domain/secret.ts @@ -0,0 +1 @@ +export const secret = 'secret'; diff --git a/project/scripts/tests/fixtures/deep-from-app/app/main.ts b/project/scripts/tests/fixtures/deep-from-app/app/main.ts new file mode 100644 index 0000000..64d2f4b --- /dev/null +++ b/project/scripts/tests/fixtures/deep-from-app/app/main.ts @@ -0,0 +1,2 @@ +import { routes } from '../modules/alpha/api/routes.js'; +export const main = [routes]; diff --git a/project/scripts/tests/fixtures/deep-from-app/modules/alpha/api/routes.ts b/project/scripts/tests/fixtures/deep-from-app/modules/alpha/api/routes.ts new file mode 100644 index 0000000..2536e6f --- /dev/null +++ b/project/scripts/tests/fixtures/deep-from-app/modules/alpha/api/routes.ts @@ -0,0 +1 @@ +export const routes = 'routes'; diff --git a/project/scripts/tests/fixtures/ok/app/main.ts b/project/scripts/tests/fixtures/ok/app/main.ts new file mode 100644 index 0000000..771104c --- /dev/null +++ b/project/scripts/tests/fixtures/ok/app/main.ts @@ -0,0 +1,3 @@ +import { alpha } from '../modules/alpha/index.js'; +import { beta } from '../modules/beta/index.js'; +export const main = [alpha, beta]; diff --git a/project/scripts/tests/fixtures/ok/modules/alpha/api/routes.ts b/project/scripts/tests/fixtures/ok/modules/alpha/api/routes.ts new file mode 100644 index 0000000..e0e93d8 --- /dev/null +++ b/project/scripts/tests/fixtures/ok/modules/alpha/api/routes.ts @@ -0,0 +1,3 @@ +import { alpha } from '../index.js'; +import { sharedUtil } from '../../../shared/util.js'; +export const routes = [alpha, sharedUtil]; diff --git a/project/scripts/tests/fixtures/ok/modules/alpha/index.ts b/project/scripts/tests/fixtures/ok/modules/alpha/index.ts new file mode 100644 index 0000000..17562cd --- /dev/null +++ b/project/scripts/tests/fixtures/ok/modules/alpha/index.ts @@ -0,0 +1 @@ +export const alpha = 'alpha'; diff --git a/project/scripts/tests/fixtures/ok/modules/beta/index.ts b/project/scripts/tests/fixtures/ok/modules/beta/index.ts new file mode 100644 index 0000000..a56b919 --- /dev/null +++ b/project/scripts/tests/fixtures/ok/modules/beta/index.ts @@ -0,0 +1 @@ +export const beta = 'beta'; diff --git a/project/scripts/tests/fixtures/ok/shared/util.ts b/project/scripts/tests/fixtures/ok/shared/util.ts new file mode 100644 index 0000000..78a7c95 --- /dev/null +++ b/project/scripts/tests/fixtures/ok/shared/util.ts @@ -0,0 +1 @@ +export const sharedUtil = 'shared'; diff --git a/project/src/app/build-app.ts b/project/src/app/build-app.ts new file mode 100644 index 0000000..f7c61c4 --- /dev/null +++ b/project/src/app/build-app.ts @@ -0,0 +1,31 @@ +import Fastify, { type FastifyInstance } from 'fastify'; +import type { FastifyError, FastifyReply, FastifyRequest } from 'fastify'; +import { registerHealthRoutes } from '../modules/health/index.js'; +import { errorEnvelope } from '../shared/errors.js'; + +function notFoundHandler(_request: FastifyRequest, reply: FastifyReply): void { + void reply.code(404).send(errorEnvelope(404, 'Not Found')); +} + +function errorHandler(error: FastifyError, _request: FastifyRequest, reply: FastifyReply): void { + const statusCode = + error.statusCode !== undefined && error.statusCode >= 400 ? error.statusCode : 500; + const message = statusCode >= 500 ? 'Internal Server Error' : error.message; + void reply.code(statusCode).send(errorEnvelope(statusCode, message)); +} + +/** + * Composition root. The only place allowed to wire modules together. + */ +export async function buildApp(): Promise { + const app = Fastify({ logger: false }); + + app.setErrorHandler(errorHandler); + app.setNotFoundHandler(notFoundHandler); + + await app.register(async (instance) => { + await registerHealthRoutes(instance); + }); + + return app; +} diff --git a/project/src/app/tests/build-app.test.ts b/project/src/app/tests/build-app.test.ts new file mode 100644 index 0000000..74db7e1 --- /dev/null +++ b/project/src/app/tests/build-app.test.ts @@ -0,0 +1,25 @@ +import { describe, expect, it } from 'vitest'; +import { buildApp } from '../build-app.js'; + +describe('composition root', () => { + it('exposes GET /health through the wired app', async () => { + const app = await buildApp(); + const response = await app.inject({ method: 'GET', url: '/health' }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ status: 'ok' }); + await app.close(); + }); + + it('returns 404 JSON envelope for unknown routes without leaking internals', async () => { + const app = await buildApp(); + const response = await app.inject({ method: 'GET', url: '/does-not-exist' }); + + expect(response.statusCode).toBe(404); + const body = response.json() as { error: { statusCode: number; message: string } }; + expect(body.error.statusCode).toBe(404); + expect(body.error.message).toBe('Not Found'); + expect(response.body).not.toContain('stack'); + await app.close(); + }); +}); diff --git a/project/src/infrastructure/http/server.ts b/project/src/infrastructure/http/server.ts new file mode 100644 index 0000000..f15f60c --- /dev/null +++ b/project/src/infrastructure/http/server.ts @@ -0,0 +1,12 @@ +import { buildApp } from '../../app/build-app.js'; + +const port = Number(process.env.PORT ?? 3000); +const host = process.env.HOST ?? '0.0.0.0'; + +try { + const app = await buildApp(); + await app.listen({ port, host }); +} catch (error) { + console.error('Failed to start HTTP server', error); + process.exit(1); +} diff --git a/project/src/modules/health/api/health.routes.ts b/project/src/modules/health/api/health.routes.ts new file mode 100644 index 0000000..8c66e3b --- /dev/null +++ b/project/src/modules/health/api/health.routes.ts @@ -0,0 +1,11 @@ +import type { FastifyInstance } from 'fastify'; + +interface HealthResponse { + status: 'ok'; +} + +export async function registerHealthRoutes(app: FastifyInstance): Promise { + app.get('/health', async (): Promise => { + return { status: 'ok' }; + }); +} diff --git a/project/src/modules/health/index.ts b/project/src/modules/health/index.ts new file mode 100644 index 0000000..f4803b4 --- /dev/null +++ b/project/src/modules/health/index.ts @@ -0,0 +1,5 @@ +/** + * Public API of the health module. Everything a module exposes to the + * outside world goes through this file. + */ +export { registerHealthRoutes } from './api/health.routes.js'; diff --git a/project/src/modules/health/tests/health.test.ts b/project/src/modules/health/tests/health.test.ts new file mode 100644 index 0000000..6e1ba4e --- /dev/null +++ b/project/src/modules/health/tests/health.test.ts @@ -0,0 +1,18 @@ +import Fastify from 'fastify'; +import { describe, expect, it } from 'vitest'; +import { registerHealthRoutes } from '../index.js'; + +describe('health module API', () => { + it('GET /health returns 200 and status ok', async () => { + const app = Fastify({ logger: false }); + await app.register(async (instance) => { + await registerHealthRoutes(instance); + }); + + const response = await app.inject({ method: 'GET', url: '/health' }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ status: 'ok' }); + await app.close(); + }); +}); diff --git a/project/src/shared/errors.ts b/project/src/shared/errors.ts new file mode 100644 index 0000000..a63c2a5 --- /dev/null +++ b/project/src/shared/errors.ts @@ -0,0 +1,15 @@ +/** + * Shared error envelope. Single shape for every API error. + * Never leak stack traces or internal details to the client. + */ + +export interface ErrorEnvelope { + error: { + statusCode: number; + message: string; + }; +} + +export function errorEnvelope(statusCode: number, message: string): ErrorEnvelope { + return { error: { statusCode, message } }; +} diff --git a/project/tsconfig.build.json b/project/tsconfig.build.json new file mode 100644 index 0000000..2adef06 --- /dev/null +++ b/project/tsconfig.build.json @@ -0,0 +1,12 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "noEmit": false, + "outDir": "dist", + "rootDir": "src", + "declaration": false, + "sourceMap": true + }, + "include": ["src/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/project/tsconfig.json b/project/tsconfig.json new file mode 100644 index 0000000..3074621 --- /dev/null +++ b/project/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2023"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true, + "esModuleInterop": true, + "isolatedModules": true, + "noEmit": true + }, + "include": ["src/**/*.ts", "scripts/tests/**/*.ts"], + "exclude": ["node_modules", "dist", "scripts/tests/fixtures"] +} diff --git a/project/vitest.config.ts b/project/vitest.config.ts new file mode 100644 index 0000000..f0648d5 --- /dev/null +++ b/project/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + environment: 'node', + include: ['src/**/*.test.ts', 'scripts/tests/**/*.test.ts'], + exclude: ['node_modules/**', 'dist/**', 'scripts/tests/fixtures/**'], + }, +}); diff --git a/scripts/agent_status.py b/scripts/agent_status.py new file mode 100755 index 0000000..ed6b870 --- /dev/null +++ b/scripts/agent_status.py @@ -0,0 +1,371 @@ +#!/usr/bin/env python3 +import argparse +import json +import re +from datetime import datetime, timezone +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +STATUS_PATH = ROOT / 'work' / 'runtime-status.json' +MATRIX_PATH = ROOT / 'harness' / 'agents.matrix.yml' +WORKFLOW_PATH = ROOT / 'harness' / 'workflow.stages.yml' +ARTIFACTS_DIR = ROOT / 'work' / 'artifacts' +VALID_RUNTIME_STATES = {'idle', 'waiting', 'running', 'blocked', 'done'} + +DEFAULT_EMOJIS = { + 'leader': '🧭', + 'triager': '🧩', + 'architect': '🏗️', + 'implementer': '🛠️', + 'reviewer': '🔍', + 'security': '🔒', + 'qa': '🧪', + 'documenter': '📝', +} + +GATE_FILES = { + 'reviewer': 'reviewer.json', + 'security': 'security.json', + 'qa': 'qa.json', + 'leader': 'leader-close.json', +} + +AGENT_FILES = { + 'architect': 'architect.md', + 'implementer': 'implementer.md', + 'reviewer': 'reviewer.json', + 'security': 'security.json', + 'qa': 'qa.json', + 'documenter': 'documenter.md', + 'leader': 'leader-close.json', +} + + +def now_iso(): + return datetime.now(timezone.utc).replace(microsecond=0).isoformat().replace('+00:00', 'Z') + + +def load_json(path: Path, default=None): + if not path.exists(): + return default + return json.loads(path.read_text(encoding='utf-8')) + + +def save_json(path: Path, payload): + path.write_text(json.dumps(payload, indent=2, ensure_ascii=False) + '\n', encoding='utf-8') + + +def load_role_emojis(): + emojis = dict(DEFAULT_EMOJIS) + if not MATRIX_PATH.exists(): + return emojis + current_role = None + for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines(): + match_role = re.match(r'^ ([a-z_]+):\s*$', line) + if match_role: + current_role = match_role.group(1) + continue + match_emoji = re.match(r'^\s{4}emoji:\s*["\']?(.*?)["\']?\s*$', line) + if match_emoji and current_role: + emojis[current_role] = match_emoji.group(1) + return emojis + + +def load_roles(): + roles = [] + if not MATRIX_PATH.exists(): + return roles + for line in MATRIX_PATH.read_text(encoding='utf-8').splitlines(): + match_role = re.match(r'^ ([a-z_]+):\s*$', line) + if match_role: + roles.append(match_role.group(1)) + return roles + + +def load_stage_names(): + stages = [] + if not WORKFLOW_PATH.exists(): + return stages + for line in WORKFLOW_PATH.read_text(encoding='utf-8').splitlines(): + match_stage = re.match(r'^ - name:\s*([a-z_]+)\s*$', line) + if match_stage: + stages.append(match_stage.group(1)) + return stages + + +def default_status(): + return { + 'feature_id': None, + 'stage': 'idle', + 'agent': 'leader', + 'action': 'Sin ejecución activa', + 'state': 'waiting', + 'next_agent': 'leader', + 'waiting_for': 'Seleccionar una feature pending y actualizar este estado', + 'updated_at': now_iso(), + 'timeline': [], + } + + +def load_status(): + status = load_json(STATUS_PATH, default_status()) + base = default_status() + for key, value in base.items(): + status.setdefault(key, value) + if not isinstance(status.get('timeline'), list): + status['timeline'] = [] + return status + + +def gate_status(feature_id): + gates = {} + if not feature_id: + return gates + feature_dir = ARTIFACTS_DIR / feature_id + for gate, filename in GATE_FILES.items(): + path = feature_dir / filename + if not path.exists(): + gates[gate] = 'pending' + continue + try: + payload = json.loads(path.read_text(encoding='utf-8')) + gates[gate] = 'approved' if payload.get('verdict') == 'APPROVED' else 'present' + except Exception: + gates[gate] = 'invalid' + return gates + + +def render_gate(gate, state, emojis): + icon = { + 'approved': '✅', + 'pending': '⏳', + 'present': '⚠️', + 'invalid': '❌', + }.get(state, '•') + label = { + 'leader': 'close', + }.get(gate, gate) + return f"{icon} {emojis.get(gate, '•')} {label}: {state.upper()}" + + +def artifact_status(feature_id, agent): + if not feature_id: + return 'pending' + filename = AGENT_FILES.get(agent) + if not filename: + return 'pending' + path = ARTIFACTS_DIR / feature_id / filename + if not path.exists(): + return 'pending' + if filename.endswith('.md'): + return 'done' + try: + payload = json.loads(path.read_text(encoding='utf-8')) + return 'done' if payload.get('verdict') == 'APPROVED' else 'present' + except Exception: + return 'invalid' + + +def render_artifact(agent, state, current_agent, emojis): + icon = { + 'done': '✅', + 'pending': '▶️' if agent == current_agent else '⏳', + 'present': '⚠️', + 'invalid': '❌', + }.get(state, '•') + return f"{icon} {emojis.get(agent, '•')} {agent}: {state.upper()}" + + +STAGE_OWNERS = { + 'idle': 'leader', + 'intake': 'leader', + 'design': 'architect', + 'build': 'implementer', + 'review_gate': 'reviewer', + 'security_gate': 'security', + 'qa_gate': 'qa', + 'document': 'documenter', + 'close': 'leader', +} + +STAGE_PREREQUISITES = { + 'review_gate': [('implementer.md', None)], + 'security_gate': [('implementer.md', None), ('reviewer.json', 'APPROVED')], + 'qa_gate': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED')], + 'document': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED'), ('qa.json', 'APPROVED')], + 'close': [('implementer.md', None), ('reviewer.json', 'APPROVED'), ('security.json', 'APPROVED'), ('qa.json', 'APPROVED')], +} + + +def artifact_ok(feature_id, filename, verdict=None): + path = ARTIFACTS_DIR / feature_id / filename + if not path.exists(): + return False + if verdict is None: + return True + try: + return json.loads(path.read_text(encoding='utf-8')).get('verdict') == verdict + except Exception: + return False + + +def validate_stage_transition(status, args): + stage = args.stage or status.get('stage') or 'idle' + agent = args.agent or status.get('agent') or STAGE_OWNERS.get(stage) + feature_id = args.feature_id if args.feature_id is not None else status.get('feature_id') + + expected_owner = STAGE_OWNERS.get(stage) + if expected_owner and agent != expected_owner: + raise SystemExit(f"Stage {stage} debe ejecutarlo {expected_owner}, no {agent}") + + if stage != 'idle' and not feature_id: + raise SystemExit(f"Stage {stage} requiere feature_id") + + if feature_id: + for filename, verdict in STAGE_PREREQUISITES.get(stage, []): + if not artifact_ok(feature_id, filename, verdict): + suffix = f" con verdict {verdict}" if verdict else "" + raise SystemExit(f"No se puede entrar en {stage}: falta {filename}{suffix} para {feature_id}") + + +def validate_runtime_args(args): + roles = set(load_roles()) or set(DEFAULT_EMOJIS) + stages = set(load_stage_names()) | {'idle'} + + if args.agent is not None and args.agent not in roles: + raise SystemExit(f"Invalid agent: {args.agent}. Allowed: {', '.join(sorted(roles))}") + if args.next_agent is not None and args.next_agent not in roles: + raise SystemExit(f"Invalid next-agent: {args.next_agent}. Allowed: {', '.join(sorted(roles))}") + if args.stage is not None and args.stage not in stages: + raise SystemExit(f"Invalid stage: {args.stage}. Allowed: {', '.join(sorted(stages))}") + if args.state is not None and args.state not in VALID_RUNTIME_STATES: + raise SystemExit(f"Invalid state: {args.state}. Allowed: {', '.join(sorted(VALID_RUNTIME_STATES))}") + + +def show_status(): + status = load_status() + emojis = load_role_emojis() + feature_id = status.get('feature_id') or '—' + current_agent = status.get('agent', 'leader') + next_agent = status.get('next_agent') or '—' + gates = gate_status(status.get('feature_id')) + + print('╔══════════════════════════════════════════════════════════════╗') + print('║ Orquestra · Runtime Status ║') + print('╚══════════════════════════════════════════════════════════════╝') + print(f"Feature activa : {feature_id}") + print(f"Stage actual : {status.get('stage', '—')}") + print(f"Agente actual : {emojis.get(current_agent, '•')} {current_agent}") + print(f"Acción : {status.get('action', '—')}") + print(f"Estado : {status.get('state', '—')}") + print(f"Siguiente : {emojis.get(next_agent, '•')} {next_agent}") + print(f"Esperando : {status.get('waiting_for', '—')}") + print(f"Actualizado : {status.get('updated_at', '—')}") + print() + print('Gates') + if gates: + for gate in ['reviewer', 'security', 'qa', 'leader']: + print(f" {render_gate(gate, gates.get(gate, 'pending'), emojis)}") + else: + print(' — Sin feature activa —') + print() + print('Artifacts') + if status.get('feature_id'): + for agent in ['architect', 'implementer', 'reviewer', 'security', 'qa', 'documenter', 'leader']: + state = artifact_status(status.get('feature_id'), agent) + print(f" {render_artifact(agent, state, current_agent, emojis)}") + else: + print(' — Sin feature activa —') + print() + print('Timeline') + timeline = status.get('timeline', [])[-8:] + if not timeline: + print(' — Sin eventos —') + return + for item in timeline: + agent = item.get('agent', 'leader') + emoji = emojis.get(agent, '•') + ts = item.get('ts', '—') + stage = item.get('stage', '—') + state = item.get('state', '—') + message = item.get('message', '') + print(f" - {ts} · {emoji} {agent} · {stage} · {state} · {message}") + + +def set_status(args): + validate_runtime_args(args) + status = load_status() + validate_stage_transition(status, args) + if args.feature_id is not None: + status['feature_id'] = args.feature_id or None + if args.stage is not None: + status['stage'] = args.stage + if args.agent is not None: + status['agent'] = args.agent + if args.action is not None: + status['action'] = args.action + if args.state is not None: + status['state'] = args.state + if args.next_agent is not None: + status['next_agent'] = args.next_agent + if args.waiting_for is not None: + status['waiting_for'] = args.waiting_for + + status['updated_at'] = now_iso() + event_message = args.note or status.get('action') or 'Estado actualizado' + status['timeline'].append({ + 'ts': status['updated_at'], + 'agent': status.get('agent', 'leader'), + 'stage': status.get('stage', '—'), + 'state': status.get('state', '—'), + 'message': event_message, + }) + status['timeline'] = status['timeline'][-20:] + save_json(STATUS_PATH, status) + show_status() + + +def reset_status(_args): + status = default_status() + status['updated_at'] = now_iso() + save_json(STATUS_PATH, status) + show_status() + + +def build_parser(): + parser = argparse.ArgumentParser(description='Renderiza y actualiza el estado visible de Orquestra.') + sub = parser.add_subparsers(dest='command', required=True) + + sub.add_parser('show', help='Muestra el panel visible de estado') + + set_parser = sub.add_parser('set', help='Actualiza el estado runtime y añade evento a timeline') + set_parser.add_argument('--feature-id') + set_parser.add_argument('--stage') + set_parser.add_argument('--agent') + set_parser.add_argument('--action') + set_parser.add_argument('--state') + set_parser.add_argument('--next-agent') + set_parser.add_argument('--waiting-for') + set_parser.add_argument('--note') + + sub.add_parser('reset', help='Resetea el estado runtime a idle') + return parser + + +def main(): + parser = build_parser() + args = parser.parse_args() + if args.command == 'show': + show_status() + elif args.command == 'set': + set_status(args) + elif args.command == 'reset': + reset_status(args) + else: + parser.print_help() + return 1 + return 0 + + +if __name__ == '__main__': + raise SystemExit(main()) diff --git a/scripts/install.sh b/scripts/install.sh new file mode 100755 index 0000000..21802af --- /dev/null +++ b/scripts/install.sh @@ -0,0 +1,165 @@ +#!/usr/bin/env bash +set -euo pipefail + +banner() { + cat <<'EOF' + ___ ____ ___ _ _ _____ ____ _____ ____ _ + / _ \| _ \ / _ \| | | | ____/ ___|_ _| _ \ / \ + | | | | |_) | | | | | | | _| \___ \ | | | |_) | / _ \ + | |_| | _ <| |_| | |_| | |___ ___) || | | _ < / ___ \ + \___/|_| \_\\__\_\\___/|_____|____/ |_| |_| \_\/_/ \_\ + + sequential orchestration runtime +EOF +} + +usage() { + cat <<'EOF' +Usage: ./scripts/install.sh /path/to/project-repo + +Install or update Orquestra from this source checkout into a target project repo. +Project-owned progress files are created only when missing and are never overwritten. +EOF +} + +fail() { + printf '[FAIL] %s\n' "$1" >&2 + exit 1 +} + +warn() { + printf '[WARN] %s\n' "$1" >&2 +} + +info() { + printf '[OK] %s\n' "$1" +} + +copy_file_update() { + local src=$1 + local dst=$2 + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" +} + +copy_file_if_missing() { + local src=$1 + local dst=$2 + mkdir -p "$(dirname "$dst")" + if [ ! -e "$dst" ]; then + cp "$src" "$dst" + fi +} + +copy_dir_update() { + local src=$1 + local dst=$2 + mkdir -p "$dst" + cp -R "$src"/. "$dst"/ +} + +copy_dir_if_missing_contents() { + local src=$1 + local dst=$2 + local dir file rel + mkdir -p "$dst" + if [ -d "$src" ]; then + find "$src" -type d | while IFS= read -r dir; do + rel=${dir#"$src"} + mkdir -p "$dst$rel" + done + find "$src" -type f | while IFS= read -r file; do + rel=${file#"$src"/} + copy_file_if_missing "$file" "$dst/$rel" + done + fi +} + +append_gitignore_block() { + local gitignore=$1 + touch "$gitignore" + if ! grep -q '^# BEGIN ORQUESTRA$' "$gitignore"; then + cat >>"$gitignore" <<'EOF' + +# BEGIN ORQUESTRA +__pycache__/ +*.pyc +.pytest_cache/ +.codegraph/ +.atl/ +# END ORQUESTRA +EOF + fi +} + +if [ "$#" -ne 1 ]; then + usage >&2 + exit 2 +fi + +banner + +command -v pi >/dev/null 2>&1 || fail "pi is required in PATH before installing Orquestra" +command -v python3 >/dev/null 2>&1 || fail "python3 is required in PATH before installing Orquestra" + +SOURCE_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P) +TARGET_ARG=$1 +TARGET_DIR=$(python3 -c 'import os, sys; print(os.path.realpath(sys.argv[1]))' "$TARGET_ARG") + +if [ "$TARGET_DIR" = "$SOURCE_DIR" ]; then + fail "target repo must be different from the Orquestra source repo" +fi + +mkdir -p "$TARGET_DIR" + +if [ ! -d "$TARGET_DIR/.git" ]; then + git -C "$TARGET_DIR" init >/dev/null + info "Initialized git repo in $TARGET_DIR" +fi + +# Harness-owned files: update on every run. +for file in AGENTS.md README.md HOWTO.md CHECKPOINTS.md; do + copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file" +done + +copy_dir_update "$SOURCE_DIR/harness" "$TARGET_DIR/harness" +copy_dir_update "$SOURCE_DIR/platforms/pi" "$TARGET_DIR/platforms/pi" + +for file in scripts/verify.sh scripts/agent_status.py scripts/new_ticket.py scripts/pi_orquestra.sh scripts/install.sh; do + copy_file_update "$SOURCE_DIR/$file" "$TARGET_DIR/$file" +done + +append_gitignore_block "$TARGET_DIR/.gitignore" + +# Project-owned/state files: create only when missing. +copy_dir_if_missing_contents "$SOURCE_DIR/project" "$TARGET_DIR/project" +copy_file_if_missing "$SOURCE_DIR/backlog/features.json" "$TARGET_DIR/backlog/features.json" +copy_dir_if_missing_contents "$SOURCE_DIR/spec" "$TARGET_DIR/spec" +copy_file_if_missing "$SOURCE_DIR/work/current.md" "$TARGET_DIR/work/current.md" +copy_file_if_missing "$SOURCE_DIR/work/history.md" "$TARGET_DIR/work/history.md" +copy_file_if_missing "$SOURCE_DIR/work/runtime-status.json" "$TARGET_DIR/work/runtime-status.json" +mkdir -p "$TARGET_DIR/work/artifacts" +if [ -f "$SOURCE_DIR/work/artifacts/.gitkeep" ] && [ ! -e "$TARGET_DIR/work/artifacts/.gitkeep" ]; then + cp "$SOURCE_DIR/work/artifacts/.gitkeep" "$TARGET_DIR/work/artifacts/.gitkeep" +fi + +# Pi project-local runtime extensions. +mkdir -p "$TARGET_DIR/.pi/extensions" +copy_dir_update "$SOURCE_DIR/platforms/pi/extensions/orquestra-status" "$TARGET_DIR/.pi/extensions/orquestra-status" +copy_file_update "$SOURCE_DIR/platforms/pi/extensions/orquestra-web-fetch.ts" "$TARGET_DIR/.pi/extensions/orquestra-web-fetch.ts" + +if [ -d "$TARGET_DIR/.pi/subagents" ] || [ -f "$TARGET_DIR/.pi/subagents.json" ]; then + warn "Found .pi/subagents or .pi/subagents.json in target. Orquestra does not delete user Pi config automatically; remove Orquestra-owned stale subagent files manually before running verify.sh." +fi + +chmod +x "$TARGET_DIR/scripts/verify.sh" "$TARGET_DIR/scripts/pi_orquestra.sh" "$TARGET_DIR/scripts/install.sh" + +cat </dev/null 2>&1; then + echo "[FAIL] pi is required before running Orquestra" >&2 + exit 1 +fi + +STATUS_EXT=".pi/extensions/orquestra-status" +WEB_FETCH_EXT=".pi/extensions/orquestra-web-fetch.ts" + +if [ ! -f "$STATUS_EXT/index.ts" ] || [ ! -f "$WEB_FETCH_EXT" ]; then + echo "[FAIL] Orquestra Pi extensions are not installed in this project." >&2 + echo " Run: /path/to/orquestra/scripts/install.sh $ROOT" >&2 + exit 1 +fi + +exec pi --no-extensions --no-skills -e "$STATUS_EXT" -e "$WEB_FETCH_EXT" "$@" diff --git a/scripts/verify.sh b/scripts/verify.sh new file mode 100755 index 0000000..a6507a8 --- /dev/null +++ b/scripts/verify.sh @@ -0,0 +1,215 @@ +#!/usr/bin/env bash +set -u + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +NC='\033[0m' + +ok() { printf "${GREEN}[OK]${NC} %s\n" "$1"; } +warn() { printf "${YELLOW}[WARN]${NC} %s\n" "$1"; } +fail() { printf "${RED}[FAIL]${NC} %s\n" "$1"; } + +EXIT_CODE=0 + +echo "── 1) Verificando estructura base ─────────────────────" +required=( + "AGENTS.md" + "CHECKPOINTS.md" + "harness/agents.matrix.yml" + "harness/workflow.stages.yml" + "harness/policies/governance.md" + "harness/policies/security.md" + "harness/policies/quality.md" + "harness/contracts/handoff.md" + "harness/contracts/evidence.schema.json" + "harness/model-routing.yml" + "spec/product.md" + "spec/tech.md" + "spec/acceptance.md" + "backlog/features.json" + "work/current.md" + "work/history.md" + "work/runtime-status.json" + "scripts/install.sh" + "scripts/agent_status.py" + "scripts/new_ticket.py" + "scripts/pi_orquestra.sh" + "platforms/pi/README.md" + "platforms/pi/extensions/orquestra-status/index.ts" + "platforms/pi/extensions/orquestra-web-fetch.ts" +) + +for f in "${required[@]}"; do + if [ -f "$f" ]; then + ok "Existe $f" + else + fail "Falta $f" + EXIT_CODE=1 + fi +done + +required_dirs=("project") +for d in "${required_dirs[@]}"; do + if [ -d "$d" ]; then + ok "Existe directorio $d" + else + fail "Falta directorio $d" + EXIT_CODE=1 + fi +done + +root_product_files=$(find . -mindepth 1 -maxdepth 1 -type f \( \ + -name '*.py' -o -name '*.js' -o -name '*.ts' -o -name '*.go' -o -name '*.rs' -o \ + -name '*.java' -o -name '*.php' -o -name '*.rb' \ +\) -print 2>/dev/null) +if [ -n "$root_product_files" ]; then + fail "Archivos de producto/código en la raíz no permitidos; usar project/: $root_product_files" + EXIT_CODE=1 +else + ok "Sin archivos de producto/código en la raíz" +fi + +echo "" +echo "── 2) Validando requisitos Pi ─────────────────────────" +if command -v pi >/dev/null 2>&1; then + ok "Pi instalado: $(command -v pi)" +else + fail "Pi es obligatorio antes de instalar Orquestra" + EXIT_CODE=1 +fi + +if [ -d ".pi/subagents" ] || [ -f ".pi/subagents.json" ]; then + fail "Orquestra es secuencial: no debe instalar .pi/subagents ni .pi/subagents.json" + EXIT_CODE=1 +else + ok "Sin subagentes project-local" +fi + +if [ -d ".pi/extensions" ]; then + extra_extensions=$(find .pi/extensions -mindepth 1 -maxdepth 1 \ + ! -name orquestra-status \ + ! -name orquestra-web-fetch.ts \ + -print 2>/dev/null) + if [ -n "$extra_extensions" ]; then + fail "Extensiones project-local no declaradas: $extra_extensions" + EXIT_CODE=1 + else + ok "Extensiones project-local declaradas" + fi +else + warn "Sin .pi/extensions; la instalación debe copiar extensiones Orquestra en proyectos" +fi + +echo "" +echo "── 3) Validando backlog + runtime ─────────────────────" +python3 - <<'PY' +import json +import pathlib +import sys + +root = pathlib.Path('.') +path = root / 'backlog' / 'features.json' + +try: + data = json.loads(path.read_text(encoding='utf-8')) +except Exception as e: + print(f"[FAIL] backlog/features.json inválido: {e}") + sys.exit(1) + +valid = set(data.get('rules', {}).get('valid_status', ["pending", "in_progress", "blocked", "done"])) +features = data.get('features', []) +if not isinstance(features, list): + print('[FAIL] features debe ser una lista') + sys.exit(1) + +in_progress = [f for f in features if f.get('status') == 'in_progress'] +if len(in_progress) > 1: + print(f"[FAIL] Hay {len(in_progress)} features in_progress (máximo 1)") + sys.exit(1) + +for f in features: + fid = str(f.get('id', '')).strip() + status = f.get('status') + if status not in valid: + print(f"[FAIL] Estado inválido en feature {fid}: {status}") + sys.exit(1) + + if status == 'done': + d = root / 'work' / 'artifacts' / fid + req = ['reviewer.json', 'security.json', 'qa.json', 'leader-close.json'] + missing = [name for name in req if not (d / name).is_file()] + if missing: + print(f"[FAIL] Feature {fid} done sin artefactos: {', '.join(missing)}") + sys.exit(1) + + expected = { + 'reviewer.json': 'reviewer', + 'security.json': 'security', + 'qa.json': 'qa', + 'leader-close.json': 'leader', + } + for filename, agent in expected.items(): + try: + obj = json.loads((d / filename).read_text(encoding='utf-8')) + except Exception as e: + print(f"[FAIL] {fid}/{filename} inválido: {e}") + sys.exit(1) + + if obj.get('agent') != agent: + print(f"[FAIL] {fid}/{filename} agent debe ser '{agent}'") + sys.exit(1) + if obj.get('verdict') != 'APPROVED': + print(f"[FAIL] {fid}/{filename} no está APPROVED") + sys.exit(1) + +runtime_path = root / 'work' / 'runtime-status.json' +try: + runtime = json.loads(runtime_path.read_text(encoding='utf-8')) +except Exception as e: + print(f"[FAIL] work/runtime-status.json inválido: {e}") + sys.exit(1) + +required_runtime = ['feature_id', 'stage', 'agent', 'action', 'state', 'next_agent', 'waiting_for', 'updated_at', 'timeline'] +missing_runtime = [key for key in required_runtime if key not in runtime] +if missing_runtime: + print(f"[FAIL] work/runtime-status.json incompleto: {', '.join(missing_runtime)}") + sys.exit(1) +if not isinstance(runtime.get('timeline'), list): + print('[FAIL] work/runtime-status.json timeline debe ser una lista') + sys.exit(1) + +print(f"[OK] backlog válido ({len(features)} features)") +print('[OK] runtime-status válido') +PY +if [ $? -ne 0 ]; then EXIT_CODE=1; fi + +echo "" +echo "── 4) Verificación de tests/build (opcional auto-detect) ─" +if [ -f "Makefile" ] && grep -qE '^test:' Makefile; then + if make test; then ok "make test OK"; else fail "make test falló"; EXIT_CODE=1; fi +elif [ -f "package.json" ]; then + if command -v npm >/dev/null 2>&1; then + if npm test --silent --if-present; then ok "npm test OK"; else fail "npm test falló"; EXIT_CODE=1; fi + else + warn "package.json detectado pero npm no está disponible" + fi +elif [ -d "tests" ]; then + if command -v pytest >/dev/null 2>&1; then + if pytest -q; then ok "pytest OK"; else fail "pytest falló"; EXIT_CODE=1; fi + else + if python3 -m unittest discover -s tests -v; then ok "unittest OK"; else fail "unittest falló"; EXIT_CODE=1; fi + fi +else + ok "Sin suite de producto detectada; no aplica al harness base" +fi + +echo "" +echo "── 5) Resumen ─────────────────────────────────────────" +if [ $EXIT_CODE -eq 0 ]; then + ok "Orquestra verificado. Puedes trabajar." +else + fail "Orquestra NO verificado. Corrige antes de continuar." +fi + +exit $EXIT_CODE diff --git a/spec/acceptance.md b/spec/acceptance.md new file mode 100644 index 0000000..7b010fd --- /dev/null +++ b/spec/acceptance.md @@ -0,0 +1,9 @@ +# Acceptance Criteria + +Define criterios verificables por feature. + +Formato recomendado: +- Feature ID: +- Escenario: +- Given / When / Then: +- Evidencia esperada (test/comando): diff --git a/spec/product.md b/spec/product.md new file mode 100644 index 0000000..6e7828c --- /dev/null +++ b/spec/product.md @@ -0,0 +1,15 @@ +# Product Spec + +## Problema +Describe el problema de negocio. + +## Objetivo +Define el resultado esperado del producto. + +## Usuarios +- Usuario principal: +- Usuario secundario: + +## Alcance v1 +- In scope: +- Out of scope: diff --git a/spec/roadmap.md b/spec/roadmap.md new file mode 100644 index 0000000..3f046a9 --- /dev/null +++ b/spec/roadmap.md @@ -0,0 +1,52 @@ +# MercadoDeVida vNext — Roadmap + +Source of truth for intent: project/design_prompt.md +Source of truth for execution: backlog/features.json (one feature at a time, gates before done) + +Stack decisions (fixed, boring): + +- Backend: TypeScript + Node.js + Fastify (modular monolith) +- Frontend: Next.js + React + TypeScript + Tailwind (SSG/ISR for SEO pages) +- Database: PostgreSQL (source of truth) + Redis (cache only) +- Product code lives in project/ + +## Waves + +### Wave 0 — Foundation (F-001 … F-004) + +Skeleton, migrations, request context, config/flags. Nothing business yet. +Exit: build/lint/typecheck/test green, dev DB up with one command. + +### Wave 1 — Identity & Users (F-005 … F-006) + +Accounts, sessions, RBAC. Exit: customer and admin identities work. + +### Wave 2 — Catalog (F-007 … F-012) + +Categories → products → brands → variants/rich data → images → search. +Exit: full catalog model behind public module APIs, search replaceable. + +### Wave 3 — Storefront & SEO (F-013 … F-015) + +Next.js shell → catalog pages (SSG/ISR, stable URLs) → structured data, sitemap, redirects. +Exit: SEO-critical public surface is live and crawlable. + +### Wave 4 — Commerce core (F-016 … F-023) + +Inventory → pricing → cart → promotions → shipping → orders → checkout → payments. +This is the money path. Order matters; do not skip dependencies. +Exit: customer pays, order becomes PAID, stock is correct under concurrency. + +### Wave 5 — Post-purchase & cross-cutting (F-024 … F-030) + +Notifications, reviews, CMS, caching, security hardening, observability, E2E. +Exit: production-grade cross-cutting concerns with the money path protected end to end. + +## Execution rules (from design_prompt.md) + +1. NO FEATURE WITHOUT SPEC: each ticket produces specs//SPEC.md, DESIGN.md, TASKS.md, TESTS.md during the design stage. +2. Smallest possible change; no unrelated refactors. +3. Every change reports expected blast radius before coding. +4. Modules communicate only through public interfaces or domain events. +5. Never trust frontend for price, stock, discount, payment status or user identity. +6. ./scripts/verify.sh green is a precondition for closing any ticket. diff --git a/spec/tech.md b/spec/tech.md new file mode 100644 index 0000000..6ddc087 --- /dev/null +++ b/spec/tech.md @@ -0,0 +1,30 @@ +# Technical Spec — MercadoDeVida vNext + +## Stack +- Lenguaje: TypeScript (strict) — backend y frontend +- Framework backend: Fastify 5 (monolito modular; elegido por tipado, simplicidad y ausencia de magia) +- Framework frontend: Next.js + React + Tailwind (llega en F-013; SSG/ISR para SEO) +- Runtime: Node.js >= 22 +- Base de datos: PostgreSQL (fuente de verdad) + Redis (solo caché) — desde F-002 + +## Dependencias (justificación obligatoria por política de seguridad) + +| Dependencia | Versión | Justificación | +|---|---|---| +| fastify | ^5.2 | Servidor HTTP tipado, maintained, sin magia; única dep de runtime en F-001 | +| typescript | ^5.7 | Tipado estricto (dev) | +| vitest | ^3 | Tests unit/integración TS-native (dev) | +| eslint + @eslint/js + typescript-eslint + eslint-config-prettier | ^9 / ^8 / ^10 | Linting estándar (dev) | +| prettier | ^3 | Formato consistente (dev) | + +Regla: toda dependencia nueva debe agregarse a esta tabla con justificación en el ticket que la introduce. + +## Restricciones +- Seguridad: nunca confiar en frontend para precio/stock/descuento/estado de pago; envelopes de error sin stack traces; sin secretos en repo +- Rendimiento: caché solo donde sea medible (F-027) +- Compatibilidad: URLs públicas estables /productos/, /categoria/, /marca/ + +## Observabilidad +- Logging: estructurado con request_id (F-003) +- Métricas: Prometheus con métricas de negocio (F-029) +- Alertas: fuera de alcance inicial diff --git a/specs/F-001-scaffold/DESIGN.md b/specs/F-001-scaffold/DESIGN.md new file mode 100644 index 0000000..eebd809 --- /dev/null +++ b/specs/F-001-scaffold/DESIGN.md @@ -0,0 +1,78 @@ +# DESIGN — F-001 Scaffold modular monolith skeleton + +## Affected modules +- New: `project/` application skeleton (no business modules yet, only exemplar `health` module). + +## Modules touched +- `src/modules/health` (exemplar module proving the layout and boundary rule) +- `src/shared` (error envelope helper) +- `src/infrastructure/http` (Fastify server bootstrap) +- `src/app` (composition root) + +## Modules NOT touched +- Everything else. No business modules exist yet. No harness files outside `work/artifacts/` and `specs/`. + +## New interfaces +- `health` module public API: `registerHealthRoutes(app: FastifyInstance): Promise` exported only from `src/modules/health/index.ts`. +- Boundary checker script: `node scripts/check-module-boundaries.mjs src` → exit 0 ok / exit 1 violation. + +## API changes +- Adds `GET /health` → `200 {"status":"ok"}`. + +## Database changes +- None. + +## Events +- None. + +## External integrations +- None. + +## Cache changes +- None. + +## Security considerations +- Fastify default JSON error handler replaced with an envelope that never leaks stack traces. +- No dependencies beyond Fastify + toolchain. + +## Layout + +```text +project/ +├── package.json +├── tsconfig.json +├── eslint.config.mjs +├── vitest.config.ts +├── .gitignore +├── scripts/ +│ └── check-module-boundaries.mjs +└── src/ + ├── app/ + │ └── build-app.ts # composition root: wires modules + ├── infrastructure/ + │ └── http/ + │ └── server.ts # listen entrypoint + ├── modules/ + │ └── health/ + │ ├── index.ts # public API + │ ├── api/ + │ │ └── health.routes.ts + │ └── tests/ + │ └── health.test.ts + └── shared/ + └── errors.ts +``` + +## Boundary rules enforced by the checker +1. Files inside `src/modules//` may only import: own subtree (relative), `src/shared/...` (relative or alias-free path), Node builtins, and npm packages. +2. Any relative import escaping `src/modules//` toward another module or toward `src/app`/`src/infrastructure` is a violation. +3. Files outside modules (`src/app`, `src/infrastructure`) may import a module only via its `index.ts` (direct deep import = violation). + +## Toolchain +- TypeScript strict, Fastify 5, Vitest, ESLint (flat config) + Prettier, tsc build to `dist/`. + +## Migration strategy +- None (greenfield). + +## Rollback strategy +- Delete `project/` content added by this ticket; no other system depends on it yet. diff --git a/specs/F-001-scaffold/SPEC.md b/specs/F-001-scaffold/SPEC.md new file mode 100644 index 0000000..4068da5 --- /dev/null +++ b/specs/F-001-scaffold/SPEC.md @@ -0,0 +1,55 @@ +# SPEC — F-001 Scaffold modular monolith skeleton + +## Problem +No codebase exists. MercadoDeVida vNext needs a boring, typed, modular home before any business feature. + +## Goal +TypeScript modular monolith skeleton with strict module boundaries and a green toolchain. + +## Non-goals +- No business logic +- No database access +- No frontend + +## User story +As a developer (human or AI), I can run install/build/lint/typecheck/test from `project/` and get a running HTTP app with a health endpoint, so every later feature starts from a known green base. + +## Functional requirements +1. `project/` contains a Node + TypeScript app with Fastify. +2. Layout: `src/modules/`, `src/shared/`, `src/infrastructure/`, `src/app/`. +3. Every module folder exposes its public API through `index.ts`. +4. A boundary checker fails when a module imports another module's internal files. +5. `GET /health` returns 200 `{"status":"ok"}`. + +## Business rules +- Modules communicate only through public interfaces (`index.ts`). +- Composition happens only in `src/app` (composition root). + +## Inputs +- `GET /health`: none. + +## Outputs +- `GET /health`: `200 application/json {"status":"ok"}`. + +## Edge cases +- Invalid route → 404 JSON envelope. +- Server start failure (port busy) → non-zero exit with clear error. + +## Acceptance criteria +1. `npm install`, `npm run build`, `npm run lint`, `npm run typecheck`, `npm test` all green in `project/`. +2. `GET /health` returns HTTP 200 with `{"status":"ok"}`. +3. `src/modules`, `src/shared`, `src/infrastructure`, `src/app` exist. +4. A module importing another module's internal file fails the boundary check (demonstrated by test). +5. `./scripts/verify.sh` green at repo root. + +## Dependencies +- None (first ticket). + +## Security implications +- No secrets, no auth surface. Error responses must not leak stack traces. + +## SEO implications +- None. + +## Performance implications +- None measurable yet; baseline latency observable via test only. diff --git a/specs/F-001-scaffold/TASKS.md b/specs/F-001-scaffold/TASKS.md new file mode 100644 index 0000000..b063368 --- /dev/null +++ b/specs/F-001-scaffold/TASKS.md @@ -0,0 +1,13 @@ +# TASKS — F-001 Scaffold modular monolith skeleton + +- TASK-001 Init `project/` package: package.json (type module), tsconfig strict, .gitignore +- TASK-002 Add Fastify + shared error envelope + `build-app.ts` composition root +- TASK-003 Add `health` exemplar module (index.ts public API, routes, tests) +- TASK-004 Add HTTP server entrypoint (`src/infrastructure/http/server.ts`) +- TASK-005 Add boundary checker script + fixtures proving it fails on violations +- TASK-006 Add toolchain: ESLint flat config (incl. no deep cross-module imports), Prettier, Vitest +- TASK-007 Wire npm scripts: build, lint, lint:boundaries, typecheck, test, start +- TASK-008 Write project README with run commands +- TASK-009 Run full verification and write implementer evidence + +Each task is independently understandable and leaves the tree consistent. diff --git a/specs/F-001-scaffold/TESTS.md b/specs/F-001-scaffold/TESTS.md new file mode 100644 index 0000000..a53f6ce --- /dev/null +++ b/specs/F-001-scaffold/TESTS.md @@ -0,0 +1,25 @@ +# TESTS — F-001 Scaffold modular monolith skeleton + +## Unit / integration (Vitest) +- `src/modules/health/tests/health.test.ts`: module-scoped; registers the module public API on a bare Fastify instance; `GET /health` → 200 and `{"status":"ok"}`. +- `src/app/tests/build-app.test.ts`: composition root; health wired through `buildApp()`; unknown route → 404 JSON envelope without stack leak. +- `scripts/tests/boundary-checker.test.ts`: run `check-module-boundaries.mjs` against fixture trees: + - fixture `ok` (imports inside own module + shared) → exit 0 + - fixture `cross-module-internal` (imports another module's `domain/`) → exit 1 + - fixture `deep-from-app` (app imports `modules/x/api/...` directly) → exit 1 + +## Toolchain checks (manual/CI commands) +- `npm run build` exits 0 +- `npm run typecheck` exits 0 +- `npm run lint` exits 0 +- `npm run lint:boundaries` exits 0 on clean tree +- `npm test` exits 0 + +## Acceptance traceability +| Criterion | Evidence | +|---|---| +| install/build/lint/typecheck/test green | command outputs in implementer.md | +| GET /health 200 ok | health.test.ts + build-app.test.ts | +| src/{modules,shared,infrastructure,app} exist | tree listing in implementer.md | +| boundary violation fails lint | boundary-checker.test.ts fixtures | +| verify.sh green | qa.json evidence | diff --git a/work/artifacts/.gitkeep b/work/artifacts/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/work/artifacts/F-001/architect.md b/work/artifacts/F-001/architect.md new file mode 100644 index 0000000..604f27c --- /dev/null +++ b/work/artifacts/F-001/architect.md @@ -0,0 +1,31 @@ +# Architect — F-001 Scaffold modular monolith skeleton + +done -> work/artifacts/F-001/architect.md + +## Decision summary +- Backend skeleton: Node 22 + TypeScript strict + Fastify 5, ESM, tsc build. No framework magic, no aliases (relative imports only) so runtime resolution stays trivial. +- Composition root: `src/app/build-app.ts` is the only place that wires modules onto the HTTP server. +- Boundary enforcement: explicit script `scripts/check-module-boundaries.mjs` wired into `lint:boundaries`, tested with fixtures. ESLint additionally blocks deep `@module` style imports is NOT needed because we use no aliases; the script is the single source of truth. +- Spec-first artifacts at `specs/F-001-scaffold/` (SPEC, DESIGN, TASKS, TESTS). + +## Expected blast radius + +```text +EXPECTED BLAST RADIUS + +Modules modified: none (greenfield; new project/ skeleton only) +Modules indirectly affected: none +Database changes: none +API changes: adds GET /health +Events added/changed: none +Risk level: low +``` + +## Stack justification (boring choices) +- Fastify: maintained, typed, small, no magic. +- Vitest: TS-native, same mental model as the test pyramid required by design_prompt. +- ESLint flat config + Prettier: standard. +- No ORM, no Redis client, no config framework yet — each arrives with the ticket that needs it. + +## Handoff to implementer +Follow `specs/F-001-scaffold/TASKS.md` in order. Definition of done for build stage: all five commands green and evidence written to `work/artifacts/F-001/implementer.md`. diff --git a/work/artifacts/F-001/documenter.md b/work/artifacts/F-001/documenter.md new file mode 100644 index 0000000..6be9e19 --- /dev/null +++ b/work/artifacts/F-001/documenter.md @@ -0,0 +1,22 @@ +# Documenter — F-001 Scaffold modular monolith skeleton + +done -> work/artifacts/F-001/documenter.md + +## Why documentation applies +F-001 introduces new developer-facing contracts: npm scripts, module boundary rules, and project layout. + +## Documents reviewed/produced +- `project/README.md` (created at build stage, verified accurate): + - requirements (Node >= 22, npm) + - all 6 npm scripts match package.json exactly: build, start, lint, lint:boundaries, typecheck, test + - layout diagram matches actual tree + - module rules match scripts/check-module-boundaries.mjs behavior (R1/R2) +- `specs/F-001-scaffold/` SPEC/DESIGN/TASKS/TESTS kept consistent with final implementation (TESTS.md updated at build stage). +- `spec/tech.md` completed with stack and dependency justification table (security policy requirement). + +## Verification +- `grep` cross-check: README command list == package.json scripts keys (build, start, lint, lint:boundaries, typecheck, test) -> match. +- No API docs needed yet beyond /health (documented in implementer.md and SPEC.md). + +## Pending docs (not this ticket) +- API reference will start being meaningful from F-005 onward. diff --git a/work/artifacts/F-001/implementer.md b/work/artifacts/F-001/implementer.md new file mode 100644 index 0000000..7f4b946 --- /dev/null +++ b/work/artifacts/F-001/implementer.md @@ -0,0 +1,55 @@ +# Implementer — F-001 Scaffold modular monolith skeleton + +done -> work/artifacts/F-001/implementer.md + +## Files created (all under project/) +- package.json, tsconfig.json, tsconfig.build.json, eslint.config.mjs, vitest.config.ts +- .prettierrc.json, .prettierignore, .gitignore, README.md +- scripts/check-module-boundaries.mjs +- scripts/tests/boundary-checker.test.ts +- scripts/tests/fixtures/{ok,cross-module-internal,deep-from-app}/ (boundary test fixtures) +- src/app/build-app.ts (composition root) +- src/app/tests/build-app.test.ts +- src/infrastructure/http/server.ts (listen entrypoint) +- src/modules/health/index.ts, src/modules/health/api/health.routes.ts, src/modules/health/tests/health.test.ts +- src/shared/errors.ts + +## Files modified +- specs/F-001-scaffold/TESTS.md (test locations updated to final layout) + +## Database migrations +- None. + +## API changes +- Adds GET /health -> 200 {"status":"ok"} +- Adds 404 JSON envelope: {"error":{"statusCode":404,"message":"Not Found"}} +- Error handler never leaks stack traces (500 -> generic message). + +## Tests added +- health module API test (module-scoped, no cross-module imports) +- composition root tests (health wired, 404 envelope without stack leak) +- boundary checker tests: ok tree, R1 violation fixture, R2 violation fixture, index import allowed + +## Tests passed (evidence) +``` +npm run lint -> OK (eslint + prettier) +npm run lint:boundaries -> Boundary check OK: 7 file(s) checked +npm run typecheck -> exit 0 +npm run build -> exit 0 (dist generated) +npm test -> Test Files 3 passed (3), Tests 7 passed (7) +smoke: PORT=3999 node dist/infrastructure/http/server.js + curl /health -> HTTP 200 {"status":"ok"} + curl /nope -> {"error":{"statusCode":404,"message":"Not Found"}} +``` + +## Known limitations +- Boundary checker covers static import/export specifiers only (no require(), no runtime reflection). Acceptable for TS ESM codebase. +- No request_id / structured logging yet (F-003 scope). +- No CI pipeline defined (repo-level decision, out of F-001 scope). + +## Follow-up work +- F-002 database foundation, F-003 request context, F-004 config/flags (per backlog dependencies). + +## Notes +- The boundary checker caught a real violation during build (health test importing src/app). Fixed by moving composition tests to src/app/tests and keeping module tests module-scoped. This is the rule working as designed. +- design_prompt.md added to .prettierignore: it is a project input document, not product code. diff --git a/work/artifacts/F-001/leader-close.json b/work/artifacts/F-001/leader-close.json new file mode 100644 index 0000000..2f70d95 --- /dev/null +++ b/work/artifacts/F-001/leader-close.json @@ -0,0 +1,29 @@ +{ + "feature_id": "F-001", + "agent": "leader", + "verdict": "APPROVED", + "summary": "F-001 closed. Scaffold modular monolith implemented in project/, all gates APPROVED, verify.sh exit 0.", + "gates": { + "reviewer": "APPROVED (reviewer.json)", + "security": "APPROVED (security.json)", + "qa": "APPROVED (qa.json)", + "verify_sh": "exit 0" + }, + "deliverables": [ + "project/: TypeScript + Fastify modular monolith skeleton (7 source files + toolchain)", + "boundary checker with tested fixtures enforcing module rules", + "specs/F-001-scaffold/ SPEC, DESIGN, TASKS, TESTS", + "spec/tech.md with dependency justification" + ], + "next_feature_hint": "F-002 (database foundation) - only depends on F-001", + "evidence": [ + "work/artifacts/F-001/architect.md", + "work/artifacts/F-001/implementer.md", + "work/artifacts/F-001/reviewer.json", + "work/artifacts/F-001/security.json", + "work/artifacts/F-001/qa.json", + "work/artifacts/F-001/documenter.md", + "./scripts/verify.sh exit 0 at close" + ], + "timestamp": "2026-08-14T19:52:00Z" +} diff --git a/work/artifacts/F-001/qa.json b/work/artifacts/F-001/qa.json new file mode 100644 index 0000000..77d8a5f --- /dev/null +++ b/work/artifacts/F-001/qa.json @@ -0,0 +1,42 @@ +{ + "feature_id": "F-001", + "agent": "qa", + "verdict": "APPROVED", + "summary": "All 5 acceptance criteria verified with fresh executions against the final tree. No regressions (greenfield baseline).", + "traceability": [ + { + "criterion": "AC1: npm install, build, lint, typecheck, test all green in project/", + "test": "fresh run of npm run build && typecheck && lint && lint:boundaries && test", + "result": "PASS (all exit 0; 3 test files / 7 tests passed)" + }, + { + "criterion": "AC2: GET /health returns 200 {\"status\":\"ok\"}", + "test": "vitest inject tests (health.test.ts, build-app.test.ts) + live smoke: PORT=3998 node dist/infrastructure/http/server.js; curl /health", + "result": "PASS (HTTP 200 {\"status\":\"ok\"} live)" + }, + { + "criterion": "AC3: src/modules, src/shared, src/infrastructure, src/app exist", + "test": "ls -d over project/src", + "result": "PASS (all four directories present)" + }, + { + "criterion": "AC4: cross-module internal import fails boundary check", + "test": "node scripts/check-module-boundaries.mjs scripts/tests/fixtures/cross-module-internal + boundary-checker.test.ts fixtures", + "result": "PASS (exit 1, R1 violation reported)" + }, + { + "criterion": "AC5: ./scripts/verify.sh green at repo root", + "test": "./scripts/verify.sh", + "result": "PASS (exit 0, 'Orquestra verificado')" + } + ], + "regressions": "n/a - greenfield ticket, no prior behavior to regress", + "evidence": [ + "project/: npm run build/typecheck/lint/lint:boundaries/test -> all exit 0", + "npm test -> Test Files 3 passed (3), Tests 7 passed (7)", + "live smoke on PORT=3998 -> 200 {\"status\":\"ok\"}", + "boundary checker on violation fixture -> exit 1 with R1 violation message", + "./scripts/verify.sh -> exit 0" + ], + "timestamp": "2026-08-14T19:50:00Z" +} diff --git a/work/artifacts/F-001/reviewer.json b/work/artifacts/F-001/reviewer.json new file mode 100644 index 0000000..208fbba --- /dev/null +++ b/work/artifacts/F-001/reviewer.json @@ -0,0 +1,32 @@ +{ + "feature_id": "F-001", + "agent": "reviewer", + "verdict": "APPROVED", + "summary": "Scaffold matches specs/F-001-scaffold DESIGN.md. Layout, composition root, boundary rules, error envelope and toolchain all verified by re-running commands against the tree. No blockers.", + "checks": { + "layout_matches_design": "PASS: src/{app,infrastructure,modules,shared} present; health exemplar exposes public API only via index.ts", + "boundary_rules": "PASS: R1 (module escape) and R2 (deep import from outside) enforced by scripts/check-module-boundaries.mjs, demonstrated by fixture tests (7 file(s) checked clean)", + "composition_root": "PASS: only src/app/build-app.ts wires modules; server.ts is a thin entrypoint", + "error_handling": "PASS: single error envelope, 404 handler, 500 message generic (no stack leak)", + "toolchain_green": "PASS: lint, lint:boundaries, typecheck, build, test all exit 0; 3 test files / 7 tests passed" + }, + "findings": [ + { + "severity": "info", + "note": "Boundary checker is regex-based over static import/export specifiers; documented limitation acceptable for TS ESM, revisit only if dynamic imports appear." + }, + { + "severity": "info", + "note": "Implementer evidence shows the checker caught a real violation during build (test escaping module) and fixed it architecturally - rule is effective, not decorative." + } + ], + "evidence": [ + "npm run lint -> exit 0 (eslint + prettier)", + "npm run lint:boundaries -> Boundary check OK: 7 file(s) checked", + "npm run typecheck -> exit 0", + "npm test -> Test Files 3 passed (3), Tests 7 passed (7)", + "files reviewed: project/src/**, project/scripts/**, project/*.json, project/eslint.config.mjs", + "specs compared: specs/F-001-scaffold/SPEC.md, DESIGN.md, TASKS.md, TESTS.md" + ], + "timestamp": "2026-08-14T19:46:00Z" +} diff --git a/work/artifacts/F-001/security.json b/work/artifacts/F-001/security.json new file mode 100644 index 0000000..ccb9f66 --- /dev/null +++ b/work/artifacts/F-001/security.json @@ -0,0 +1,27 @@ +{ + "feature_id": "F-001", + "agent": "security", + "verdict": "APPROVED", + "summary": "Security gate passed. Zero audit vulnerabilities, no hardcoded secrets, no dangerous patterns in src/, error envelope prevents stack leakage, new dependencies justified in spec/tech.md per policy.", + "checks": { + "secrets": "PASS: grep for api_key/secret/password/token patterns in project code found none", + "dependencies": "PASS: npm audit --omit=dev -> 0 vulnerabilities; npm audit (full) -> 0 vulnerabilities; dependency table with justification added to spec/tech.md", + "sast_basic": "PASS: no eval, no new Function, no child_process in src/ (execFile used only in test harness with fixed, non-user-controlled arguments)", + "input_validation": "PASS (n/a scope): no user input surfaces yet beyond GET /health; Fastify default JSON body limits apply; error handler returns generic message for 5xx", + "repo_hygiene": "PASS: project/.gitignore excludes node_modules, dist, coverage, logs" + }, + "findings": [ + { + "severity": "info", + "note": "No auth, rate limiting or CSRF surface exists yet; tracked in F-005/F-028. Nothing to mitigate in F-001." + } + ], + "evidence": [ + "npm audit --omit=dev -> found 0 vulnerabilities", + "npm audit -> found 0 vulnerabilities", + "grep secret scan over project/src, project/scripts, configs -> no hardcoded secrets", + "grep eval|new Function|child_process over project/src -> none", + "dependency justification added to spec/tech.md (fastify, typescript, vitest, eslint stack, prettier)" + ], + "timestamp": "2026-08-14T19:48:00Z" +} diff --git a/work/current.md b/work/current.md new file mode 100644 index 0000000..e065b77 --- /dev/null +++ b/work/current.md @@ -0,0 +1,14 @@ +# Sesión actual + +- Feature en curso: _ninguna_ (F-001 cerrada DONE el 2026-08-14) +- Inicio: — +- Orquestador: — + +## Plan +- Próxima feature según dependencias: F-002 (Database foundation) o F-003 (HTTP foundation) o F-004 (config/flags) — todas dependen solo de F-001. + +## Bitácora +- 2026-08-14: F-001 completada con todos los gates APPROVED y verify.sh verde. + +## Próximo paso +- intake de la siguiente feature (sugerida: F-002). diff --git a/work/history.md b/work/history.md new file mode 100644 index 0000000..8d72971 --- /dev/null +++ b/work/history.md @@ -0,0 +1,9 @@ +# Historial (append-only) + +> Añadir entradas al final. No reescribir historial previo. + +## 2026-08-14 — F-001 Scaffold modular monolith skeleton — DONE +- Gates: reviewer APPROVED, security APPROVED, qa APPROVED, verify.sh exit 0 +- Entregable: skeleton TypeScript + Fastify en project/ con boundary checker testeado; specs/F-001-scaffold completos; spec/tech.md con justificación de dependencias +- Artefactos: work/artifacts/F-001/ (architect.md, implementer.md, reviewer.json, security.json, qa.json, documenter.md, leader-close.json) +- Nota: el boundary checker detectó una violación real durante build (test escapando del módulo) y se corrigió moviendo los tests de composición a src/app diff --git a/work/runtime-status.json b/work/runtime-status.json new file mode 100644 index 0000000..b31d536 --- /dev/null +++ b/work/runtime-status.json @@ -0,0 +1,11 @@ +{ + "feature_id": null, + "stage": "idle", + "agent": "leader", + "action": "Sin ejecución activa", + "state": "waiting", + "next_agent": "leader", + "waiting_for": "Seleccionar una feature pending y actualizar este estado", + "updated_at": "2026-08-14T19:46:48Z", + "timeline": [] +}