Semantic Diff
The semantic diff compares two versions of an Effect program at the IR level, not the text level. It reports structural changes (steps added, removed, moved, or renamed) giving you a meaningful view of what changed in the program’s behavior.
This is especially useful when reviewing AI-generated code where the text diff can be large and hard to follow, but the structural change may be quite targeted.
Worked Example: Reviewing an AI Agent’s PR
Section titled “Worked Example: Reviewing an AI Agent’s PR”An AI coding agent was asked to “make the payment flow production-ready.” The resulting PR touches 40+ lines across services, error types, and the main workflow. A text diff is noisy. The semantic diff tells you exactly what changed structurally.
The before: a working MVP
Section titled “The before: a working MVP”// Services: TransferValidation, ExchangeRates, Accounts, Transfers
export const sendMoney = (input: TransferRequest) => Effect.gen(function* () { const validation = yield* TransferValidation; const rates = yield* ExchangeRates; const accounts = yield* Accounts; const transfers = yield* Transfers;
const validated = yield* validation.validate(input); const rate = yield* rates.getRate(validated.fromCurrency, validated.toCurrency); const balance = yield* accounts.getBalance(validated.senderId);
if (balance < validated.amount) { return yield* Effect.fail( new InsufficientFundsError(balance, validated.amount), ); }
const convertedAmount = Math.round(validated.amount * rate * 100) / 100;
const transfer = yield* transfers.execute({ recipientIban: validated.recipientIban, amount: convertedAmount, currency: validated.toCurrency, });
return { transferId: transfer.transferId, convertedAmount, rate }; });Railway diagram (before):
flowchart LR
A[validation <- TransferValidation] -->|ok| B[rates <- ExchangeRates]
B -->|ok| C[accounts <- Accounts]
C -->|ok| D[transfers <- Transfers]
D -->|ok| E[rate <- rates.getRate]
E -->|ok| F[balance <- accounts.getBalance]
F -->|ok| G{balance < amount?}
G -->|ok| H[transfers.execute]
H -->|ok| Done((Success))
E -->|err| EE([RateUnavailable])
G -->|err| GE([InsufficientFunds])
H -->|err| HE([TransferRejected])
The after: AI-generated “production-ready” version
Section titled “The after: AI-generated “production-ready” version”The AI agent added:
- FraudScreening service, screens every transfer before execution
- Notifications service, sends a confirmation after success
- Retry with exponential backoff on
transfers.execute - New error types:
FraudRiskError,ConfirmationFailedError
export const sendMoney = (input: TransferRequest) => Effect.gen(function* () { const validation = yield* TransferValidation; const rates = yield* ExchangeRates; const accounts = yield* Accounts; const fraud = yield* FraudScreening; // NEW const transfers = yield* Transfers; const notifications = yield* Notifications; // NEW
const validated = yield* validation.validate(input); yield* fraud.screen(validated); // NEW const rate = yield* rates.getRate(validated.fromCurrency, validated.toCurrency); const balance = yield* accounts.getBalance(validated.senderId);
if (balance < validated.amount) { return yield* Effect.fail( new InsufficientFundsError(balance, validated.amount), ); }
const convertedAmount = Math.round(validated.amount * rate * 100) / 100;
const transfer = yield* transfers.execute({ // CHANGED: now has retry recipientIban: validated.recipientIban, amount: convertedAmount, currency: validated.toCurrency, }).pipe( Effect.retry( Schedule.exponential('200 millis').pipe( Schedule.intersect(Schedule.recurs(2)), ), ), );
yield* notifications.sendConfirmation({ // NEW transferId: transfer.transferId, amount: convertedAmount, currency: validated.toCurrency, });
return { transferId: transfer.transferId, convertedAmount, rate }; });Railway diagram (after):
flowchart LR
A[validation <- TransferValidation] -->|ok| B[rates <- ExchangeRates]
B -->|ok| C[accounts <- Accounts]
C -->|ok| D[fraud <- FraudScreening]
D -->|ok| E[transfers <- Transfers]
E -->|ok| F[notifications <- Notifications]
F -->|ok| G[fraud.screen]
G -->|ok| H[rate <- rates.getRate]
H -->|ok| I[balance <- accounts.getBalance]
I -->|ok| J{balance < amount?}
J -->|ok| K["transfers.execute + retry"]
K -->|ok| L[notifications.sendConfirmation]
L -->|ok| Done((Success))
G -->|err| GE([FraudRisk])
H -->|err| HE([RateUnavailable])
J -->|err| JE([InsufficientFunds])
K -->|err| KE([TransferRejected])
L -->|err| LE([ConfirmationFailed])
Running the diff
Section titled “Running the diff”The two fixture files live in the repo. Run the diff with:
npx effect-analyze \ src/__fixtures__/docs/send-money-before.ts \ src/__fixtures__/docs/send-money-after.ts \ --diffDiff output (markdown)
Section titled “Diff output (markdown)”# Effect Program Diff: sendMoney → sendMoney
## Summary
| Metric | Count ||--------|-------|| Added | 8 || Removed | 1 || Renamed | 0 || Moved | 0 || Unchanged | 7 || Structural changes | 1 |
## Step Changes
- transfers.execute (removed)+ FraudScreening (added)+ Notifications (added)+ fraud.screen (added)+ transfers.execute(...).pipe (added)+ notifications.sendConfirmation (added)+ Effect.retry (added)+ Schedule.exponential (added)
## Structural Changes
- + retry block added
## Added program: FraudScreening## Added program: NotificationsWhat a reviewer learns in 10 seconds
Section titled “What a reviewer learns in 10 seconds”- 7 unchanged steps: the core flow (validate, get rate, check balance, fail path) is untouched
- 1 removed step (
transfers.execute): replaced by the.pipe(Effect.retry(...))variant - 2 new service dependencies:
FraudScreeningandNotificationsare new blast radius - 1 structural change: a
retryblock was added (exponential backoff on the transfer) - New error surface: the railway diagram shows
FraudRiskandConfirmationFailedas new error branches
Basic Usage
Section titled “Basic Usage”Compare the current file with the last commit:
npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diffSingle-File Shorthand
Section titled “Single-File Shorthand”With a single argument and --diff, the analyzer compares HEAD against the working copy:
npx effect-analyze src/transfer.ts --diffThis is equivalent to HEAD:src/transfer.ts src/transfer.ts --diff.
Git Ref Syntax
Section titled “Git Ref Syntax”Use any git ref with the ref:path syntax:
# Compare two branchesnpx effect-analyze main:src/transfer.ts feature:src/transfer.ts --diff
# Compare specific commitsnpx effect-analyze abc123:src/transfer.ts def456:src/transfer.ts --diff
# Compare a tag with the working copynpx effect-analyze v1.0.0:src/transfer.ts src/transfer.ts --diffWhat the Diff Detects
Section titled “What the Diff Detects”The diff engine classifies every step in both versions:
| Change Type | Description | Example |
|---|---|---|
| Unchanged | Step exists in both versions with the same structure | validation.validate stayed the same |
| Added | Step exists only in the new version | fraud.screen is new |
| Removed | Step exists only in the old version | transfers.execute was replaced |
| Moved | Step exists in both but in a different structural container | A step moved from sequential into Effect.all |
| Renamed | Same callee but different binding name | const result → const transfer |
Beyond individual steps, the diff also reports:
- Structural changes: new or removed
retry,parallel,race,error-handlerblocks - Added programs: entirely new Effect programs in the file
- Removed programs: Effect programs that no longer exist
Output Formats
Section titled “Output Formats”npx effect-analyze before.ts after.ts --diffProduces a structured report with a summary table, step changes as a diff block, and structural changes. Best for human review and pasting into PR comments.
npx effect-analyze before.ts after.ts --diff --format jsonMachine-readable diff object for CI integration. Each step has a kind field (added, removed, unchanged, moved, renamed) and metadata like callee, containerBefore, containerAfter.
{ "beforeName": "sendMoney", "afterName": "sendMoney", "steps": [ { "kind": "unchanged", "callee": "TransferValidation" }, { "kind": "unchanged", "callee": "ExchangeRates" }, { "kind": "unchanged", "callee": "Accounts" }, { "kind": "added", "callee": "FraudScreening" }, { "kind": "removed", "callee": "transfers.execute" }, { "kind": "added", "callee": "fraud.screen" }, { "kind": "added", "callee": "notifications.sendConfirmation" } ], "structuralChanges": [ { "kind": "added", "nodeType": "retry" } ], "summary": { "added": 8, "removed": 1, "unchanged": 7, "structuralChanges": 1, "hasRegressions": false }}npx effect-analyze before.ts after.ts --diff --format mermaidRenders the “after” program as a flowchart with color-coded nodes: green for added, yellow for moved, blue for renamed, and red dashed for removed steps (with --show-removed).
Regression Mode
Section titled “Regression Mode”Flag removed steps as regressions. Useful in CI to catch when an AI agent accidentally deletes error handling or removes a service call:
npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diff --regressionIn regression mode, the summary includes a hasRegressions flag set to true if any step or structural block was removed. Combine with --format json to parse in CI:
# Fail the build if the AI removed any stepsDIFF=$(npx effect-analyze HEAD:src/transfer.ts src/transfer.ts --diff --regression --format json)if echo "$DIFF" | jq -e '.[0].summary.hasRegressions' > /dev/null 2>&1; then echo "REGRESSION: Steps were removed from the Effect program" exit 1fiProgrammatic Usage
Section titled “Programmatic Usage”import { analyze, diffPrograms, renderDiffMarkdown } from "effect-analyzer"import { Effect } from "effect"
const before = await Effect.runPromise(analyze("./old-version.ts").single())const after = await Effect.runPromise(analyze("./new-version.ts").single())
const diff = diffPrograms(before, after)const report = renderDiffMarkdown(diff)
console.log(report)Additional rendering functions:
import { renderDiffJSON, renderDiffMermaid } from "effect-analyzer"
// JSON output for CIconst json = renderDiffJSON(diff)
// Mermaid diagram with change highlightsconst mermaid = renderDiffMermaid(diff)Related
Section titled “Related”- Coverage Audit - project-wide analysis
- CLI Reference -
--diffand--regressionflags - Complexity Metrics - track complexity changes over time
- Interactive HTML Viewer - explore the before/after programs visually