affaan-m/ECC · error · ReplayError

replay.bad_declaration

replay.bad_declaration

Error message

tool ${name} must declare an effect_class

What it means

createReplayer validates every entry in the tools map and throws ReplayError('replay.bad_declaration') when a tool's effect_class is missing or not one of envelope.EFFECT_CLASSES. Effect classes (SE0..SE3) drive the effect fence that decides whether a tool may run or be replayed, so an undeclared class makes the tool unusable and the whole replayer refuses to start.

Solutions

  1. Add a valid effect_class from envelope.EFFECT_CLASSES to the tool declaration, e.g. { effect_class: 'SE2', determinism: 'deterministic', impl }.
  2. Log the declared class for the offending tool (the error message names it) and compare against the allowed list from require('./envelope').EFFECT_CLASSES.
  3. Fix case/typo issues — classes are exact strings like 'SE2'; 'se2' will not pass.
  4. After a library upgrade, re-check all tool declarations against the new EFFECT_CLASSES list and update renamed classes.

Example fix

// before
const tools = {
  fetchWeather: { determinism: 'deterministic', impl: fetchWeather },
};

// after
const tools = {
  fetchWeather: { effect_class: 'SE2', determinism: 'deterministic', impl: fetchWeather },
};
Defensive patterns

Strategy: validation

Validate before calling

const { EFFECT_CLASSES } = require('./envelope');
for (const [name, tool] of Object.entries(tools)) {
  if (!EFFECT_CLASSES.includes(tool.effect_class)) {
    throw new Error(`tool ${name} has invalid effect_class: ${tool.effect_class}`);
  }
}

Type guard

function declaresEffectClass(tool, EFFECT_CLASSES) {
  return typeof tool === 'object' && tool !== null &&
    EFFECT_CLASSES.includes(tool.effect_class);
}

Try / catch

try {
  return createReplayer(tools, { mode, store });
} catch (e) {
  if (e instanceof ReplayError && e.code === 'replay.bad_declaration' && e.message.includes('effect_class')) {
    console.error('Fix tool declarations:', e.message);
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a tool object without an effect_class property, with a typo ('se2', 'SE_2'), or with a custom class name not present in envelope.EFFECT_CLASSES — e.g. `{ name: { impl(args) {...}, determinism: 'deterministic' } }`.

Common situations: Adding a new tool and forgetting the effect_class field; renaming effect classes in a library upgrade so old declarations no longer match; hand-writing tool registries instead of using a shared declaration helper; copy-pasting tool defs from an older example that used a different field name.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/cf24c370ca77f7df. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/eval-harness/replay.js:102

}

/**
 * tools: { name: { effect_class, determinism: 'deterministic'|'nondeterministic', impl(args) } }
 * options: { mode: 'record'|'replay', store: FixtureStore, maxEffectClass: 'SE2', onCall(entry) }
 */
function createReplayer(tools, options = {}) {
  const mode = options.mode || 'replay';
  const store = options.store;
  const maxRank = envelope.effectRank(options.maxEffectClass || 'SE2');
  if (!['record', 'replay'].includes(mode)) {
    throw new ReplayError('replay.bad_mode', `mode must be record or replay, got ${mode}`);
  }
  if (!store) {
    throw new ReplayError('replay.no_store', 'a FixtureStore is required');
  }
  for (const [name, tool] of Object.entries(tools)) {
    if (!envelope.EFFECT_CLASSES.includes(tool.effect_class)) {
      throw new ReplayError('replay.bad_declaration', `tool ${name} must declare an effect_class`);
    }
    if (!['deterministic', 'nondeterministic'].includes(tool.determinism)) {
      throw new ReplayError('replay.bad_declaration', `tool ${name} must declare determinism`);
    }
  }

  const calls = [];
  const emit = (entry) => {
    calls.push(entry);
    if (typeof options.onCall === 'function') {
      options.onCall(entry);
    }
  };

  return {
    mode,
    calls,
    call(name, args = {}) {

View on GitHub (pinned to 8321021c54)