{"record":{"id":"93df044b91e0c325","repo":"affaan-m/ECC","slug":"canonical-session-snapshot-requires-aggregates-sta","errorCode":null,"errorMessage":"Canonical session snapshot requires aggregates.states to be an object","messagePattern":"Canonical session snapshot requires aggregates\\.states to be an object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/session-adapters/canonical-session.js","lineNumber":245,"sourceCode":"    ensureArrayOfStrings(worker.outputs.validation, `workers[${index}].outputs.validation`);\n    ensureArrayOfStrings(worker.outputs.remainingRisks, `workers[${index}].outputs.remainingRisks`);\n\n    if (!isObject(worker.artifacts)) {\n      throw new Error(`Canonical session snapshot requires workers[${index}].artifacts to be an object`);\n    }\n  });\n\n  if (!isObject(snapshot.aggregates)) {\n    throw new Error('Canonical session snapshot requires aggregates to be an object');\n  }\n\n  ensureInteger(snapshot.aggregates.workerCount, 'aggregates.workerCount');\n  if (snapshot.aggregates.workerCount !== snapshot.workers.length) {\n    throw new Error('Canonical session snapshot requires aggregates.workerCount to match workers.length');\n  }\n\n  if (!isObject(snapshot.aggregates.states)) {\n    throw new Error('Canonical session snapshot requires aggregates.states to be an object');\n  }\n\n  if (!isObject(snapshot.aggregates.healths)) {\n    throw new Error('Canonical session snapshot requires aggregates.healths to be an object');\n  }\n\n  for (const [state, count] of Object.entries(snapshot.aggregates.states)) {\n    ensureString(state, 'aggregates.states key');\n    ensureInteger(count, `aggregates.states.${state}`);\n  }\n\n  for (const [health, count] of Object.entries(snapshot.aggregates.healths)) {\n    ensureString(health, 'aggregates.healths key');\n    ensureInteger(count, `aggregates.healths.${health}`);\n  }\n\n  return snapshot;\n}","sourceCodeStart":227,"sourceCodeEnd":263,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/session-adapters/canonical-session.js#L227-L263","documentation":"aggregates.states is missing or not a plain object. The schema requires states to be a map of worker-state string keys to non-negative integer counts (e.g. { active: 2, completed: 1 }). buildAggregates produces this from workers; the guard fires on hand-built or stale snapshots where states is null/undefined/array.","triggerScenarios":"Hand-built snapshot omits states. Recording written by an adapter version that used an array of state objects instead of a map. aggregates.states set to null because no workers were active.","commonSituations":"Custom adapter constructs aggregates manually and forgets states. Empty-workers case where the author set states:null instead of states:{}.","solutions":["Use buildAggregates(workers); it always returns states as an object (possibly empty {}).","If building by hand for zero workers, use states: {} not null.","When loading legacy recordings, coerce arrays of {state,count} into a map before validation.","Re-run validateCanonicalSnapshot after any aggregate change."],"exampleFix":"// before\naggregates: { workerCount: 0, states: null, healths: {} }\n\n// after\naggregates: buildAggregates(workers) // yields e.g. { workerCount: 0, states: {}, healths: {} }","handlingStrategy":"type-guard","validationCode":"function ensureStatesMap(snapshot) {\n  const a = snapshot.aggregates || (snapshot.aggregates = {});\n  if (a.states === null || typeof a.states !== 'object' || Array.isArray(a.states)) {\n    a.states = (snapshot.workers || []).reduce((m, w) => { const s = w.state || 'unknown'; m[s] = (m[s]||0)+1; return m; }, {});\n  }\n  return snapshot;\n}","typeGuard":"function hasValidStatesMap(s) {\n  const st = s.aggregates && s.aggregates.states;\n  return st !== null && typeof st === 'object' && !Array.isArray(st)\n    && Object.entries(st).every(([k,v]) => typeof k === 'string' && Number.isInteger(v) && v >= 0);\n}","tryCatchPattern":"try { validateCanonicalSnapshot(snapshot); }\ncatch (err) {\n  if (err.message.includes('aggregates.states to be an object')) {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n    validateCanonicalSnapshot(snapshot);\n  } else throw err;\n}","preventionTips":["Use buildAggregates so states is always a map.","For zero workers, use states:{} not null.","Migrate legacy array-based state summaries into maps before validation."],"tags":["canonical-session","schema-validation","aggregates","states"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}