import dotenv from "dotenv"; import { z } from "zod"; // Loads `.env.test` under Mocha (see `.mocharc.json`'s `NODE_ENV=test`, // package.json's `test` script) instead of `.env` — same reasoning as // `apps/api/src/config/env.ts`'s identical guard: `test/*.test.ts` needs a // real (if dummy) `INTERNAL_WORKER_SECRET` to satisfy `envSchema` below // without requiring every test file to set `process.env` by hand before // importing anything that (transitively) imports this module. dotenv.config({ path: process.env.NODE_ENV === "test" ? ".env.test" : ".env" }); /** * Every environment variable this worker reads. Parsing (below) fails fast * at startup if something required is missing/invalid — same "no silent * fallback" posture as `apps/api/src/config/env.ts`, this worker's closest * analog even though it isn't part of that workspace. */ const envSchema = z.object({ /** Base URL of `apps/api` — `http://app:3000` (the `app` service's own docker-compose hostname) is the right default inside the compose network; override for local dev against a host-run API. */ API_BASE_URL: z.string().url().default("http://app:3000"), /** Must match `apps/api`'s own `INTERNAL_WORKER_SECRET` (`config/env.ts`) — required, no default, same reasoning as that variable's own doc comment. */ INTERNAL_WORKER_SECRET: z.string().min(32), /** * `hf::` URI `resolveModelFile` (node-llama-cpp) resolves * and downloads — defaults to the model this feature's plan settled on * (`qwen2.5-1.5b`, `Q4_K_M`): best empirically observed FR/EN robustness * and JSON-structuring instruction-following among the small models * `experiments/llm-tech-step-poc` benchmarked, and not slower than the * smaller alternatives there despite having more parameters. See that * PoC's `README.md` for the fuller comparison this default is based on. */ TECH_STEP_LLM_MODEL_URI: z.string().default("hf:Qwen/Qwen2.5-1.5B-Instruct-GGUF:Q4_K_M"), /** Explicit local GGUF path, bypassing the HF download above — same escape hatch `experiments/llm-tech-step-poc`'s own `LLM_TECH_STEP_MODEL_PATH` provides, useful offline or when a network download mid-deploy isn't wanted. */ TECH_STEP_LLM_MODEL_PATH: z.string().optional(), /** * Cron expression (`node-cron` syntax) the scheduler wakes up on to run * both jobs — default weekly (Sunday 03:00) is a provisional floor, not * a calibrated value: this feature's plan explicitly flags the real * cadence as needing to be set from observed recipe/correction volume * and server resources once this is actually deployed (see that plan's * "Risques" section). */ TECH_STEP_WORKER_CRON: z.string().default("0 3 * * 0"), /** Which of `TECH_STEP_TRAINING_DATA`'s locales `audit-low-confidence` samples against — see that job's own doc comment for why this can't just be discovered per-`Step` (the app has no per-recipe locale field yet). */ TECH_STEP_WORKER_LOCALE: z.string().default("fr"), /** Upper bound passed as `?limit=` to both `GET /internal/tech-steps/audit-batch` and `GET /internal/tech-steps/pending-corrections` per run — keeps one scheduled run's LLM inference cost bounded regardless of backlog size; a larger backlog just takes more scheduled runs to drain, not one slower one. */ TECH_STEP_WORKER_BATCH_LIMIT: z.coerce.number().int().positive().default(50), /** * Runs both jobs once immediately and exits, instead of starting the * cron loop — for a manual/CI-triggered run (`pnpm start`) rather than * the long-lived container process. `z.coerce.boolean()` is deliberately * *not* used here — it coerces via `Boolean(value)`, which makes the * literal string `"false"` coerce to `true` (any non-empty string does), * a real footgun for an env var — same explicit string-comparison * transform `apps/api/src/config/env.ts`'s `COOKIE_SECURE` already uses * for the identical reason. */ RUN_ONCE: z .string() .optional() .transform((value) => value === "true"), }); /** Parsed, validated environment — import this instead of reading `process.env` directly anywhere else. */ export const env = envSchema.parse(process.env);