Tutorial: Build a custom analyzer
Goal
Add a new deterministic analyzer to Refract — one that produces a new event type and integrates into the pipeline. This tutorial walks through the full cycle: define the event type, implement the analyzer, integrate with the CLI, write tests, add an eval benchmark, and verify the build gate.
Prerequisites
- Bun 1.2+
- Clone of the refract monorepo (
git clone https://github.com/refract-org/refract) - Passing build gate before you start:
bun run ci
Step 1: Define the event type
All event types live in packages/evidence-graph/src/schemas/evidence.ts. Add your
new type to the EventType union. This example adds a "redirect detected" event:
export type EventType =
// ... existing types ...
| "redirect_detected"; // ← add here
Keep the union alphabetically grouped within its category. Bump the
EVENT_SCHEMA_VERSION minor version:
export const EVENT_SCHEMA_VERSION = "0.5.0";
Build to verify the type propagates:
cd packages/evidence-graph && bun run build
Step 2: Implement the analyzer
Create packages/analyzers/src/redirect-detector.ts:
import type { EvidenceEvent, Revision } from "@refract-org/evidence-graph";
export function detectRedirects(revisions: Revision[]): EvidenceEvent[] {
const events: EvidenceEvent[] = [];
for (let i = 1; i < revisions.length; i++) {
const prev = revisions[i - 1];
const curr = revisions[i];
// A redirect is wikitext starting with #REDIRECT
const wasRedirect = prev.content.trimStart().startsWith("#REDIRECT");
const becameRedirect = curr.content.trimStart().startsWith("#REDIRECT");
if (becameRedirect && !wasRedirect) {
events.push({
eventType: "redirect_detected",
fromRevisionId: prev.revId,
toRevisionId: curr.revId,
section: "",
before: prev.content.slice(0, 200),
after: curr.content.slice(0, 200),
deterministicFacts: [{
fact: "redirect_detected",
detail: `page converted to redirect in revision ${curr.revId}`,
provenance: {
analyzer: "redirect-detector",
version: "0.5.0",
inputHashes: [],
},
}],
layer: "observed",
timestamp: curr.timestamp,
});
}
}
return events;
}
Add the export to packages/analyzers/src/index.ts:
export { detectRedirects } from "./redirect-detector.js";
Step 3: Integrate with the CLI
In packages/cli/src/commands/analyze.ts, add the analyzer to the forensic depth
pipeline (or create a new depth level if appropriate):
import { detectRedirects } from "@refract-org/analyzers";
// In the forensic depth block:
if (depth === "forensic") {
const redirectEvents = detectRedirects(revisions);
events.push(...redirectEvents);
}
Step 4: Write tests
Create packages/analyzers/src/__tests__/redirect-detector.test.ts:
import { describe, expect, it } from "vitest";
import type { Revision } from "@refract-org/evidence-graph";
import { detectRedirects } from "../redirect-detector.js";
function makeRev(revId: number, content: string, timestamp = "2026-01-01T00:00:00Z"): Revision {
return {
revId, content, comment: "", timestamp,
pageId: 1, pageTitle: "Test", user: undefined,
};
}
describe("detectRedirects", () => {
it("detects when a page becomes a redirect", () => {
const revs = [
makeRev(1, "Normal article text"),
makeRev(2, "#REDIRECT [[Target page]]"),
];
const events = detectRedirects(revs);
expect(events).toHaveLength(1);
expect(events[0].eventType).toBe("redirect_detected");
});
it("does not fire when page stays a redirect", () => {
const revs = [
makeRev(1, "#REDIRECT [[Old target]]"),
makeRev(2, "#REDIRECT [[New target]]"),
];
const events = detectRedirects(revs);
expect(events).toHaveLength(0);
});
it("does not fire when page stays normal", () => {
const revs = [
makeRev(1, "Normal text"),
makeRev(2, "Updated normal text"),
];
expect(detectRedirects(revs)).toHaveLength(0);
});
});
Run the tests:
bun run test
Step 5: Add an eval benchmark
In packages/eval/src/index.ts, the benchmarkPages() method returns test cases.
Add one for your analyzer:
{
id: "redirect-detection",
description: "Pages that were converted to redirects produce redirect_detected events",
pageTitle: "Wikipedia:Redirect", // a known redirect
pageId: 12345,
revisionRange: { from: 0, to: 0 },
expectedEvents: [{ eventType: "redirect_detected", section: "" }],
tolerance: { minEventCount: 1, minPrecision: 0.0 },
},
Run the eval:
refract eval --page "Wikipedia:Redirect"
Step 6: Verify the build gate
bun run ci
This runs: build && lint && typecheck && check:boundaries && test. All must pass.
If your analyzer introduces exports that cross package boundaries incorrectly,
check:boundaries will catch it.
Step 7: Update documentation
- Add the new event type to
refract-docs/docs/schema.md - Add it to
refract-docs/docs/events.mdevent taxonomy - If the CLI flag changed, update
refract-docs/docs/cli.md
Conventions checklist
- File: kebab-case (
redirect-detector.ts) - Function: camelCase (
detectRedirects) - Imports:
import typefor types,.jsextension for intra-package - Test file:
src/__tests__/redirect-detector.test.ts - Exports: only the public API from
index.ts - Event types: snake_case (
redirect_detected) - No model calls in the analyzer
- No comments explaining what code does (only non-obvious constraints)
- Commit:
feat(analyzers): add redirect detector
Next steps
- SDK reference — all packages and their APIs
- Architecture decisions — why deterministic-only
- Workqueue protocol — how tasks are structured