Skip to main content
// Props
import { DriftDetector } from "smithers-orchestrator";

type DriftDetectorProps = {
  id?: string; // default "drift"; ids {id}-capture, {id}-compare
  captureAgent: AgentLike;
  compareAgent: AgentLike;
  captureOutput: OutputTarget;
  compareOutput: OutputTarget; // include `drifted: boolean`
  baseline: unknown;
  alertIf?: (comparison: any) => boolean; // default: comparison.drifted === true
  alert?: ReactElement;
  poll?: { intervalMs?: number; maxPolls?: number }; // default maxPolls = 100; intervalMs is reserved, not yet passed to the Loop
  skipIf?: boolean;
};
<Workflow name="api-drift-check">
  <DriftDetector
    captureAgent={snapshotAgent}
    compareAgent={diffAgent}
    captureOutput={outputs.capture}
    compareOutput={outputs.compare}
    baseline={{ endpoints: ["/users", "/orders"], schemaHash: "abc123" }}
    alert={
      <Task id="notify" output={outputs.notify} agent={slackAgent}>
        API drift detected. Notify the team.
      </Task>
    }
  />
</Workflow>

Notes

  • Without poll, the component runs once; with poll, it wraps in a Loop.
  • When comparison output exists, alertIf decides whether to render alert; without alertIf, comparison.drifted === true is the trigger.
  • Without alert, the component compares but takes no action on drift.

Source

The <DriftDetector> implementation and the files it imports, straight from the package source. This section is generated; edit the source, not this block.
import React from "react";
import { SmithersContext } from "@smithers-orchestrator/react-reconciler/context";
import { Task } from "./Task.js";
import { Sequence } from "./Sequence.js";
import { Branch } from "./Branch.js";
import { Loop } from "./Ralph.js";
/** @typedef {import("./DriftDetectorProps.ts").DriftDetectorProps} DriftDetectorProps */

/**
 * @param {unknown} value
 * @returns {value is Record<string, unknown>}
 */
function isRecord(value) {
    return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}

/**
 * @param {unknown} comparison
 * @param {((comparison: unknown) => boolean) | undefined} alertIf
 * @returns {boolean}
 */
function shouldAlert(comparison, alertIf) {
    if (comparison == null) {
        return false;
    }
    if (alertIf) {
        return Boolean(alertIf(comparison));
    }
    return isRecord(comparison) && comparison.drifted === true;
}

/**
 * @param {DriftDetectorProps} props
 */
export function DriftDetector(props) {
    if (props.skipIf)
        return null;
    const prefix = props.id ?? "drift";
    const ctx = React.useContext(SmithersContext);
    const comparison = ctx?.outputMaybe(props.compareOutput, { nodeId: `${prefix}-compare` });
    const drifted = shouldAlert(comparison, props.alertIf);
    const captureTask = React.createElement(Task, {
        id: `${prefix}-capture`,
        output: props.captureOutput,
        agent: props.captureAgent,
        children: `Capture the current state for drift detection. Baseline reference: ${typeof props.baseline === "string"
            ? props.baseline
            : JSON.stringify(props.baseline)}`,
    });
    const compareTask = React.createElement(Task, {
        id: `${prefix}-compare`,
        output: props.compareOutput,
        agent: props.compareAgent,
        dependsOn: [`${prefix}-capture`],
        children: `Compare the captured current state against the baseline and determine if meaningful drift has occurred. Include a "drifted" boolean and "significance" string in your response. Baseline: ${typeof props.baseline === "string"
            ? props.baseline
            : JSON.stringify(props.baseline)}`,
    });
    const alertBranch = props.alert
        ? React.createElement(Branch, {
            if: drifted,
            then: props.alert,
        })
        : null;
    const sequenceChildren = [captureTask, compareTask];
    if (alertBranch)
        sequenceChildren.push(alertBranch);
    const sequence = React.createElement(Sequence, null, ...sequenceChildren);
    if (props.poll) {
        return React.createElement(Loop, {
            id: `${prefix}-poll`,
            until: false,
            maxIterations: props.poll.maxPolls ?? 100,
            onMaxReached: "return-last",
        }, sequence);
    }
    return sequence;
}