← Examples

Visual architecture diffs in pull requests

Let an agent update an architecture diagram when a PR changes services, dependencies, queues, databases, or API edges - then post the visual diff (added=green, removed=red, modified=amber) directly into the PR comment.

architecture Pull-request diff scanupdatediff

What this example shows

The “after” architecture for a PR that introduces a new Payments service plus its connections to Stripe, the orders flow, and PostgreSQL. Scroll to the Revision diff panel below to see the captured before and after diagrams side-by-side along with the structured dsp_diff_scene response listing the four added elements (Payments, Stripe, the two new edges) and the modified Orders service.

When to use it

When a PR meaningfully changes the system’s shape - adds a new service, replaces a database, removes a dependency, swaps a vendor - and you want reviewers to see that before they merge. Architecture diagrams are notoriously stale because nobody updates them; this pattern flips it: the architecture diagram updates on every PR, and the diff lands in the PR comment automatically.

What the agent does

Triggered by a pull-request webhook, the agent fetches the persisted “main” scene representing current production architecture, reads the PR diff to identify added / removed / modified services and dependencies, applies typed operations (createNode, createEdge, deleteElement) to a fresh revision, and calls dsp_diff_scene to compare the new revision against main. The result is rendered with diff styling - additions in green, removals in red, modifications in amber - and posted to the PR as a comment.

If the PR is merged, the new revision becomes the canonical “main” scene. If closed without merging, the revision is dropped and main is unchanged. The persisted-scene model means architectural state is durable and auditable across PRs without ever being regenerated from scratch.

What the output includes

  • A “before” + “after” pair of architecture scenes per PR.
  • A visual diff highlighting added (green), removed (red), and modified (amber) elements.
  • A scene-diff JSON for programmatic consumers (CI gates that block merges introducing forbidden dependencies, for example).
  • A PR comment template the bot posts automatically with the rendered diff inline.
  • A revision history accessible via dsp_list_revisions showing every PR’s architectural delta over time.

Tip: The CI/CD recipe panel below contains the complete runnable GitHub Actions workflow that produces the side-by-side diff on every pull request - copy it into .github/workflows/zindex-pr-arch-diff.yml, add the listed secrets, and PR comments with before / after diagrams start landing on the next push.

Rendered diagram

Visual architecture diffs in pull requests — rendered diagram

Built by Zindex from the canonical scene below. Open in Playground to swap themes (clean / dark / blueprint / sketch), or POST the scene to /v1/scenes/render with format: "png" for a rasterised version.

Scene JSON

Raw

The canonical DSP scene used to render the diagram above. Drop into the Playground or POST to /v1/scenes/render to reproduce.

{
  "schemaVersion": "0.1",
  "diagramFamily": "architecture",
  "scene": {
    "id": "pr-architecture-after",
    "title": "Architecture (after PR adds Payments service)",
    "units": "px",
    "canvas": {
      "width": 1100,
      "height": 520
    }
  },
  "layoutStrategy": {
    "algorithm": "hierarchical",
    "direction": "LR",
    "nodeSpacing": 50,
    "rankSpacing": 120
  },
  "elements": [
    {
      "id": "client",
      "kind": "node",
      "nodeType": "actor",
      "shape": "roundedRect",
      "label": "Web Client",
      "icon": "lucide:globe"
    },
    {
      "id": "gateway",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network"
    },
    {
      "id": "auth",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth Service",
      "icon": "lucide:lock"
    },
    {
      "id": "users",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users Service",
      "icon": "lucide:users"
    },
    {
      "id": "orders",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders Service",
      "icon": "lucide:package"
    },
    {
      "id": "payments",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Payments Service",
      "icon": "lucide:credit-card"
    },
    {
      "id": "stripe",
      "kind": "node",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Stripe",
      "icon": "lucide:external-link"
    },
    {
      "id": "postgres",
      "kind": "node",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "PostgreSQL",
      "icon": "lucide:database"
    },
    {
      "id": "redis",
      "kind": "node",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "Redis",
      "icon": "lucide:database"
    },
    {
      "id": "e_client_gw",
      "kind": "edge",
      "from": {
        "elementId": "client"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_auth",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "validates",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_users",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_gw_orders",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_orders_payments",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "payments"
      },
      "label": "triggers",
      "router": "orthogonal"
    },
    {
      "id": "e_payments_stripe",
      "kind": "edge",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "charges",
      "router": "orthogonal"
    },
    {
      "id": "e_users_db",
      "kind": "edge",
      "from": {
        "elementId": "users"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_orders_db",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_payments_db",
      "kind": "edge",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_orders_redis",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "redis"
      },
      "label": "cache",
      "router": "orthogonal"
    }
  ]
}

