{"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":"validation","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/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/session-adapters/canonical-session.js#L227-L263","documentation":"validateCanonicalSnapshot requires snapshot.aggregates.states to be a plain object mapping state names to integer counts. This error fires when aggregates.states is missing, null, or a non-object. The states histogram lets consumers render per-state worker counts without rescanning the workers array.","triggerScenarios":"Persisting a snapshot whose aggregates block includes workerCount but omits states; assigning states as a Map or array instead of a plain object (e.g. after JSON round-trip of a Map).","commonSituations":"Hand-built aggregates that only set workerCount; JSON.stringify of a Map producing {} silently; older snapshot versions with a different aggregates shape.","solutions":["Add aggregates.states as a plain object of state -> integer counts derived from the workers array.","Convert any Map/arr form to a plain object (Object.fromEntries) before persisting.","Rebuild the whole aggregates block from workers in one helper so states is never omitted.","Validate snapshots post-construction."],"exampleFix":"// before\naggregates.states = new Map([['idle', 2]]);\n// after\naggregates.states = Object.fromEntries(\n  Object.entries(workers.reduce((acc, w) => { acc[w.state] = (acc[w.state] || 0) + 1; return acc; }, {}))\n);","handlingStrategy":"validation","validationCode":"if (!snapshot.aggregates?.states || typeof snapshot.aggregates.states !== 'object' || Array.isArray(snapshot.aggregates.states)) throw new Error('aggregates.states must be a plain object');","typeGuard":"function hasStatesHistogram(a) { return !!a && typeof a.states === 'object' && a.states !== null && !Array.isArray(a.states); }","tryCatchPattern":"try { persistCanonicalSnapshot(snapshot); } catch (err) { if (String(err.message).includes('aggregates.states to be an object')) { console.error('Missing states histogram:', err.message); /* derive states from workers and retry */ } else { throw err; } }","preventionTips":["Use Object.fromEntries(map) when converting Map-based counts to plain objects.","Always build all three aggregates fields (workerCount, states, healths) together.","Round-trip snapshots through JSON to catch non-serializable types before persisting.","Validate snapshots in tests."],"tags":["schema-validation","session-snapshot"],"backgroundTag":"schema-validation-failed","analyzedSha":"8321021c54d670126ce3b2969d5deb880b4b0c2a","analyzedAt":"2026-09-16T10:08:13.343Z","contentChangedAt":"2026-09-16T10:08:13.343Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}