{"record":{"id":"bd8ca5ffacdffa14","repo":"affaan-m/ECC","slug":"canonical-session-snapshot-requires-aggregates-wor","errorCode":null,"errorMessage":"Canonical session snapshot requires aggregates.workerCount to match workers.length","messagePattern":"Canonical session snapshot requires aggregates\\.workerCount to match workers\\.length","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"scripts/lib/session-adapters/canonical-session.js","lineNumber":241,"sourceCode":"      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}`);\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}`);","sourceCodeStart":223,"sourceCodeEnd":259,"githubUrl":"https://github.com/affaan-m/ECC/blob/01e15490f04e29cfefe3896951f43db46994d8ee/scripts/lib/session-adapters/canonical-session.js#L223-L259","documentation":"aggregates.workerCount is a valid non-negative integer but does not equal snapshot.workers.length. This is a consistency check: aggregates must reflect the same worker set as workers[]. The built-in buildAggregates sets workerCount = workers.length, so a mismatch means aggregates were computed against a different (stale or hand-edited) worker list.","triggerScenarios":"Workers were added/removed after aggregates were computed (e.g. aggregates built from a cached list, then workers re-filtered). Hand-editing a recording JSON to add a worker without updating workerCount. Concurrent mutation of the workers array between buildAggregates and validateCanonicalSnapshot.","commonSituations":"Race in an adapter that builds aggregates early then filters dead workers. Copy-paste fixture where workers array was trimmed but workerCount was not. Migration script that transforms workers but copies aggregates verbatim.","solutions":["Recompute aggregates immediately before validation: snapshot.aggregates = buildAggregates(snapshot.workers).","Never mutate workers after buildAggregates; treat snapshots as immutable and rebuild aggregates on any change.","If filtering workers, rebuild aggregates from the filtered array in the same step.","Add an assertion in adapter tests: expect(snapshot.aggregates.workerCount).toBe(snapshot.workers.length)."],"exampleFix":"// before\nconst workers = rawWorkers.filter(w => w && typeof w === 'object');\nconst snapshot = { schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers, aggregates: cachedAggregates };\n\n// after: recompute after final worker list is settled\nconst workers = rawWorkers.filter(w => w && typeof w === 'object');\nconst snapshot = {\n  schemaVersion:'ecc.session.v1', adapterId:'custom', session:{...}, workers,\n  aggregates: buildAggregates(workers)\n};","handlingStrategy":"validation","validationCode":"function reconcileAggregates(snapshot) {\n  if (!snapshot.aggregates || typeof snapshot.aggregates !== 'object') {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n  } else {\n    snapshot.aggregates.workerCount = (snapshot.workers || []).length;\n  }\n  return snapshot;\n}","typeGuard":"function workerCountMatches(s) {\n  return s.aggregates && Number.isInteger(s.aggregates.workerCount)\n    && s.aggregates.workerCount === (s.workers || []).length;\n}","tryCatchPattern":"try { validateCanonicalSnapshot(snapshot); }\ncatch (err) {\n  if (err.message.includes('workerCount to match')) {\n    snapshot.aggregates = buildAggregates(snapshot.workers || []);\n    validateCanonicalSnapshot(snapshot);\n  } else throw err;\n}","preventionTips":["Treat snapshots as immutable; never mutate workers after computing aggregates.","Filter/transform workers and rebuild aggregates in the same expression.","Assert workerCount === workers.length in adapter tests."],"tags":["canonical-session","schema-validation","aggregates","consistency"],"backgroundTag":null,"analyzedSha":"01e15490f04e29cfefe3896951f43db46994d8ee","analyzedAt":"2026-08-13T00:31:08.655Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}