ProvenPin Install

dequal deep compare with object-hash digests: working versions (tested 2026-10-16)

Deep-compare nested objects, arrays, Maps, Sets and Dates with dequal, then compute stable object-hash digests using unorderedArrays and excludeKeys, fully offline.

passed6 passed · 0 failed2026-10-16 20:33 UTC
Status
passed
Last passed
2026-10-16 20:33 UTC
Tests
6 / 6
Language
TypeScript
Runtime
node v22.23.3
pinned versions4
dequal@2.0.3object-hash@3.0.0@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false
npm test
test environment
node v22.23.3
linux 6.17.0-1022-azure
x64
runner: github-actions 38962988948

The code

Exactly the files that passed. Your agent can pull them with get_recipe("dequal-object-hash-compare") (install), or see them on GitHub.

package.json
{
  "name": "dequal-object-hash-compare",
  "private": true,
  "type": "module",
  "scripts": {
    "test": "node --import tsx --test test/*.test.ts"
  },
  "dependencies": {
    "dequal": "2.0.3",
    "object-hash": "3.0.0"
  },
  "devDependencies": {
    "@types/node": "26.6.5",
    "tsx": "4.23.15"
  }
}
src/compare.ts
import { dequal } from "dequal";
import hash from "object-hash";
import type { HashOptions } from "object-hash";

export function deepEqual(left: unknown, right: unknown): boolean {
  return dequal(left, right);
}

export function stableHash(value: unknown, options: HashOptions = {}): string {
  return hash(value, {
    algorithm: "sha1",
    encoding: "hex",
    unorderedObjects: true,
    unorderedSets: true,
    ...options,
  });
}
src/object-hash.d.ts
declare module "object-hash" {
  export interface HashOptions {
    algorithm?: string;
    encoding?: "hex" | "buffer" | "base64" | "latin1";
    excludeValues?: boolean;
    ignoreUnknown?: boolean;
    respectFunctionProperties?: boolean;
    respectFunctionNames?: boolean;
    respectType?: boolean;
    replacer?: (value: unknown) => unknown;
    unorderedArrays?: boolean;
    unorderedSets?: boolean;
    unorderedObjects?: boolean;
    excludeKeys?: (key: string) => boolean;
  }

  export default function hash(value: unknown, options?: HashOptions): string;
}
test/compare.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { deepEqual, stableHash } from "../src/compare.ts";

test("dequal deep-compares nested objects, arrays, Maps, Sets and Dates", () => {
  const left = {
    id: "u1",
    tags: ["alpha", "beta"],
    profile: { active: true, scores: [1, 2, 3] },
    roles: new Set(["admin", "editor"]),
    limits: new Map([
      ["daily", 10],
      ["monthly", 100],
    ]),
    createdAt: new Date("2024-01-01T00:00:00.000Z"),
  };

  const right = {
    id: "u1",
    tags: ["alpha", "beta"],
    profile: { active: true, scores: [1, 2, 3] },
    roles: new Set(["editor", "admin"]),
    limits: new Map([
      ["monthly", 100],
      ["daily", 10],
    ]),
    createdAt: new Date("2024-01-01T00:00:00.000Z"),
  };

  assert.equal(deepEqual(left, right), true);
});

test("dequal reports inequality for different nested values", () => {
  assert.equal(
    deepEqual({ a: 1, nested: { list: [1, 2] } }, { a: 1, nested: { list: [1, 3] } }),
    false,
  );
  assert.equal(deepEqual(new Set([1, 2]), new Set([1, 3])), false);
  assert.equal(deepEqual(new Map([["a", 1]]), new Map([["a", 2]])), false);
  assert.equal(deepEqual(new Date("2024-01-01"), new Date("2024-01-02")), false);
});

test("stableHash matches equivalent objects regardless of key order", () => {
  const first = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 42 } } };
  const second = { nested: { deep: { value: 42 } }, user: { roles: ["admin"], id: "u1" } };
  assert.equal(stableHash(first), stableHash(second));
});

test("stableHash differs when a nested value changes", () => {
  const base = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 42 } } };
  const changed = { user: { id: "u1", roles: ["admin"] }, nested: { deep: { value: 43 } } };
  assert.notEqual(stableHash(base), stableHash(changed));
});

test("unorderedArrays makes array hashes order independent", () => {
  const first = { ids: [3, 1, 2], name: "x" };
  const second = { ids: [1, 2, 3], name: "x" };
  assert.equal(
    stableHash(first, { unorderedArrays: true }),
    stableHash(second, { unorderedArrays: true }),
  );
  assert.notEqual(stableHash(first), stableHash(second));
});

test("excludeKeys ignores volatile fields", () => {
  const before = { id: "u1", name: "Ada", updatedAt: "2024-01-01T00:00:00.000Z" };
  const after = { id: "u1", name: "Ada", updatedAt: "2024-06-01T00:00:00.000Z" };
  const excludeKeys = (key: string) => key === "updatedAt";
  assert.equal(stableHash(before, { excludeKeys }), stableHash(after, { excludeKeys }));
  assert.notEqual(stableHash(before), stableHash(after));
});

Connect to ProvenPin

Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.

terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp

Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.