{"record":{"id":"b647b3e2c2241a1f","repo":"ruvnet/ruflo","slug":"routes-config-must-be-a-flat-array-of-routes","errorCode":null,"errorMessage":"Routes config must be a flat array of routes","messagePattern":"Routes config must be a flat array of routes","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"ruflo/src/ruvocal/src/lib/server/router/policy.ts","lineNumber":13,"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();","sourceCodeStart":1,"sourceCodeEnd":31,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/ruflo/src/ruvocal/src/lib/server/router/policy.ts#L1-L31","documentation":"Thrown by loadPolicy in router/policy.ts when the JSON file at LLM_ROUTER_ROUTES_PATH parses successfully but the top-level value is not an Array. The router routes file must be a flat JSON array of Route objects (each with name, description, primary_model); an object, string, number, or null at the top level is rejected before any per-entry validation runs.","triggerScenarios":"LLM_ROUTER_ROUTES_PATH points at a JSON file whose contents are an object (e.g. {\"routes\": [...]} or {\"default\": {...}}) instead of a bare [...] array; or the file contains a single route object {\"name\": ...} rather than a one-element array; or the file was overwritten with non-JSON-yaml/map structure.","commonSituations":"Hand-editing the routes file and wrapping it in an envelope key; converting from YAML/TOML and keeping the top-level mapping; a deployment tool templating the file with an object schema; copy-pasting a single route object instead of an array; merging config from a CMS that returns objects keyed by route name.","solutions":["Rewrite the routes file as a flat JSON array of route objects: [{ \"name\": ..., \"description\": ..., \"primary_model\": ... }, ...].","If your source of truth is keyed by name, flatten it at config-load time (Object.values(obj)) before writing the file.","Validate the file shape in CI with a JSON schema or a quick node -e check that JSON.parse yields Array.isArray.","Confirm LLM_ROUTER_ROUTES_PATH points at the file you actually edited."],"exampleFix":"// before (routes.json)\n{\n  \"routes\": [\n    { \"name\": \"code\", \"description\": \"coding\", \"primary_model\": \"Qwen2.5-Coder-32B\" }\n  ]\n}\n\n// after\n[\n  { \"name\": \"code\", \"description\": \"coding\", \"primary_model\": \"Qwen2.5-Coder-32B\" }\n]","handlingStrategy":"validation","validationCode":"import { readFileSync } from \"node:fs\";\nfunction routesFileIsValid(path: string): boolean {\n  try {\n    const arr = JSON.parse(readFileSync(path, \"utf8\"));\n    if (!Array.isArray(arr)) return false;\n    return arr.every((r) => r?.name && r?.description && r?.primary_model);\n  } catch {\n    return false;\n  }\n}","typeGuard":"function isRouteArray(value: unknown): value is { name: string; description: string; primary_model: string }[] {\n  return Array.isArray(value) && value.every((r) => r && typeof r === \"object\"\n    && typeof r.name === \"string\" && typeof r.description === \"string\" && typeof r.primary_model === \"string\");\n}","tryCatchPattern":"try { await loadPolicy(); }\ncatch (e) {\n  if (e instanceof Error && /Routes config must be a flat array/.test(e.message)) {\n    // stop the router from loading; fall back to no routes or default route\n    loaded = true; // prevent reload loop\n    ROUTES = [];\n  } else throw e;\n}","preventionTips":["Author the routes file as a bare JSON array of route objects, no envelope key.","Add a CI lint that JSON.parses the file and asserts Array.isArray.","If templating from a keyed source, flatten with Object.values before writing the file.","Confirm LLM_ROUTER_ROUTES_PATH points at the file you actually edited."],"tags":["router","config","json","routes","startup"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}