{"record":{"id":"f6222c8e422d3b89","repo":"ruvnet/ruflo","slug":"event-must-have-a-valid-aggregateid-string","errorCode":null,"errorMessage":"Event must have a valid aggregateId string","messagePattern":"Event must have a valid aggregateId string","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/shared/src/events/rvf-event-log.ts","lineNumber":163,"sourceCode":"    this.events = [];\n    this.aggregateIndex.clear();\n    this.aggregateVersions.clear();\n    this.snapshots.clear();\n    this.initialized = false;\n\n    this.emit('shutdown');\n  }\n\n  // ===========================================================================\n  // Write Operations\n  // ===========================================================================\n\n  /** Append a domain event to the log. */\n  async append(event: DomainEvent): Promise<void> {\n    this.ensureInitialized();\n\n    if (!event.aggregateId || typeof event.aggregateId !== 'string') {\n      throw new Error('Event must have a valid aggregateId string');\n    }\n    if (!event.type || typeof event.type !== 'string') {\n      throw new Error('Event must have a valid type string');\n    }\n\n    // Assign next version for aggregate\n    const currentVersion = this.aggregateVersions.get(event.aggregateId) ?? 0;\n    const nextVersion = currentVersion + 1;\n    event.version = nextVersion;\n\n    // Persist to disk first (crash-safe ordering)\n    this.appendRecord(this.config.logPath, event);\n\n    // Update in-memory state\n    this.indexEvent(event);\n\n    this.emit('event:appended', event);\n","sourceCodeStart":145,"sourceCodeEnd":181,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/shared/src/events/rvf-event-log.ts#L145-L181","documentation":"RvfEventLog.append() validates the incoming DomainEvent before persisting and requires aggregateId to be a non-empty string, because it keys per-aggregate versioning (the aggregateVersions map) and the on-disk index on that field. Events missing, blanking, or carrying a non-string aggregateId are rejected before any write, keeping the log consistent. The usual culprit is an event assembled from spreads or deserialized JSON where the field was dropped or renamed.","triggerScenarios":"append({ type: 'task.created', payload }) with no aggregateId; aggregateId: '' or undefined; aggregateId supplied as a number (e.g. 42) or any non-string value.","commonSituations":"Events built via { ...base, type } that accidentally drop the id; JSON deserialized with a field-name mismatch (aggregate_id vs aggregateId); test fixtures missing the field; ORM rows used directly as events.","solutions":["Set aggregateId explicitly at every append site — String(aggregate.id) if the id is numeric","Fix serialization so the field arrives named exactly aggregateId","Centralize event construction in a helper that validates the DomainEvent shape once"],"exampleFix":"// before\nawait log.append({ type: 'order.updated', payload } as DomainEvent); // throws\n\n// after\nawait log.append({ aggregateId: String(order.id), type: 'order.updated', payload } as DomainEvent);","handlingStrategy":"type-guard","validationCode":"if (typeof event.aggregateId !== 'string' || event.aggregateId.length === 0) {\n  throw new TypeError('DomainEvent.aggregateId must be a non-empty string');\n}\nawait log.append(event);","typeGuard":"function isDomainEvent(e: unknown): e is DomainEvent {\n  return !!e && typeof e === 'object'\n    && typeof (e as any).aggregateId === 'string' && (e as any).aggregateId.length > 0\n    && typeof (e as any).type === 'string' && (e as any).type.length > 0;\n}","tryCatchPattern":"try {\n  await log.append(event);\n} catch (e) {\n  if (e instanceof Error && e.message.includes('aggregateId')) {\n    // fix the producer: the event was missing/blank/non-string aggregateId\n  } else throw e;\n}","preventionTips":["Construct events through one validated factory instead of ad-hoc object literals","Run the isDomainEvent guard before append() at integration boundaries","String() numeric ids at the call site: String(order.id)"],"tags":["event-sourcing","validation","aggregate","event-log"],"backgroundTag":"schema-validation-failed","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-08-21T13:17:26.733Z"}