Agent workflow

Surface architectural impact during PR review by deriving the architecture state from the PR's diff, comparing it to the architecture on main, and posting the visual diff back to the PR - so reviewers see what services and dependencies a PR adds or removes without re-reading the change set.

Inputs

  • PR diff (list of changed files in the PR)
  • Repository checkout at the PR head AND at the merge-base
  • Existing Zindex scene id storing the architecture on main (long-lived, never recreated)
  • Zindex API key with scene-write scope

Outputs

  • Two transient revisions - 'before' (= main's revision) and 'after' (= main + PR changes)
  • Rendered before-after diagrams as separate SVGs (or a single side-by-side composite)
  • Structural diff JSON listing added / removed / changed elements
  • PR comment with the diff summary + rendered diagrams, idempotently re-posted as the PR evolves
  1. 01

    Fetch the architecture scene on main

    Get the persisted scene representing the architecture on the default branch. Note the current revision number - that's the 'before' baseline.

    dsp_get_scene GET /v1/scenes/${SCENE_ID}
  2. 02

    Scan the repository at the PR head

    Check out the PR branch. Run the same component-detection logic that the living-architecture workflow uses, but against the PR's tree. Emit the (component, edge) tuple that represents what the architecture *would be* if this PR merged.

  3. 03

    Compute the operation batch that would update main

    Diff the PR-head scan against the persisted main scene. Emit a hypothetical applyOps batch (createNode for new services, deleteElement for removed ones, createEdge / deleteElement for changed dependencies). Do NOT apply this batch to the production scene - this is a what-if comparison, not a merge.

  4. 04

    Apply ops to a transient branch revision

    Apply the computed ops to the persisted scene with a revision message tagged with the PR number - `pr-1234: hypothetical post-merge state`. This creates a new revision; you'll diff against it and then leave it. (Optional: tag the revision with the PR id so you can clean up old branch revisions on PR close.)

    dsp_apply_ops POST /v1/scenes/${SCENE_ID}/ops
  5. 05

    Diff main's revision against the PR's hypothetical revision

    Call dsp_diff_scene with from = main's revision, to = the PR's branch revision. The output describes precisely what the PR adds and removes from the architecture - far more reviewable than re-reading source files.

    dsp_diff_scene GET /v1/scenes/${SCENE_ID}/diff?from=${MAIN_REVISION}&to=${PR_REVISION}
  6. 06

    Render before-and-after diagrams

    Render the scene at main's revision (before) and at the PR's revision (after). Both watermarks include the revision number so the reviewer can verify exactly which state each diagram represents.

    dsp_render_scene POST /v1/scenes/${SCENE_ID}/render
  7. 07

    Post the architectural diff to the PR

    Comment on the PR with both rendered SVGs (or a side-by-side composite) and the structural diff summary. Re-post idempotently as new commits land on the PR - overwrite the previous comment rather than appending. On PR merge, the next scheduled living-architecture run will pick up the merged change and the branch revision is no longer authoritative.

  8. 08

    Optional: roll back the branch revision on PR close

    If the PR closes without merging, the branch revision in the persisted scene no longer represents reality. Either delete the revision (if revision deletion is supported in your environment) or rely on the next scheduled main-branch sync to overwrite it. Most teams just leave branch revisions in place - the revision history then doubles as a record of architectural proposals, accepted and rejected.

MCP recipe

For agents using Model Context Protocol. The tool sequence below matches the workflow steps; copy the prompt as a system message.

Tool sequence

  1. 01 dsp_get_scene Fetch the architecture scene on main
  2. 02 dsp_apply_ops Apply ops to a transient branch revision
  3. 03 dsp_diff_scene Diff main's revision against the PR's hypothetical revision
  4. 04 dsp_render_scene Render before-and-after diagrams

Unique tools used: dsp_get_scene, dsp_apply_ops, dsp_diff_scene, dsp_render_scene.

Copyable agent prompt

Drop this verbatim into a system prompt for an MCP-connected agent. The Zindex MCP server (@zindex-ai/mcp, configured with a ZINDEX_API_KEY environment variable - setup guide) exposes the tools the prompt references.

You are an automated PR-review agent. Your job is to surface the architectural impact of a pull request by deriving what the architecture would look like if the PR merged, comparing that against the architecture on main, and posting the visual diff back to the PR - so a human reviewer can see at a glance which services and dependencies the PR adds, removes, or rewires.

The persisted Zindex scene id is `${SCENE_ID}`; it represents the architecture on the default branch and is updated independently by the living-architecture workflow on every default-branch commit. You will not modify main's revision history. Instead, you will create transient PR-tagged revisions for the diff comparison.

Workflow on every PR (open + every push to the PR branch):

1. Call `dsp_get_scene({ sceneId: "${SCENE_ID}" })`. Read the current revision number - call this `MAIN_REVISION`. This is the 'before' state for the diff.

2. Check out the repository at the PR head. Run the same component-detection logic the living-architecture workflow uses (services from `services/*/Dockerfile`, databases from URL strings, queues from imports, external systems from SDK packages) - but apply it to the PR's tree, not main's. Emit a `(components, edges)` tuple that represents the architecture *as the PR would leave it*.

3. Compute the operation batch that would move the persisted scene from its current state to the PR's hypothetical state. New components → `createNode`; removed components → `deleteElement`; changed labels / endpoints → `updateEdge`. Reuse stable element ids (the directory name / service name) so renames produce `updateNode` rather than delete-and-create.

4. Call `dsp_apply_ops` with that batch. Set `errorPolicy: "allOrNothing"` and use a `revisionMessage` tagged with the PR number - `pr-1234: hypothetical post-merge state`. This produces a new revision in the persisted scene; call this `PR_REVISION`. (Optional: also pass a `tags` field so you can clean up old PR revisions on close, if your environment supports tagged-revision cleanup.)

5. Call `dsp_diff_scene({ from: MAIN_REVISION, to: PR_REVISION })`. The platform returns a structural diff: which elements were added, removed, or changed. This is the canonical answer to 'what does this PR change architecturally?' - and it is precisely what you'll surface in the PR comment.

6. Call `dsp_render_scene` twice - once at `MAIN_REVISION` (the 'before' diagram) and once at `PR_REVISION` (the 'after' diagram). The watermark includes the revision number on both, so reviewers can verify which state each image represents. Render both at `theme: "clean"` and the same dimensions so they compare visually.

7. Post a single PR comment containing: the diff summary (`+1 service, -2 edges, 1 service renamed`), the before SVG, the after SVG, and a short prose description if the diff is non-trivial. Re-post idempotently on every new push to the PR branch - overwrite the previous comment rather than appending - so the reviewer always sees the latest comparison pinned at the top of the conversation.

8. On PR close (merged or rejected), do nothing about `PR_REVISION` - the next scheduled living-architecture run on main will produce a new authoritative revision regardless. The PR-tagged revisions can stay in the history as a record of architectural proposals (some accepted, some not).

Hard rules: never modify the scene under any other revision message - main's history must reflect what actually shipped, not what was proposed. Never collapse the diff into a single 'after' diagram without the 'before' for comparison; the value here is the comparison, not just the rendered output. If the diff is empty (the PR is purely cosmetic / non-architectural), comment with 'No architectural changes detected' so the reviewer knows the workflow ran but had nothing to report.

HTTP API recipe

For agents/devs not using MCP. Set $ZINDEX_API_KEY in the Authorization header on authenticated calls. Stateless endpoints (/v1/scenes/render, /v1/scenes/validate, /v1/scenes/normalize) need no key. Full reference: API endpoints, OpenAPI spec.

  1. 01 GET /v1/scenes/${SCENE_ID}

    Get the persisted scene representing the architecture on the default branch. Note the current revision number - that's the 'before' baseline.

  2. 02 POST /v1/scenes/${SCENE_ID}/ops

    Apply the computed ops to the persisted scene with a revision message tagged with the PR number - `pr-1234: hypothetical post-merge state`. This creates a new revision; you'll diff against it and then leave it. (Optional: tag the revision with the PR id so you can clean up old branch revisions on PR close.)

  3. 03 GET /v1/scenes/${SCENE_ID}/diff?from=${MAIN_REVISION}&to=${PR_REVISION}

    Call dsp_diff_scene with from = main's revision, to = the PR's branch revision. The output describes precisely what the PR adds and removes from the architecture - far more reviewable than re-reading source files.

  4. 04 POST /v1/scenes/${SCENE_ID}/render

    Render the scene at main's revision (before) and at the PR's revision (after). Both watermarks include the revision number so the reviewer can verify exactly which state each diagram represents.

Operations

Raw

The typed-operation envelope that builds this scene from empty. POST to /v1/scenes/:id/ops after creating a scene, or pass to dsp_apply_ops. Each op carries a stable id so subsequent runs can update the same elements instead of regenerating.

  • 19 operations
  • 10 createEdge, 9 createNode
{
  "schemaVersion": "0.1",
  "errorPolicy": "allOrNothing",
  "revisionMessage": "Initial baseline architecture for PR diff comparison",
  "ops": [
    {
      "op": "createNode",
      "id": "client",
      "nodeType": "actor",
      "shape": "roundedRect",
      "label": "Web Client",
      "icon": "lucide:globe"
    },
    {
      "op": "createNode",
      "id": "gateway",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network"
    },
    {
      "op": "createNode",
      "id": "auth",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth Service",
      "icon": "lucide:lock"
    },
    {
      "op": "createNode",
      "id": "users",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users Service",
      "icon": "lucide:users"
    },
    {
      "op": "createNode",
      "id": "orders",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders Service",
      "icon": "lucide:package"
    },
    {
      "op": "createNode",
      "id": "payments",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Payments Service",
      "icon": "lucide:credit-card"
    },
    {
      "op": "createNode",
      "id": "stripe",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Stripe",
      "icon": "lucide:external-link"
    },
    {
      "op": "createNode",
      "id": "postgres",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "PostgreSQL",
      "icon": "lucide:database"
    },
    {
      "op": "createNode",
      "id": "redis",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "Redis",
      "icon": "lucide:database"
    },
    {
      "op": "createEdge",
      "id": "e_client_gw",
      "from": {
        "elementId": "client"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_auth",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "validates",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_users",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_orders",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_payments",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "payments"
      },
      "label": "triggers",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_payments_stripe",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "charges",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_users_db",
      "from": {
        "elementId": "users"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_db",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_payments_db",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_redis",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "redis"
      },
      "label": "cache",
      "router": "orthogonal"
    }
  ]
}

Validation

Valid

Captured response from POST /v1/scenes/validate. The platform runs 40+ semantic checks; see the full list in the validation rules reference.

  • 0 diagnostics

Scene validates with no diagnostics. Agents that produce scenes like this can ship the rendered SVG without a recovery loop.

Codes the platform can emit include TEXT_OVERFLOW, CANVAS_AUTO_EXTENDED, EDGE_LABEL_SUPPRESSED_REDUNDANT, EDGE_LABEL_SUPPRESSED_FANIN, EDGE_COLUMN_NOT_FOUND, LABEL_DUPLICATION_DETECTED, LAYOUT_ABSOLUTE_AT_ORIGIN, and MISSING_DIAGRAM_FAMILY. Each diagnostic carries a structured data field with element ids and context an agent can act on programmatically.

Revision diff

Raw

The structural diff between two revisions of the persisted scene — the response shape dsp_diff_scene returns. Stateful diagram evolution is Zindex's strongest differentiator: the same scene id evolves through immutable revisions, each diffable.

Evolution scenario

A pull request introduces the new Payments service and its Stripe SDK integration. The Orders service gains an outbound edge to Payments. The reviewer sees the diff, the rendered before-and-after diagrams, and a single comment summarising the architectural impact - far easier than re-reading the source diff to figure out which services are now talking to which.

  • Revision 8889
  • +4 added
  • -0 removed
  • ~1 modified
Before · revision 88
Visual architecture diffs in pull requests — revision 88
After · revision 89
Visual architecture diffs in pull requests — revision 89

+ Added

  • payments
  • stripe
  • e_orders_payments
  • e_payments_stripe

~ Modified

  • orders

Raw dsp_diff_scene response

{
  "schemaVersion": "1.0",
  "sceneId": "saas-platform-arch",
  "fromRevision": 88,
  "toRevision": 89,
  "summary": {
    "added": 4,
    "removed": 0,
    "modified": 1
  },
  "added": [
    "payments",
    "stripe",
    "e_orders_payments",
    "e_payments_stripe"
  ],
  "removed": [],
  "modified": [
    "orders"
  ],
  "scenario": "A pull request introduces the new Payments service and its Stripe SDK integration. The Orders service gains an outbound edge to Payments. The reviewer sees the diff, the rendered before-and-after diagrams, and a single comment summarising the architectural impact - far easier than re-reading the source diff to figure out which services are now talking to which."
}

CI/CD recipe

Pull request Raw YAML

A complete, runnable GitHub Actions workflow for this example. Drop the YAML into .github/workflows/zindex-pr-architecture-diff.yml, add the listed secrets, and the agent runs unattended on every qualifying trigger. Re-comments idempotently using a hidden marker so the PR conversation stays clean across pushes.

Trigger

Runs on every PR (no path filter - every PR potentially has architectural impact, even if the diff turns out to be empty). Cancels in-flight runs when new commits land on the same PR so reviewers always see comparison against the latest commit.

Required secrets

  • ZINDEX_API_KEY required Zindex API key with scene-write scope.
  • ZINDEX_SCENE_ID required The same long-lived scene id used by the living-architecture workflow. PR runs create transient PR-tagged revisions; they don't compete with main's history.

Inputs

  • scripts/scan-architecture.mjs (shared with the living-architecture workflow)
  • scripts/architecture-to-ops.mjs - same script, called with --revision-message naming the PR number

Outputs

  • out/before.svg + out/after.svg - pre-PR and post-PR architecture diagrams
  • out/diff.json - structural diff (the canonical 'what changed architecturally?' answer)
  • PR comment with side-by-side download link, or a clear 'no architectural changes' message when the diff is empty
  • Workflow artifact 'pr-architecture-diff' (30-day retention)

GitHub Actions workflow

# Zindex - PR architecture diff. Surfaces architectural impact in every PR
# review by deriving the architecture from the PR head, comparing against
# main's persisted scene, and posting before-and-after diagrams as a PR comment.
# Unlike the living-architecture workflow, this never modifies main's revision
# history - it creates transient PR-tagged revisions for the diff comparison
# only.
#
# Drop into .github/workflows/zindex-pr-arch-diff.yml.

name: Zindex - PR architecture diff

on:
  pull_request:
    types: [opened, synchronize, reopened]
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: true

env:
  ZINDEX_API_BASE: https://api.zindex.ai

jobs:
  pr-architecture-diff:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # need merge-base for the comparison

      # 1. Capture main's revision - the "before" baseline. Don't modify it.
      - name: Capture main revision
        id: main_rev
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          echo "rev=$(curl -fsSL -H "Authorization: Bearer $ZINDEX_API_KEY" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID" | jq -r '.revision')" >> "$GITHUB_OUTPUT"

      # 2. Scan the PR head's tree (NOT main). Same convention map as the
      #    living-architecture workflow - but applied to the PR's source.
      - name: Scan PR head for architectural state
        run: |
          mkdir -p out
          node scripts/scan-architecture.mjs > out/architecture-after.json

      # 3. Compute the "would-be" applyOps batch. Tag the revision message
      #    with the PR number so transient PR revisions are easy to spot in
      #    the audit log.
      - name: Compute hypothetical applyOps batch
        run: |
          node scripts/architecture-to-ops.mjs \
            --revision-message "pr-${{ github.event.pull_request.number }}: hypothetical post-merge state" \
            out/architecture-after.json > out/ops.json

      # 4. Apply ops - produces a new revision representing the PR's "after"
      #    state. This is intentional: the persisted scene now temporarily
      #    holds main + this PR's hypothetical changes. The next scheduled
      #    main-branch run will overwrite this when the PR merges (or you
      #    can leave PR revisions in place as a record of architectural
      #    proposals).
      - name: Apply ops as PR-tagged revision
        id: pr_apply
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          RESP=$(curl -fsSL -X POST \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            -H "Content-Type: application/json" \
            --data-binary @out/ops.json \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/ops")
          echo "rev=$(echo "$RESP" | jq -r '.revision')" >> "$GITHUB_OUTPUT"

      # 5. Diff main → PR. This is the canonical answer to "what does this
      #    PR change architecturally?" - surface this in the comment.
      - name: Diff main → PR
        id: diff
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
          MAIN: ${{ steps.main_rev.outputs.rev }}
          PR: ${{ steps.pr_apply.outputs.rev }}
        run: |
          curl -fsSL -H "Authorization: Bearer $ZINDEX_API_KEY" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/diff?from=$MAIN&to=$PR" \
            > out/diff.json
          ADDED=$(jq -r '.summary.added' out/diff.json)
          REMOVED=$(jq -r '.summary.removed' out/diff.json)
          MODIFIED=$(jq -r '.summary.modified' out/diff.json)
          echo "added=$ADDED" >> "$GITHUB_OUTPUT"
          echo "removed=$REMOVED" >> "$GITHUB_OUTPUT"
          echo "modified=$MODIFIED" >> "$GITHUB_OUTPUT"
          TOTAL=$((ADDED + REMOVED + MODIFIED))
          echo "total=$TOTAL" >> "$GITHUB_OUTPUT"

      # 6. Render BOTH revisions for the side-by-side comparison.
      - name: Render before (main)
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
          REV: ${{ steps.main_rev.outputs.rev }}
        run: |
          curl -fsSL -X POST \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"format\":\"svg\",\"theme\":\"clean\",\"revision\":$REV}" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/render" \
            | jq -r '.output.content' > out/before.svg

      - name: Render after (PR)
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
          REV: ${{ steps.pr_apply.outputs.rev }}
        run: |
          curl -fsSL -X POST \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            -H "Content-Type: application/json" \
            -d "{\"format\":\"svg\",\"theme\":\"clean\",\"revision\":$REV}" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/render" \
            | jq -r '.output.content' > out/after.svg

      - uses: actions/upload-artifact@v4
        id: upload
        with:
          name: pr-architecture-diff
          path: |
            out/before.svg
            out/after.svg
            out/diff.json
          retention-days: 30

      - uses: peter-evans/find-comment@v3
        id: find_comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: "github-actions[bot]"
          body-includes: "<!-- zindex-bot:pr-architecture-diff -->"

      # 7. Post the diff comment. If the PR has no architectural impact
      #    (total = 0), be honest about it - agents shouldn't generate
      #    noise about non-architectural PRs.
      - uses: peter-evans/create-or-update-comment@v4
        with:
          comment-id: ${{ steps.find_comment.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          edit-mode: replace
          body: |
            <!-- zindex-bot:pr-architecture-diff -->
            ${{ steps.diff.outputs.total == '0' && format('### No architectural changes detected
            This PR does not add, remove, or rewire any service, database, queue, or external dependency relative to `main` (revision {0}).
            ', steps.main_rev.outputs.rev) || format('### Architecture diff · main {0} → pr-{1}

            | | |
            |---|---|
            | Architectural changes | +{2} / -{3} / ~{4} |
            | Main revision | `{0}` |
            | PR revision | `{5}` |

            <details><summary>Download before / after / diff (SVGs + JSON)</summary>

            {6}

            </details>
            ', steps.main_rev.outputs.rev, github.event.pull_request.number, steps.diff.outputs.added, steps.diff.outputs.removed, steps.diff.outputs.modified, steps.pr_apply.outputs.rev, steps.upload.outputs.artifact-url) }}

            <sub>Rendered by Zindex · scene `${{ secrets.ZINDEX_SCENE_ID }}`</sub>

PR comment template

The bot posts this comment on every triggering PR. The hidden marker <!-- zindex-bot:pr-architecture-diff --> lets peter-evans/create-or-update-comment find and overwrite the previous comment instead of appending a new one.

<!-- zindex-bot:pr-architecture-diff -->
### Architecture diff · main ${MAIN_REVISION} → pr-${PR_NUMBER}

| | |
|---|---|
| Architectural changes | +${ADDED} / -${REMOVED} / ~${MODIFIED} |
| Main revision | `${MAIN_REVISION}` |
| PR revision | `${PR_REVISION}` |

<details><summary>Download before / after / diff (SVGs + JSON)</summary>

${ARTIFACT_URL}

</details>

<sub>Rendered by Zindex · scene `${SCENE_ID}`</sub>

Agent resources

Machine-readable versions of this example. Agents should fetch these rather than scrape the rendered HTML.

  • pr-architecture-diff.scene.json Canonical DSP scene Open
  • pr-architecture-diff.ops.json Typed-operation envelope that builds the scene Open
  • pr-architecture-diff.workflow.json Structured agent workflow (goal, inputs, outputs, steps) Open
  • pr-architecture-diff.diff.json Sample dsp_diff_scene response (revision evolution) Open
  • pr-architecture-diff.github-actions.yml Runnable GitHub Actions workflow for the CI/CD recipe Open
  • pr-architecture-diff.svg Build-time rendered diagram Open
  • pr-architecture-diff.md Agent-readable markdown summary Open
  • /examples/index.json Manifest of all examples (cross-linked) Open