{"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":"validation","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/8321021c54d670126ce3b2969d5deb880b4b0c2a/scripts/lib/session-adapters/canonical-session.js#L223-L259","documentation":"This consistency error fires when snapshot.aggregates.workerCount is an integer but does not equal snapshot.workers.length. The canonical schema treats aggregates.workerCount as a denormalized count of the workers array; the validator rejects any snapshot where the summary and the actual data disagree, since that signals a corrupted or partially updated snapshot.","triggerScenarios":"Calling persistCanonicalSnapshot after mutating the workers array (push/filter/splice) without recomputing aggregates.workerCount; merging snapshots by concatenating workers while copying stale aggregates.","commonSituations":"Script that adds a worker to a loaded snapshot and re-persists it without updating counts; a filter that removes dead workers but keeps the old aggregate; concurrent writers where one updated workers and another updated aggregates.","solutions":["Recompute aggregates.workerCount = snapshot.workers.length (and rebuild states/healths counts) immediately before persisting.","Never mutate a loaded snapshot's workers in place; rebuild the whole snapshot object including aggregates.","Add a helper that derives aggregates from workers so they can never drift.","Run validateCanonicalSnapshot after every snapshot mutation in tests."],"exampleFix":"// before\nsnapshot.workers.push(newWorker);\npersistCanonicalSnapshot(snapshot); // workerCount stale\n// after\nsnapshot.workers.push(newWorker);\nsnapshot = { ...snapshot, aggregates: { ...snapshot.aggregates, workerCount: snapshot.workers.length } };\npersistCanonicalSnapshot(snapshot);","handlingStrategy":"validation","validationCode":"if (snapshot.aggregates && snapshot.aggregates.workerCount !== snapshot.workers.length) throw new Error('aggregates.workerCount out of sync with workers.length');","typeGuard":"function aggregatesInSync(s) { return Number.isInteger(s.aggregates?.workerCount) && s.aggregates.workerCount === s.workers.length; }","tryCatchPattern":"try { persistCanonicalSnapshot(snapshot); } catch (err) { if (String(err.message).includes('workerCount to match workers.length')) { console.error('Aggregates stale:', err.message); snapshot = recomputeAggregates(snapshot); persistCanonicalSnapshot(snapshot); } else { throw err; } }","preventionTips":["Treat snapshots as immutable: create a new snapshot object with recomputed aggregates after any worker change.","Never push/filter workers on a loaded snapshot without rebuilding aggregates.","Wrap all mutations in a helper that recomputes workerCount, states, and healths together.","Add a round-trip test: mutate -> validate -> persist."],"tags":["schema-validation","consistency","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"}