{"record":{"id":"03cab321455250a4","repo":"affaan-m/ECC","slug":"canonical-session-snapshot-requires-aggregates-hea","errorCode":null,"errorMessage":"Canonical session snapshot requires aggregates.healths to be an object","messagePattern":"Canonical session snapshot requires aggregates\\.healths to be an object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/session-adapters/canonical-session.js","lineNumber":249,"sourceCode":"      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}\n\nfunction resolveRecordingDir(options = {}) {\n  if (typeof options.recordingDir === 'string' && options.recordingDir.length > 0) {\n    return path.resolve(options.recordingDir);","sourceCodeStart":231,"sourceCodeEnd":267,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/session-adapters/canonical-session.js#L231-L267","documentation":"aggregates.healths is missing or not a plain object. Symmetric to states: must be a map of health string keys (healthy/degraded/stale/unknown) to non-negative integer counts. buildAggregates derives this from worker.health on each worker.","triggerScenarios":"Hand-built snapshot omits healths. Adapter version that stored only states and skipped healths. Empty-workers case where author set healths:null.","commonSituations":"Custom adapter that does not classify worker health yet and omits the field. Migration from a schema that only tracked states.","solutions":["Use buildAggregates(workers); it always returns healths as an object (possibly empty {}).","If building by hand for zero workers, use healths: {} not null.","When adapting a source with no health signal, derive health via deriveWorkerHealth(rawWorker) before building aggregates.","Pin a unit test that asserts both states and healths are objects after buildAggregates."],"exampleFix":"// before\naggregates: { workerCount: workers.length, states: buildStates(workers), healths: null }\n\n// after\naggregates: buildAggregates(workers) // yields { workerCount, states, healths } together","handlingStrategy":"type-guard","validationCode":"function ensureHealthsMap(snapshot) {\n  const a = snapshot.aggregates || (snapshot.aggregates = {});\n  if (a.healths === null || typeof a.healths !== 'object' || Array.isArray(a.healths)) {\n    a.healths = (snapshot.workers || []).reduce((m, w) => { const h = w.health || 'unknown'; m[h] = (m[h]||0)+1; return m; }, {});\n  }\n  return snapshot;\n}","typeGuard":"function hasValidHealthsMap(s) {\n  const h = s.aggregates && s.aggregates.healths;\n  return h !== null && typeof h === 'object' && !Array.isArray(h)\n    && Object.entries(h).every(([k,v]) => typeof k === 'string' && Number.isInteger(v) && v >= 0);\n}","tryCatchPattern":"try { validateCanonicalSnapshot(snapshot); }\ncatch (err) {\n  if (err.message.includes('aggregates.healths to be an object')) {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n    validateCanonicalSnapshot(snapshot);\n  } else throw err;\n}","preventionTips":["Always derive health via deriveWorkerHealth before building aggregates.","Use buildAggregates so healths is always a map.","For zero workers, use healths:{} not null."],"tags":["canonical-session","schema-validation","aggregates","healths"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}