{"record":{"id":"8df4598af09389ff","repo":"ruvnet/ruflo","slug":"unknown-strategy-name-available-roster-ma","errorCode":null,"errorMessage":"unknown strategy \"${name}\". Available: ${roster.map((r) => r.name).join(', ')}","messagePattern":"unknown strategy \"(.+?)\"\\. Available: (.+?)","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugins/ruflo-arena/src/domain/strategies.ts","lineNumber":130,"sourceCode":"      antiCopy(a, 0, 'suspicious-anti-tft'),\n      alternate(a, 'alternate'),\n      random(a, 'random'),\n    ];\n  }\n  return [\n    constant(a, 0, `always-${a[0]}`),\n    constant(a, 1, `always-${a[1]}`),\n    copyOpponent(a, 0, 'copy-opponent'),\n    antiCopy(a, 0, 'anti-copy'),\n    alternate(a, 'alternate'),\n    random(a, 'random'),\n  ];\n}\n\nexport function findStrategy(game: GameSpec, name: string): Strategy {\n  const roster = classicRoster(game);\n  const s = roster.find((r) => r.name === name || r.name.startsWith(name));\n  if (!s) throw new Error(`unknown strategy \"${name}\". Available: ${roster.map((r) => r.name).join(', ')}`);\n  return s;\n}\n\n// --- Evolvable FSMs — random genomes + mutation operators (ADR-148) ------------------------\n\nexport function randomFSM(game: GameSpec, rng: () => number, nStates = 2, name = 'evolved'): FsmStrategy {\n  const a = game.actions;\n  const states = [];\n  for (let i = 0; i < nStates; i++) {\n    states.push({\n      action: choice(rng, a),\n      next: Object.fromEntries(a.map((x) => [x, randInt(rng, nStates)])),\n    });\n  }\n  return { kind: 'fsm', name, nStates, start: randInt(rng, nStates), states };\n}\n\nfunction cloneFSM(fsm: FsmStrategy): FsmStrategy {","sourceCodeStart":112,"sourceCodeEnd":148,"githubUrl":"https://github.com/ruvnet/ruflo/blob/6b01dc5a687b26b3e218f796de45ec51f8fa9e8c/plugins/ruflo-arena/src/domain/strategies.ts#L112-L148","documentation":"Thrown by findStrategy() when no entry in the classic roster matches the requested name. The roster is built per-game by classicRoster(game) (strategies.ts:103); matching is permissive — a roster entry counts as a hit if its name equals the query OR starts with the query (r.name === name || r.name.startsWith(name)). The message lists every roster name for the game so the caller can pick a valid one.","triggerScenarios":"Calling findStrategy(game, name) where name matches neither exactly nor as a prefix of any roster entry. For prisoner's-dilemma the roster is tit-for-tat, always-cooperate, always-defect, grim, pavlov, suspicious-anti-tft, alternate, random; for other games it is always-<a0>, always-<a1>, copy-opponent, anti-copy, alternate, random. Examples: findStrategy(pd, 'generous-tit-for-tat') (not in roster), findStrategy(mon, 'tit-for-tat') (PD-only strategy used against a different game).","commonSituations":"Using a strategy name documented for a different game than the one passed; expecting prefix 'tf' to match when the roster entry is 'tit-for-tat' (it will match, but 'tft' will not); passing an evolved/FSM strategy name to findStrategy (those are not in the classic roster); user-supplied strategy string from a config file or HTTP body.","solutions":["Use an exact roster name from the error's 'Available:' list, or a unique prefix of one (e.g. 'tit' resolves to 'tit-for-tat' on PD).","Confirm the roster is for the right game — classicRoster returns a different list for 'prisoners-dilemma' vs other games.","If you need a custom/evolved strategy, construct it directly (e.g. via randomFSM/mutateFSM) instead of going through findStrategy, which only resolves the classic roster.","At an API boundary, validate name against the roster via findStrategy's prefix semantics and reject unknown names with a 400."],"exampleFix":"// before\nconst s = findStrategy(game, req.body.strategy); // throws on miss\n\n// after\nimport { classicRoster, findStrategy } from './strategies';\nconst roster = classicRoster(game);\nconst matches = roster.filter(r => r.name === req.body.strategy || r.name.startsWith(req.body.strategy));\nif (matches.length === 0) {\n  return res.status(400).json({ error: `unknown strategy; available: ${roster.map(r => r.name).join(', ')}` });\n}\nconst s = findStrategy(game, req.body.strategy);","handlingStrategy":"validation","validationCode":"import { classicRoster } from './strategies';\nfunction resolveStrategy(game, name: unknown): Strategy | null {\n  if (typeof name !== 'string') return null;\n  const roster = classicRoster(game);\n  return roster.find(r => r.name === name || r.name.startsWith(name)) ?? null;\n}\nconst s = resolveStrategy(game, input);\nif (!s) return res.status(400).json({ error: 'unknown strategy' });","typeGuard":"function isKnownStrategy(game: GameSpec, name: string): boolean {\n  return classicRoster(game).some(r => r.name === name || r.name.startsWith(name));\n}","tryCatchPattern":"try { const s = findStrategy(game, name); } catch (e) { if (e instanceof Error && e.message.startsWith('unknown strategy')) return res.status(400).json({ error: e.message }); throw e; }","preventionTips":["Remember findStrategy supports prefix matches, so use a unique prefix to avoid ambiguity.","Use strategy names from the roster that matches the game you passed.","Do not route evolved/FSM strategy names through findStrategy."],"tags":["arena","strategy-lookup","validation"],"backgroundTag":null,"analyzedSha":"6b01dc5a687b26b3e218f796de45ec51f8fa9e8c","analyzedAt":"2026-08-12T13:20:50.148Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}