detect_drift
Compares current workflow fingerprints to prior runs, detecting silent rewrites, surface changes, and oscillating refactors to catch behavioral regressions after PR merges.
Instructions
Compares the current workflow fingerprints (SHA-256 hash of sorted members + edges + signals per workflow) against fingerprints from prior Veris runs persisted in .veris/state.db. Surfaces three classes of drift: silent rewrites (member set identical but internal topology changed — most dangerous because nobody's watching), surface expansion/contraction (members added/removed), and oscillating refactors (same workflow flips back and forth across runs — usually a sign of unresolved indecision). Run after every PR merge to catch behavioral regressions before users do.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
No arguments | |||
Implementation Reference
- src/mcp/McpServer.ts:294-302 (handler)Handler function that executes the detect_drift tool. It computes workflow fingerprints via WorkflowFingerprintEngine, then passes them to DriftDetector.detect() which compares against historical fingerprints in VerisState. Returns a DriftReport with summary and per-workflow drift details.
private handleDetectDrift() { const wf = this.ensureWorkflows(); const graph = this.ensureGraph(); const fps = new WorkflowFingerprintEngine().fingerprintAll(wf.workflows, graph); this.lastFingerprints = fps; if (!this.lastRunId) this.lastRunId = this.state.newRunId(); const drift = new DriftDetector().detect(this.lastRunId, fps, this.state); return this.text(drift); } - src/mcp/McpServer.ts:70-71 (registration)Registration of the detect_drift tool in the CallToolRequestSchema switch-case, routing to handleDetectDrift().
case "detect_drift": return this.handleDetectDrift(); case "generate_adversarial_probes": return this.handleGenerateProbes(); - src/mcp/McpServer.ts:108-110 (schema)Tool definition with name, description, and input schema for detect_drift. No input parameters required.
{ name: "detect_drift", description: "Compares the current workflow fingerprints (SHA-256 hash of sorted members + edges + signals per workflow) against fingerprints from prior Veris runs persisted in .veris/state.db. Surfaces three classes of drift: silent rewrites (member set identical but internal topology changed — most dangerous because nobody's watching), surface expansion/contraction (members added/removed), and oscillating refactors (same workflow flips back and forth across runs — usually a sign of unresolved indecision). Run after every PR merge to catch behavioral regressions before users do.", inputSchema: { type: "object", properties: {}, required: [] } }, - WorkflowFingerprintEngine computes a SHA-256 fingerprint for each workflow based on sorted member node IDs and internal edge signatures. Used by handleDetectDrift to generate current fingerprints.
export class WorkflowFingerprintEngine { public fingerprint(domain: WorkflowDomain, graph: BehavioralGraph): WorkflowFingerprint { const memberSet = new Set(domain.memberNodeIds); const members = [...domain.memberNodeIds].sort(); const internalEdges = graph.getEdges() .filter(e => memberSet.has(e.sourceId) && memberSet.has(e.targetId)) .map(e => `${e.sourceId}>${e.targetId}:${e.type}`) .sort(); const h = crypto.createHash('sha256'); h.update(domain.kind); for (const id of members) h.update('\n' + id); h.update('\n---\n'); for (const sig of internalEdges) h.update('\n' + sig); return { workflowId: domain.id, workflowName: domain.name, fingerprint: h.digest('hex'), memberCount: members.length }; } public fingerprintAll(domains: WorkflowDomain[], graph: BehavioralGraph): WorkflowFingerprint[] { return domains.map(d => this.fingerprint(d, graph)); } } - src/engine/DriftDetector.ts:33-97 (helper)DriftDetector compares current workflow fingerprints against historical fingerprints from VerisState to detect silent rewrites, surface expansion/contraction, and oscillation. Produces a DriftReport with per-workflow narratives and a summary.
export class DriftDetector { public detect( runId: string, current: WorkflowFingerprint[], state: VerisState | null ): DriftReport { const out: WorkflowDriftReport[] = []; let driftedCount = 0; let silentRewriteCount = 0; for (const cur of current) { const history: FingerprintRecord[] = state && state.enabled ? state.fingerprintHistory(cur.workflowId, 20) : []; // history is newest-first per VerisState const previous = history[0] || null; const previousFp = previous ? previous.fingerprint : null; const distinct = new Set(history.map(h => h.fingerprint)); distinct.add(cur.fingerprint); const changed = previousFp !== null && previousFp !== cur.fingerprint; const memberChange = previous ? cur.memberCount - previous.memberCount : 0; const oscillation = history.length >= 3 && history[0].fingerprint !== history[1].fingerprint && history[1].fingerprint !== history[2].fingerprint && history[0].fingerprint === history[2].fingerprint; let narrative: string; if (!previousFp) { narrative = `${cur.workflowName}: first observation. ${cur.memberCount} members.`; } else if (!changed) { narrative = `${cur.workflowName}: stable since last run (${cur.memberCount} members, fp unchanged).`; } else if (memberChange === 0) { silentRewriteCount++; narrative = `${cur.workflowName}: silent rewrite — same members, different internal topology. Inspect for unannounced refactors.`; } else if (memberChange > 0) { narrative = `${cur.workflowName}: surface expanded by ${memberChange} member${memberChange === 1 ? '' : 's'} (now ${cur.memberCount}). Verify scope creep.`; } else { narrative = `${cur.workflowName}: surface contracted by ${Math.abs(memberChange)} member${memberChange === -1 ? '' : 's'} (now ${cur.memberCount}). Verify regression / extraction is intentional.`; } if (oscillation) { narrative += ` Oscillating fingerprint across last runs — likely refactor instability.`; } if (changed) driftedCount++; out.push({ workflowId: cur.workflowId, workflowName: cur.workflowName, currentFingerprint: cur.fingerprint, previousFingerprint: previousFp, changedSinceLastRun: changed, distinctFingerprintsObserved: distinct.size, memberCountTrend: [cur.memberCount, ...history.map(h => h.memberCount).slice(0, 9)], oscillationDetected: oscillation, memberChange, narrative }); } const summary = driftedCount === 0 ? 'No workflow drift detected vs prior runs.' : `${driftedCount} workflow${driftedCount === 1 ? '' : 's'} drifted since last run` + (silentRewriteCount > 0 ? `; ${silentRewriteCount} appear to be silent rewrites.` : '.'); return { runId, workflows: out, summary }; } }