{"record":{"id":"1b4bfb1928b36ff0","repo":"affaan-m/ECC","slug":"canonical-session-snapshot-requires-aggregates-to","errorCode":null,"errorMessage":"Canonical session snapshot requires aggregates to be an object","messagePattern":"Canonical session snapshot requires aggregates to be an object","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/session-adapters/canonical-session.js","lineNumber":236,"sourceCode":"\n    ensureStringAllowEmpty(worker.intent.objective, `workers[${index}].intent.objective`);\n    ensureArrayOfStrings(worker.intent.seedPaths, `workers[${index}].intent.seedPaths`);\n\n    if (!isObject(worker.outputs)) {\n      throw new Error(`Canonical session snapshot requires workers[${index}].outputs to be an object`);\n    }\n\n    ensureArrayOfStrings(worker.outputs.summary, `workers[${index}].outputs.summary`);\n    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}`);","sourceCodeStart":218,"sourceCodeEnd":254,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/session-adapters/canonical-session.js#L218-L254","documentation":"snapshot.aggregates is missing or not a plain object. Aggregates must hold workerCount (non-negative integer), states (object of state->count), and healths (object of health->count). The built-in normalizers always build aggregates via buildAggregates(workers); this fires mainly when a snapshot is hand-built or loaded from a tampered/stale recording without aggregates.","triggerScenarios":"Calling validateCanonicalSnapshot with a snapshot assembled manually that omits aggregates. Loading a recording written by an older schema that did not persist aggregates. Setting aggregates:null because workerCount is unknown.","commonSituations":"User constructs a snapshot from raw session data and forgets to compute aggregates. Test fixture that builds only workers. Adapter migration that drops aggregates.","solutions":["Use buildAggregates(workers) exported from canonical-session to compute the object in one call: aggregates: buildAggregates(workers).","If building by hand, set { workerCount: workers.length, states: {...}, healths: {...} }.","When loading a recording missing aggregates, recompute them from workers before re-validating.","Always pass the full snapshot through one of the normalize*Session builders; they call buildAggregates for you."],"exampleFix":"// before\nconst { buildAggregates } = require('./canonical-session');\nconst snapshot = { schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers }; // no aggregates\n\n// after\nconst snapshot = {\n  schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers,\n  aggregates: buildAggregates(workers)\n};","handlingStrategy":"validation","validationCode":"function ensureAggregates(snapshot) {\n  if (!snapshot.aggregates || typeof snapshot.aggregates !== 'object') {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n  }\n  return snapshot;\n}","typeGuard":"function hasValidAggregates(s) {\n  const a = s.aggregates;\n  return a !== null && typeof a === 'object' && !Array.isArray(a)\n    && Number.isInteger(a.workerCount) && a.workerCount === (s.workers || []).length\n    && a.states !== null && typeof a.states === 'object'\n    && a.healths !== null && typeof a.healths === 'object';\n}","tryCatchPattern":"try { validateCanonicalSnapshot(snapshot); }\ncatch (err) {\n  if (err.message.includes('aggregates to be an object')) {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n    validateCanonicalSnapshot(snapshot);\n  } else throw err;\n}","preventionTips":["Always use buildAggregates(workers) to produce aggregates.","Never omit aggregates when hand-building snapshots.","Recompute aggregates whenever workers change."],"tags":["canonical-session","schema-validation","aggregates"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}