{
  "schemaVersion": "1.0",
  "id": "er-diagram-from-migrations",
  "title": "ER diagram from database migrations",
  "slug": "er-diagram-from-migrations",
  "diagramFamily": "entityRelationship",
  "goal": "Keep an ER diagram in sync with database migrations on every PR. Parse the migration files changed in the PR, derive entity / column / FK changes, patch the persisted Zindex scene with typed operations, validate, render, and post the visual diff back to the PR.",
  "inputs": [
    "List of migration files changed in the PR (Prisma / Drizzle / Alembic / Rails)",
    "Existing Zindex scene id (stored as a repo secret on first run)",
    "Zindex API key with scene-write scope"
  ],
  "outputs": [
    "Updated persisted scene with a new immutable revision",
    "Rendered SVG of the new schema",
    "Revision diff (JSON) describing what entities / columns / FKs changed",
    "PR comment with the rendered diagram and summary of schema changes"
  ],
  "mcpTools": [
    "dsp_get_scene",
    "dsp_apply_ops",
    "dsp_validate_scene",
    "dsp_render_scene",
    "dsp_diff_scene"
  ],
  "httpEndpoints": [
    "GET /v1/scenes/:id",
    "POST /v1/scenes/:id/applyOps",
    "POST /v1/scenes/:id/render",
    "GET /v1/scenes/:id/diff"
  ],
  "steps": [
    {
      "id": "create_or_fetch_scene",
      "label": "Create or fetch the persisted scene",
      "description": "On first run, create a scene with diagramFamily=entityRelationship and an LR hierarchical layoutStrategy. On subsequent runs, fetch the existing scene by id to read the current revision.",
      "mcp": "dsp_create_scene",
      "http": {
        "method": "POST",
        "path": "/v1/scenes",
        "exampleRequest": "{\n  \"schemaVersion\": \"0.1\",\n  \"diagramFamily\": \"entityRelationship\",\n  \"scene\": {\n    \"id\": \"blog-er\",\n    \"title\": \"Blog Entity Relationship\",\n    \"canvas\": { \"width\": 1100, \"height\": 600 }\n  },\n  \"layoutStrategy\": {\n    \"algorithm\": \"hierarchical\",\n    \"direction\": \"LR\",\n    \"nodeSpacing\": 60,\n    \"rankSpacing\": 140\n  },\n  \"elements\": []\n}",
        "exampleResponse": "{\n  \"sceneId\": \"sc_a1b2c3\",\n  \"revision\": 1\n}"
      }
    },
    {
      "id": "parse_migrations",
      "label": "Parse changed migration files",
      "description": "Read the migration files added or modified in the PR. Extract the table-level changes: new tables, dropped tables, added columns, renamed columns, dropped columns, new foreign keys, and new indexes. Most ORMs leave a structured trail (Prisma's create/alter blocks, Alembic op.create_table / op.add_column, Rails CreateTable / AddColumn) - parse those rather than free-text SQL.",
      "mcp": null,
      "http": null
    },
    {
      "id": "diff_against_scene",
      "label": "Diff parsed schema against the persisted scene",
      "description": "For each entity in the persisted scene, check whether it still exists in the parsed schema. For each column inside each entity, check whether it was added, removed, renamed, or had its type changed. Emit a list of intended ops (createNode / updateNode / deleteElement / createEdge / updateEdge) - do not yet apply them.",
      "mcp": "dsp_get_scene",
      "http": {
        "method": "GET",
        "path": "/v1/scenes/${SCENE_ID}",
        "exampleResponse": "{\n  \"sceneId\": \"sc_a1b2c3\",\n  \"revision\": 12,\n  \"scene\": { \"id\": \"blog-er\", \"...\": \"...\" },\n  \"elements\": [ \"...\" ]\n}"
      }
    },
    {
      "id": "apply_ops",
      "label": "Apply the typed-operation batch",
      "description": "Send the diff as a single applyOps batch with errorPolicy=allOrNothing so the revision either commits cleanly or fails atomically. Always reuse stable ids (`user`, `post`, etc.) so renames produce a true updateNode rather than a delete+create. The element id is the schema's stable handle; do not regenerate it on every run.",
      "mcp": "dsp_apply_ops",
      "http": {
        "method": "POST",
        "path": "/v1/scenes/${SCENE_ID}/ops",
        "exampleRequest": "{\n  \"schemaVersion\": \"0.1\",\n  \"errorPolicy\": \"allOrNothing\",\n  \"revisionMessage\": \"add Comments table from migration 20260415\",\n  \"ops\": [\n    {\n      \"op\": \"createNode\",\n      \"id\": \"comment\",\n      \"nodeType\": \"er.entity\",\n      \"shape\": \"rect\",\n      \"label\": \"Comments\",\n      \"extensions\": { \"columns\": [\"...\"] }\n    }\n  ]\n}",
        "exampleResponse": "{\n  \"sceneId\": \"sc_a1b2c3\",\n  \"revision\": 13,\n  \"applied\": 4\n}"
      }
    },
    {
      "id": "validate",
      "label": "Validate the new revision",
      "description": "Confirm the scene still validates against ER semantics: every FK column referenced by an edge resolves, every entity has a primary key, no duplicate column names within an entity. The platform emits structured diagnostic codes (EDGE_COLUMN_NOT_FOUND, LABEL_DUPLICATION_DETECTED, …) - fix EDGE_COLUMN_NOT_FOUND and LABEL_DUPLICATION_DETECTED before continuing; CANVAS_AUTO_EXTENDED and EDGE_LABEL_SUPPRESSED_REDUNDANT are informational and can ship.",
      "mcp": "dsp_validate_scene",
      "http": {
        "method": "POST",
        "path": "/v1/scenes/validate",
        "exampleResponse": "{\n  \"ok\": true,\n  \"diagnostics\": []\n}"
      }
    },
    {
      "id": "render",
      "label": "Render the updated scene",
      "description": "Render the new revision to SVG. The watermark stamps scene id + revision + date so the PR artifact is traceable back to the persisted scene without metadata sidecar files.",
      "mcp": "dsp_render_scene",
      "http": {
        "method": "POST",
        "path": "/v1/scenes/${SCENE_ID}/render",
        "exampleRequest": "{ \"format\": \"svg\", \"theme\": \"clean\" }",
        "exampleResponse": "{\n  \"output\": { \"format\": \"svg\", \"content\": \"<svg ...>\" },\n  \"revision\": 13,\n  \"diagnostics\": []\n}"
      }
    },
    {
      "id": "diff_revisions",
      "label": "Diff against the previous revision",
      "description": "Ask the platform for the structural diff between the prior revision (recorded on the previous CI run) and the new one. The response lists added / removed / changed elements - perfect for a humanly-readable PR comment, and it does not require the agent to re-derive the diff on its own.",
      "mcp": "dsp_diff_scene",
      "http": {
        "method": "GET",
        "path": "/v1/scenes/${SCENE_ID}/diff?from=${PREV_REVISION}&to=${NEW_REVISION}",
        "exampleResponse": "{\n  \"added\":   [\"comment\", \"e-comments\", \"e-has-comments\"],\n  \"removed\": [],\n  \"changed\": [\"post\"],\n  \"summary\": \"+1 entity, +2 relationships, 1 column added to post\"\n}"
      }
    },
    {
      "id": "post_pr_comment",
      "label": "Post the rendered diagram + diff to the PR",
      "description": "Comment on the PR with the rendered SVG (or a link to the .svg artifact) and the human-readable diff summary. Re-comment idempotently on subsequent commits to keep the latest snapshot pinned.",
      "mcp": null,
      "http": null
    }
  ]
}