# Story 2.4: Entity-Relationship Diagram Type Renderer

Status: done

<!-- Note: Validation is optional. Run validate-create-story for quality check before dev-story. -->

## Story

As a user,
I want to create and view E-R diagrams with entities, attributes, and relationships,
so that I can model data structures with proper cardinality notation.

## Acceptance Criteria

1. **Given** I open or create an E-R diagram, **When** the canvas renders, **Then** entities display as rectangles with a header (entity name) and rows for each attribute (name, type, constraints), **And** primary key attributes are marked with a key icon, **And** foreign key attributes show a reference indicator.

2. **Given** entities have relationships, **When** rendered, **Then** relationship edges show cardinality notation at each end (1, N, M, 0..1, 0..N, 1..N), **And** relationship labels appear at edge midpoints with a background pill.

3. **Given** an E-R diagram has a many-to-many relationship, **When** auto-layout runs, **Then** junction tables (if present) are positioned between the related entities, **And** the layout clearly shows the M:N relationship structure.

## Tasks / Subtasks

- [x] Task 1: Create E-R constants and type registry (AC: #1)
  - [x] 1.1: Create `apps/web/src/modules/diagram/types/er/constants.ts` with `ER_SIZES`, `resolveErNodeType()`, `resolveErEdgeType()`, `getErEntityHeight()`
  - [x] 1.2: Create `apps/web/src/modules/diagram/types/er/index.ts` barrel export

- [x] Task 2: Create ErEntityNode custom @xyflow/react component (AC: #1)
  - [x] 2.1: Create `apps/web/src/modules/diagram/types/er/ErEntityNode.tsx` — table-like card with entity name header, attribute rows showing (name, type, PK icon, FK indicator, nullable/unique badges)

- [x] Task 3: Create ErRelationshipEdge custom @xyflow/react component (AC: #2)
  - [x] 3.1: Create `apps/web/src/modules/diagram/types/er/ErRelationshipEdge.tsx` — edge with cardinality notation at source/target endpoints, midpoint label with background pill

- [x] Task 4: Update graph converter for E-R type resolution (AC: #1, #2)
  - [x] 4.1: Update `resolveFlowNodeType()` in `graph-converter.ts` to handle `er` diagramType and `er:` prefixed node types
  - [x] 4.2: Update `resolveFlowEdgeType()` in `graph-converter.ts` to handle `er` diagramType
  - [x] 4.3: Update `graphNodeToFlowNode()` to pass `columns` data through for E-R entity nodes (already handled by spread)
  - [x] 4.4: Update `graphEdgeToFlowEdge()` to pass `cardinality` data through for E-R edges (already handled by spread)

- [x] Task 5: Register E-R types in DiagramCanvas (AC: #1, #2, #3)
  - [x] 5.1: Import E-R node/edge components in `DiagramCanvas.tsx`
  - [x] 5.2: Add E-R entries to `nodeTypes` and `edgeTypes` objects (OUTSIDE component)
  - [x] 5.3: Add E-R SVG marker defs for relationship arrows

- [x] Task 6: E-R layout configuration (AC: #3)
  - [x] 6.1: Ensure `computeLayout` uses flat `buildElkGraph` for E-R diagrams (no compound layout needed)
  - [x] 6.2: Verify entity node widths are passed to ELK via `data.w` or `measured.width` for proper spacing

- [x] Task 7: Add E-R CSS styles (AC: #1, #2)
  - [x] 7.1: Add `.er-entity`, `.er-entity-header`, `.er-entity-row`, `.er-entity-indicator`, `.er-entity-col-name`, `.er-entity-col-type`, `.er-entity-constraint`, `.er-entity-empty`, `.er-cardinality` styles to `globals.css`
  - [x] 7.2: Use `--diagram-er` (violet) accent color for E-R elements

- [x] Task 8: Tests (AC: all)
  - [x] 8.1: Unit tests for E-R constants — `resolveErNodeType`, `resolveErEdgeType`, `getErEntityHeight` (9 tests)
  - [x] 8.2: Unit tests for graph converter E-R type mapping — node and edge types correctly resolved for `diagramType === "er"` (3 tests)
  - [x] 8.3: All 93 tests pass (81 existing + 12 new) — no regressions

## Dev Notes

### Overview — What This Story Builds

This story adds the E-R diagram type renderer: Entity-Relationship diagrams with table-like entity nodes showing attributes (columns) and relationship edges with cardinality notation. Unlike BPMN (Story 2.3), E-R diagrams use **flat ELK layout** (no compound pool/lane hierarchy).

**This story builds:**
- 1 custom E-R node component (ErEntityNode — table-like card with attribute rows)
- 1 custom E-R edge component (ErRelationshipEdge — with cardinality labels at endpoints)
- E-R constants and type registry
- Graph converter E-R type resolution
- E-R CSS styles using `--diagram-er` violet accent

**This story does NOT implement:**
- Other diagram types (Stories 2.5-2.8)
- Smart Inspector for E-R field editing (future epic)
- SQL DDL export from E-R (Epic 6, Story 6.2)
- Manual node repositioning (Story 2.9)
- Liveblocks/CRDT integration (Epic 4)
- AI-triggered mutations (Epic 3)

### Architecture Compliance

**MANDATORY patterns from Architecture Decision Document:**

1. **Unified Graph Data Model (Decision 1):** E-R nodes use type-prefixed `type` field (`er:entity`). The `columns` field on DiagramNode stores entity attributes. The `cardinality` field on DiagramEdge stores relationship cardinality.

2. **Component Structure:** Feature code in `~/modules/diagram/types/er/` — E-R-specific node components and constants. NOT co-located in route directories.

3. **@xyflow/react Custom Nodes:** All custom node components use `NodeProps` typing. The `nodeTypes` and `edgeTypes` objects MUST be defined OUTSIDE the component (performance critical — Story 2.1/2.3 pattern).

4. **ELK.js Flat Layout:** E-R uses standard flat `buildElkGraph()` — no compound hierarchy needed. The ELK layered algorithm naturally positions junction tables between related entities in an M:N structure.

5. **Lean JSON Data Model:** No x/y positions stored for E-R nodes. All positioning computed by ELK at render time. Only entity `columns` data is stored.

6. **Type Prefixing Convention:** `er:entity` prefix on DiagramNode.type. Edge type defaults to `"relationship"` for E-R.

### E-R Node Types — Size Constants

E-R entities are dynamic-height: the height depends on the number of columns.

```typescript
export const ER_SIZES = {
  entity: {
    w: 260,           // Fixed width for all entities
    headerH: 36,      // Entity name header
    rowH: 24,          // Per-attribute row
    paddingY: 8,       // Top/bottom padding
    minRows: 1,        // Minimum 1 row even if no columns
  },
} as const;

/** Compute entity height based on column count */
export function getErEntityHeight(columns?: Column[]): number {
  const rows = Math.max(ER_SIZES.entity.minRows, columns?.length ?? 0);
  return ER_SIZES.entity.headerH + rows * ER_SIZES.entity.rowH + ER_SIZES.entity.paddingY * 2;
}
```

### E-R Node Type → @xyflow/react Type Mapping

| DiagramNode.type | @xyflow Node type | Component |
|---|---|---|
| `er:entity` | `erEntity` | `ErEntityNode` |

Only one node type needed. The entity node handles all E-R data, including attribute display with PK/FK indicators.

**Type resolution:**
```typescript
export function resolveErNodeType(type: string): string {
  const bare = type.startsWith("er:") ? type.slice(3) : type;
  switch (bare) {
    case "entity":
    default:
      return "erEntity";
  }
}

export function resolveErEdgeType(type: string | undefined): string {
  // E-R only has one edge type: relationship
  return "erRelationship";
}
```

### E-R Edge Type — Cardinality Notation

| DiagramEdge.type | @xyflow Edge type | Visual |
|---|---|---|
| `relationship` (or default) | `erRelationship` | Solid line with cardinality labels at endpoints, midpoint label |

**Cardinality format:** The `DiagramEdge.cardinality` field stores notation like `"1:N"`, `"N:M"`, `"0..1:1"`, `"1..N:0..N"`. The format is `sourceCardinality:targetCardinality`, split on `:`.

**Cardinality rendering approach:**

Use @xyflow/react's `EdgeLabelRenderer` to position cardinality labels at the source and target ends. The edge itself uses `BaseEdge` + `getSmoothStepPath` for orthogonal routing. The midpoint label (relationship name) uses `BaseEdge`'s built-in `label` prop with `labelShowBg`.

```typescript
import { BaseEdge, EdgeLabelRenderer, getSmoothStepPath } from "@xyflow/react";
import type { EdgeProps } from "@xyflow/react";

export function ErRelationshipEdge(props: EdgeProps) {
  const [edgePath, labelX, labelY] = getSmoothStepPath({ ... });
  const cardinality = (props.data as { cardinality?: string })?.cardinality;
  const [srcCard, tgtCard] = (cardinality ?? "").split(":");

  return (
    <>
      <BaseEdge id={props.id} path={edgePath} style={{ stroke: "var(--diagram-er)" }} ... />
      <EdgeLabelRenderer>
        {/* Source cardinality positioned near source */}
        {srcCard && <div style={{ position: "absolute", transform: `translate(...)` }} className="er-cardinality">{srcCard}</div>}
        {/* Target cardinality positioned near target */}
        {tgtCard && <div style={{ position: "absolute", transform: `translate(...)` }} className="er-cardinality">{tgtCard}</div>}
      </EdgeLabelRenderer>
    </>
  );
}
```

**Positioning cardinality labels:** Calculate positions offset from source/target coordinates along the edge direction. Source label: ~20px from source endpoint. Target label: ~20px from target endpoint. Use the edge direction vector to compute offset positions.

### ErEntityNode — Implementation Pattern

The entity node renders as a table-like card:

```
┌──────────────────────────┐
│  Entity Name             │  ← Header (bold, violet accent border-top)
├──────────────────────────┤
│ 🔑 id         INTEGER    │  ← PK row (key icon)
│ → user_id     TEXT       │  ← FK row (arrow indicator)
│   name        VARCHAR    │  ← Normal row
│   email       VARCHAR  ? │  ← Nullable (? indicator)
│   code        TEXT     U │  ← Unique (U indicator)
└──────────────────────────┘
```

**Key implementation details:**

```typescript
import { Handle, Position } from "@xyflow/react";
import type { NodeProps } from "@xyflow/react";
import type { DiagramNode, Column } from "../graph";

export function ErEntityNode({ data }: NodeProps) {
  const d = data as unknown as DiagramNode & { label: string };
  const columns = d.columns ?? [];

  return (
    <div className="er-entity">
      <div className="er-entity-header">{d.label}</div>
      <div className="er-entity-body">
        {columns.length === 0 && (
          <div className="er-entity-row er-entity-empty">No attributes</div>
        )}
        {columns.map((col, i) => (
          <div key={col.name ?? i} className="er-entity-row">
            <span className="er-entity-indicator">
              {col.isPrimaryKey ? "🔑" : col.isForeignKey ? "→" : " "}
            </span>
            <span className="er-entity-col-name">{col.name}</span>
            <span className="er-entity-col-type">{col.type}</span>
            <span className="er-entity-constraint">
              {col.isNullable ? "?" : ""}{col.isUnique ? "U" : ""}
            </span>
          </div>
        ))}
      </div>
      <Handle type="target" position={Position.Top} style={{ opacity: 0 }} />
      <Handle type="target" position={Position.Left} id="left" style={{ opacity: 0 }} />
      <Handle type="source" position={Position.Bottom} style={{ opacity: 0 }} />
      <Handle type="source" position={Position.Right} id="right" style={{ opacity: 0 }} />
    </div>
  );
}
```

**All 4 handles** (top/right/bottom/left) for edge connections in any layout direction — same pattern as BPMN nodes.

### Graph Converter Updates

Add E-R type resolution alongside the existing BPMN resolution:

```typescript
// In resolveFlowNodeType():
if (diagramType === "er" || nodeType.startsWith("er:")) {
  return resolveErNodeType(nodeType);
}

// In resolveFlowEdgeType():
if (diagramType === "er") {
  return resolveErEdgeType(edgeType);
}
```

**Import from er/constants.ts** — same pattern as BPMN imports.

**No container nodes needed** — E-R has no pools/lanes/groups. The `graphToFlow()` function's existing default path (flat node mapping) handles E-R correctly. No need to add an `if (diagramType === "er")` block for container handling.

**Columns data already preserved:** `graphNodeToFlowNode` spreads all node data into `data: { ...node }`, which includes `columns`. And `flowNodeToGraphNode` already preserves `columns` via the conditional spread. No changes needed.

**Cardinality data already preserved:** `graphEdgeToFlowEdge` spreads all edge data into `data: { ...edge }`, which includes `cardinality`. And `flowEdgeToGraphEdge` already preserves `cardinality`. No changes needed.

### DiagramCanvas Updates

Add E-R node/edge types to the `nodeTypes` and `edgeTypes` objects:

```typescript
import { ErEntityNode } from "../../types/er";
import { ErRelationshipEdge } from "../../types/er";

const nodeTypes = {
  // Existing BPMN types...
  erEntity: ErEntityNode,
};

const edgeTypes = {
  // Existing BPMN types...
  erRelationship: ErRelationshipEdge,
};
```

**E-R marker defs:** Add an SVG marker for E-R relationship arrows. A simple solid arrowhead marker, similar to BPMN but using `--diagram-er` color:

```typescript
function ErMarkerDefs() {
  return (
    <svg style={{ position: "absolute", width: 0, height: 0 }}>
      <defs>
        <marker id="er-arrow" viewBox="0 0 10 10" refX={10} refY={5}
          markerWidth={8} markerHeight={8} orient="auto-start-reverse">
          <path d="M 0 0 L 10 5 L 0 10 Z" fill="var(--diagram-er)" />
        </marker>
      </defs>
    </svg>
  );
}
```

**BFS Path Highlighting:** The existing `handleNodeClick` uses `CONTAINER_TYPES` to skip container nodes. E-R has no container nodes, so no changes needed. BFS highlighting works on E-R entity nodes out of the box (the `bfsPath` utility is diagram-type-agnostic).

### Layout — ELK Flat Layout for E-R

E-R uses the existing flat `buildElkGraph()` — no compound layout detection needed.

**Key:** Entity node height must be dynamic based on column count. The `buildElkGraph()` function reads `data.w` or `measured.width` for width, and `measured.height` for height. For ELK layout to work correctly:
- Set `data.w` to `ER_SIZES.entity.w` (260) on entity nodes via the graph converter
- The height comes from `measured.height` after DOM render, which will be correct since the ErEntityNode renders all rows

**Alternatively**, compute the height upfront via `getErEntityHeight(columns)` and pass it to ELK. This ensures correct spacing on first layout before DOM measurement. In `buildElkGraph`, when the node has E-R data with columns, use the computed height:

```typescript
children: nodes.map((node) => {
  const data = node.data as unknown as DiagramNode;
  const h = data.columns
    ? getErEntityHeight(data.columns)
    : (node.measured?.height ?? DEFAULT_NODE_HEIGHT);
  return {
    id: node.id,
    width: data.w ?? node.measured?.width ?? DEFAULT_NODE_WIDTH,
    height: h,
  };
}),
```

This small change to `buildElkGraph` ensures E-R entities get correct ELK spacing on initial layout.

### CSS Styles for E-R Nodes

Add to `globals.css`:

```css
/* E-R Entity Styles */
.er-entity {
  background: var(--node-bg);
  border: 1.5px solid var(--diagram-er);
  border-radius: 6px;
  min-width: 220px;
  font-size: 12px;
  overflow: hidden;
}

.er-entity-header {
  background: var(--diagram-er);
  color: white;
  font-weight: 600;
  font-size: 13px;
  padding: 6px 12px;
}

.er-entity-body {
  padding: 4px 0;
}

.er-entity-row {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 2px 10px;
  font-family: var(--font-mono, ui-monospace, monospace);
  font-size: 11px;
  line-height: 20px;
}

.er-entity-row:hover {
  background: var(--node-hover, oklch(0.623 0.214 260 / 5%));
}

.er-entity-indicator {
  width: 18px;
  text-align: center;
  flex-shrink: 0;
  font-size: 12px;
}

.er-entity-col-name {
  font-weight: 500;
  color: var(--foreground);
  flex: 1;
  min-width: 0;
}

.er-entity-col-type {
  color: var(--muted-foreground);
  font-size: 10px;
  flex-shrink: 0;
}

.er-entity-constraint {
  color: var(--diagram-er);
  font-size: 10px;
  font-weight: 600;
  width: 16px;
  text-align: center;
  flex-shrink: 0;
}

.er-entity-empty {
  color: var(--muted-foreground);
  font-style: italic;
  justify-content: center;
  padding: 8px 10px;
}

/* E-R Cardinality labels on edges */
.er-cardinality {
  background: var(--edge-label-bg, oklch(1 0 0 / 90%));
  border: 1px solid var(--diagram-er);
  border-radius: 4px;
  padding: 1px 5px;
  font-size: 10px;
  font-weight: 600;
  color: var(--diagram-er);
  pointer-events: none;
}
```

**Color note:** The entity header uses `--diagram-er` (violet) as background with white text. This works in both light and dark modes because the violet oklch value provides sufficient contrast against white text.

### Existing Code to Reuse / Modify

| File | Action | What |
|------|--------|------|
| `apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx` | **MODIFY** | Register E-R node types and edge types, add ErMarkerDefs |
| `apps/web/src/modules/diagram/lib/graph-converter.ts` | **MODIFY** | Add E-R type resolution for nodes and edges (import from er/constants) |
| `apps/web/src/modules/diagram/lib/elk-layout.ts` | **MODIFY** | Update `buildElkGraph` to compute E-R entity height from `columns` data |
| `apps/web/src/assets/styles/globals.css` | **MODIFY** | Add E-R entity and cardinality CSS styles |
| `apps/web/src/modules/diagram/types/graph.ts` | **READ** | DiagramNode (columns), DiagramEdge (cardinality) — already defined, no changes |
| `apps/web/src/modules/diagram/lib/bfs-path.ts` | **REUSE** | Path highlighting works for E-R — diagram-type-agnostic |
| `apps/web/src/modules/diagram/stores/useGraphStore.ts` | **REUSE** | highlightedNodeId/setHighlightedNodeId — no changes needed |
| `apps/web/src/modules/diagram/hooks/useAutoLayout.ts` | **REUSE** | Auto-layout works with flat ELK — no changes needed |

### Library & Framework Requirements

**No new packages required.** Everything built with existing dependencies:
- `@xyflow/react` 12.10.1 — custom nodes, edges, handles, EdgeLabelRenderer, BaseEdge
- `elkjs` 0.11.0 — flat layered layout
- `zustand` 5.0.8 — highlight state (reuse existing)

### File Structure for This Story

New files:
```
apps/web/src/modules/diagram/
├── types/er/
│   ├── index.ts                        # Exports all E-R components + constants
│   ├── constants.ts                    # ER_SIZES, type mappings, getErEntityHeight
│   ├── constants.test.ts              # Tests for E-R constants
│   ├── ErEntityNode.tsx               # Entity custom node (table-like card)
│   └── ErRelationshipEdge.tsx         # Relationship edge with cardinality
```

Modified files:
```
apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx  # Register E-R node/edge types
apps/web/src/modules/diagram/lib/graph-converter.ts               # E-R type mapping
apps/web/src/modules/diagram/lib/graph-converter.test.ts          # Add E-R converter tests
apps/web/src/modules/diagram/lib/elk-layout.ts                    # E-R entity height in buildElkGraph
apps/web/src/assets/styles/globals.css                             # E-R CSS styles
```

### Project Structure Notes

- E-R node components go in `~/modules/diagram/types/er/` — follows the BPMN pattern (`~/modules/diagram/types/bpmn/`)
- Layout utilities in `~/modules/diagram/lib/` — E-R uses the existing flat `buildElkGraph`, only a small height computation change
- `bfs-path.ts` reused as-is — diagram-type-agnostic
- Tests co-located next to source files

### Anti-Patterns to Avoid

- **NEVER put `nodeTypes` or `edgeTypes` inside the component** — causes re-renders on every state change (Story 2.1/2.3 pattern)
- **NEVER hardcode positions for E-R nodes** — all positioning from ELK layout
- **NEVER import from `reactflow`** — use `@xyflow/react` (v12+)
- **NEVER use `require()`** — ESM-only project
- **NEVER co-locate feature code in route directories** — use `~/modules/diagram/`
- **NEVER store ELK-computed positions in persisted graph data** — positions are ephemeral
- **NEVER use compound/hierarchical ELK layout for E-R** — flat layout is correct (no pools/lanes)
- **DO NOT implement Smart Inspector for E-R** — that's a future story
- **DO NOT implement SQL DDL export** — that's Story 6.2
- **DO NOT implement other diagram type renderers** — those are Stories 2.5-2.8
- **DO NOT break existing tests** — 81 web tests must continue passing (50 original + 31 BPMN)
- **DO NOT use inline styles for E-R colors** — use CSS custom properties (`--diagram-er`)
- **DO NOT use BPMN-specific pool/lane logic for E-R** — E-R is flat graph only

### Previous Story Intelligence (Story 2.3 — BPMN)

**Key learnings to carry forward:**
- Constants file pattern: `BPMN_SIZES` map → `ER_SIZES` equivalent. Type resolution functions: `resolveXNodeType`, `resolveXEdgeType`
- Node component pattern: Cast `data` as `DiagramNode & { label: string }`, use `Handle` with `style={{ opacity: 0 }}` at 4 positions
- Edge component pattern: `BaseEdge` + `getSmoothStepPath`, `markerEnd` for arrows, `label` + `labelShowBg` for midpoint labels
- Graph converter: `resolveFlowNodeType` switch on `diagramType`, import type resolver from `types/[type]/constants`
- DiagramCanvas: import components, add to `nodeTypes`/`edgeTypes` objects, add marker defs SVG
- CSS: Use `--diagram-[type]` CSS variable for accent colors, all styles inside `@layer base { :root { ... } }` block
- `graphNodeToFlowNode` already spreads all DiagramNode fields into `data` — custom nodes access `data.columns` directly
- `graphEdgeToFlowEdge` already spreads all DiagramEdge fields into `data` — custom edges access `data.cardinality` directly
- `flowNodeToGraphNode` already preserves `columns` field — roundtrip works
- `flowEdgeToGraphEdge` already preserves `cardinality` field — roundtrip works
- 81 web tests currently pass (50 original + 31 BPMN) — don't break them

### Previous Story Intelligence (Story 2.2 — ELK Layout)

**Key learnings:**
- `buildElkGraph()` is the flat layout builder — E-R uses this directly
- `resolvePositions()` handles flat graphs — E-R uses this directly
- `computeLayout()` detects BPMN compound layout via `isCompound = nodes.some(n => n.type === "bpmnPool")` — E-R won't trigger this since it has no pools
- Entity height must be known at ELK time for correct spacing — compute from `columns.length`

### Git Intelligence

Recent commits:
- `0a7838a feat: implement Story 2.3 — BPMN diagram type renderer`
- `7dd5af1 feat: implement Story 2.2 — ELK.js auto-layout engine in Web Worker`
- `5033109 feat: implement Story 2.1 — canvas workspace with @xyflow/react and unified graph model`

Established patterns:
- Commit message: `feat: implement Story X.Y — description`
- Feature code in `apps/web/src/modules/diagram/`
- Co-located tests next to source files
- `diagramTypeConfig` in DiagramCard.tsx has E-R config already: `{ label: "E-R", icon: Icons.Database, color: "text-violet-500" }`
- CSS variable `--diagram-er: oklch(0.606 0.25 293)` already defined in globals.css

### Latest Tech Information

**@xyflow/react 12.10.1 — EdgeLabelRenderer API:**
- `EdgeLabelRenderer` renders edge labels in a portal layer above edges — required for cardinality labels at endpoints
- Position labels using `transform: translate(x, y)` with absolute positioning inside the label renderer
- Must add `pointer-events: none` on cardinality labels to prevent them from blocking edge/node interactions
- `getSmoothStepPath` returns `[path, labelX, labelY, offsetX, offsetY]` — `labelX/Y` is the midpoint, `offsetX/Y` is the offset for the label

**@xyflow/react 12.10.1 — BaseEdge label support:**
- `label` prop renders text at edge midpoint
- `labelShowBg` adds a background rectangle behind the label
- `labelBgStyle` accepts CSS-like style object for the background
- `labelStyle` for the text styling

**Column type in DiagramNode (already defined):**
```typescript
interface Column {
  name: string;
  type: string;
  isPrimaryKey?: boolean;
  isForeignKey?: boolean;
  isNullable?: boolean;
  isUnique?: boolean;
  references?: string;
}
```

### References

- [Source: _bmad-output/planning-artifacts/epics.md#Story 2.4] — Full AC and technical notes
- [Source: _bmad-output/planning-artifacts/architecture.md#Decision 1] — Unified Graph Data Model (hybrid schema, E-R extensions: columns, cardinality)
- [Source: _bmad-output/planning-artifacts/architecture.md#Enforcement Guidelines] — 7 mandatory rules, type prefixing: `er:entity`
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Diagram Type Renderers] — E-R: Entity with field rows, Relationship with cardinality
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Journey 3] — Elena's E-R schema editing flow
- [Source: _bmad-output/planning-artifacts/ux-design-specification.md#Diagram Type Color Accents] — E-R: violet oklch(0.606 0.25 293)
- [Source: _bmad-output/implementation-artifacts/2-3-bpmn-diagram-type-renderer.md] — BPMN implementation patterns (constants, nodes, edges, converter, canvas registration)
- [Source: _bmad-output/project-context.md] — 62 critical implementation rules
- [Source: apps/web/src/modules/diagram/types/graph.ts] — DiagramNode (columns), DiagramEdge (cardinality), Column interface
- [Source: apps/web/src/modules/diagram/lib/graph-converter.ts] — Current converter with BPMN resolution to extend for E-R
- [Source: apps/web/src/modules/diagram/lib/elk-layout.ts] — Current flat layout builder to reuse for E-R
- [Source: apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx] — Canvas to register E-R types
- [Source: apps/web/src/assets/styles/globals.css] — CSS with `--diagram-er` already defined

## Dev Agent Record

### Agent Model Used

Claude Opus 4.6

### Debug Log References

None — clean implementation with no failures.

### Completion Notes List

- **Task 1:** Created `constants.ts` with `ER_SIZES`, `getErEntityHeight()`, `resolveErNodeType()`, `resolveErEdgeType()`. Created `index.ts` barrel export.
- **Task 2:** Created `ErEntityNode.tsx` — table-like card with violet header, attribute rows showing PK (key emoji), FK (arrow), nullable (?), unique (U) indicators. 4 handles for multi-direction edge connections.
- **Task 3:** Created `ErRelationshipEdge.tsx` — uses `EdgeLabelRenderer` for cardinality labels at source/target endpoints (24px offset along edge direction). Midpoint label via `BaseEdge` built-in label with background pill. Violet-colored edge stroke and arrow marker.
- **Task 4:** Updated `graph-converter.ts` — added `er` diagramType and `er:` prefix resolution for both nodes and edges. Import from `er/constants`. Data fields (`columns`, `cardinality`) already preserved by existing spread pattern — no additional changes needed.
- **Task 5:** Updated `DiagramCanvas.tsx` — registered `erEntity` and `erRelationship` in `nodeTypes`/`edgeTypes` objects. Consolidated `BpmnMarkerDefs` into `MarkerDefs` containing both BPMN and E-R SVG markers.
- **Task 6:** Updated `elk-layout.ts` — `buildElkGraph` now computes E-R entity height from `columns` data via `getErEntityHeight()` for correct ELK spacing on initial layout before DOM measurement. E-R uses flat layout (no compound detection triggered since no pools).
- **Task 7:** Added E-R CSS styles to `globals.css` — entity card, header, body, attribute rows, indicators, cardinality labels. All using `--diagram-er` violet accent. Also fixed highlighted node drop-shadow to use `--node-selected` instead of hardcoded `--diagram-bpmn`.
- **Task 8:** 12 new tests (9 constants + 3 converter). All 93 tests pass.
- **Code Review Fixes:** 4 MEDIUM issues fixed — cardinality label positioning (M1), M:N junction table test coverage (M2), type-guarded ELK height computation (M3), CSS variable for hover color (M4). 4 new tests added. All 97 tests pass.

### Change Log

- 2026-02-24: Implemented Story 2.4 — E-R diagram type renderer with entity nodes, relationship edges with cardinality, graph converter integration, ELK layout support, and CSS styles.
- 2026-02-24: Code review (Opus 4.6) — Fixed 4 MEDIUM issues: M1 cardinality label alignment on orthogonal edges, M2 added M:N junction table test for AC #3, M3 type-guarded buildElkGraph columns check, M4 CSS hover color uses color-mix instead of hardcoded oklch. 97 tests pass.

### File List

New files:
- `apps/web/src/modules/diagram/types/er/constants.ts`
- `apps/web/src/modules/diagram/types/er/constants.test.ts`
- `apps/web/src/modules/diagram/types/er/ErEntityNode.tsx`
- `apps/web/src/modules/diagram/types/er/ErRelationshipEdge.tsx`

Modified files:
- `apps/web/src/modules/diagram/lib/graph-converter.ts`
- `apps/web/src/modules/diagram/lib/graph-converter.test.ts`
- `apps/web/src/modules/diagram/lib/elk-layout.ts`
- `apps/web/src/modules/diagram/lib/elk-layout.test.ts`
- `apps/web/src/modules/diagram/components/editor/DiagramCanvas.tsx`
- `apps/web/src/assets/styles/globals.css`
- `_bmad-output/implementation-artifacts/sprint-status.yaml`
