{"record":{"id":"5840e477aceb2df2","repo":"ruvnet/ruflo","slug":"invalid-route-entry-json-stringify-r","errorCode":null,"errorMessage":"Invalid route entry: ${JSON.stringify(r)}","messagePattern":"Invalid route entry: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/server/router/policy.ts","lineNumber":18,"sourceCode":"import { readFile } from \"node:fs/promises\";\nimport { config } from \"$lib/server/config\";\nimport type { Route } from \"./types\";\n\nlet ROUTES: Route[] = [];\nlet loaded = false;\n\nexport async function loadPolicy(): Promise<Route[]> {\n\tconst path = config.LLM_ROUTER_ROUTES_PATH;\n\tconst text = await readFile(path, \"utf8\");\n\tconst arr = JSON.parse(text) as Route[];\n\tif (!Array.isArray(arr)) {\n\t\tthrow new Error(\"Routes config must be a flat array of routes\");\n\t}\n\tconst seen = new Set<string>();\n\tfor (const r of arr) {\n\t\tif (!r?.name || !r?.description || !r?.primary_model) {\n\t\t\tthrow new Error(`Invalid route entry: ${JSON.stringify(r)}`);\n\t\t}\n\t\tif (seen.has(r.name)) {\n\t\t\tthrow new Error(`Duplicate route name: ${r.name}`);\n\t\t}\n\t\tseen.add(r.name);\n\t}\n\tROUTES = arr;\n\tloaded = true;\n\treturn ROUTES;\n}\n\nexport async function getRoutes(): Promise<Route[]> {\n\tif (!loaded) await loadPolicy();\n\treturn ROUTES;\n}\n\nexport function resolveRouteModels(\n\trouteName: string,","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/router/policy.ts#L1-L36","documentation":"Thrown by loadPolicy() while validating each entry of the LLM router routes JSON (config.LLM_ROUTER_ROUTES_PATH). The Route interface requires non-empty name, description, and primary_model fields; the guard uses truthiness (!r?.name || !r?.description || !r?.primary_model), so a missing key, null, or an empty string all trip it. The thrown message embeds JSON.stringify(r) so the offending object is visible in the error text.","triggerScenarios":"Server startup or the first getRoutes() call (lazy load) parses a routes file where at least one object omits primary_model, or uses the wrong key name (e.g. \"model\" instead of \"primary_model\"), or has name/description set to \"\".","commonSituations":"Editing the routes JSON by hand and misspelling a field; migrating from an older schema that used \"model\"; copy-pasting a route and deleting a line; setting LLM_ROUTER_ROUTES_PATH to a stale or template file that was never filled in.","solutions":["Open the file at config.LLM_ROUTER_ROUTES_PATH and inspect the exact object printed in the error message.","Ensure every entry has non-empty string values for name, description, and primary_model (the three required Route fields).","If you renamed primary_model to model for brevity, change it back; fallback_models is optional but primary_model is not.","Re-run loadPolicy()/restart the server; the loader caches ROUTES only after full validation passes."],"exampleFix":"// before (routes.json)\n[{ \"name\": \"chat\", \"description\": \"casual chat\", \"model\": \"gpt-4o\" }]\n// after\n[{ \"name\": \"chat\", \"description\": \"casual chat\", \"primary_model\": \"gpt-4o\", \"fallback_models\": [\"gpt-4o-mini\"] }]","handlingStrategy":"validation","validationCode":"import Ajv from \"ajv\";\nconst routeSchema = {\n  type: \"array\",\n  items: {\n    type: \"object\",\n    required: [\"name\", \"description\", \"primary_model\"],\n    properties: {\n      name: { type: \"string\", minLength: 1 },\n      description: { type: \"string\", minLength: 1 },\n      primary_model: { type: \"string\", minLength: 1 },\n      fallback_models: { type: \"array\", items: { type: \"string\" } },\n    },\n    additionalProperties: false,\n  },\n};\nconst validate = new Ajv({ allErrors: true }).compile(routeSchema);\nconst arr = JSON.parse(await readFile(path, \"utf8\"));\nif (!validate(arr)) {\n  throw new Error(\"Invalid routes config: \" + JSON.stringify(validate.errors));\n}","typeGuard":"import type { Route } from \"./types\";\nfunction isRoute(x: unknown): x is Route {\n  return (\n    typeof x === \"object\" && x !== null &&\n    typeof (x as Route).name === \"string\" && (x as Route).name !== \"\" &&\n    typeof (x as Route).description === \"string\" && (x as Route).description !== \"\" &&\n    typeof (x as Route).primary_model === \"string\" && (x as Route).primary_model !== \"\"\n  );\n}","tryCatchPattern":"try {\n  await loadPolicy();\n} catch (e) {\n  console.error(\"LLM router routes failed to load from\", config.LLM_ROUTER_ROUTES_PATH, String(e?.message ?? e));\n  process.exit(1); // fatal at startup; do not serve with an empty/invalid policy\n}","preventionTips":["Treat the routes file as typed config: validate it with a JSON schema in CI before deploy.","Write a unit test that loadPolicy() succeeds against the committed sample routes file.","Use additionalProperties:false in the schema so a typo like \"model\" is caught early."],"tags":["config","validation","router","startup"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}