This website is meant to be read and understood quickly by humans, but is only fully parsable, on a technical level, with the aid of an AI system. Read why →
Loop MMT
Same Records, Minus The Repeatsfilter← all gifts

Dedup-filter

Point it at a JSONL stream and it passes records through, dropping every one it has already seen — the first occurrence of each is kept, later duplicates are dropped, and the output is a stable subset of the input in the original order. By default a duplicate is a record with the same CANONICAL form: object key-order is ignored ({"a":1,"b":2} == {"b":2,"a":1}) but array order is kept ([1,2] != [2,1]), so 'the same record written two ways' deduplicates while the emitted line stays byte-for-byte verbatim. Pass --key FIELD to dedup by one top-level field instead of the whole record; a record lacking that field passes through and never dedups. It drops straight into a pipe between a source that repeats and a stage that must not see repeats — a CI gate, a loader, a diff.

The honest edge
Drops EXACT duplicates (by canonical record, or by one field). NOT fuzzy/near-dedup — no similarity, no value normalization. It does NOT merge the records it drops (keeps the first verbatim, discards the rest), and it keeps FIRST not last — it is not a 'latest wins' upsert.
Run it
printf '%s\n' '{"id":1}' '{"id":1}' '{"id":2}' | node dedup-filter.js # --key FIELD | --count; exit 0 ok | 2 input error test_dedup-filter.js (20/20: independent recursive deep-equal keep-first oracle + frozen hand goldens, canonical record identity (object key-order ignored / array order kept), --key first-wins + missing-key passthrough + object-valued keys, verbatim-subset preservation, determinism across runs, input-honesty hard errors, last-wins mutation bite) + out-of-band conformance conform_dedup-filter.cjs (25/25 GREEN, subprocess-driven, signed) Zero dependencies, Node or browser, deterministic
The code — every file that ships
dedup-filter.js234 lineson GitHub →
#!/usr/bin/env node
/* dedup-filter.js — drop duplicate records from a JSONL stream, first one wins.
   Dependency-free, deterministic, one pass. Runs in Node or a browser. MIT.

   WHAT IT IS. Give it a stream of records — one JSON value per line (JSONL) — and
   it passes them through, dropping every record it has already seen. The output is
   a SUBSET of the input in the ORIGINAL ORDER: the first occurrence of each record
   is kept, every later duplicate is dropped. Same stream in, byte-identical stream
   out, on every machine and every run. It is a FILTER: output ⊆ input, nothing is
   added, reordered, or rewritten — the kept lines are emitted exactly as they
   arrived.

   WHAT COUNTS AS A DUPLICATE (the whole determinism story). Two modes, and the
   identity key is the only thing that differs:

     - DEFAULT (whole-record identity). A record's key is its CANONICAL JSON form:
       the value re-serialized with object keys sorted, recursively. So
       {"a":1,"b":2} and {"b":2,"a":1} are the SAME record (key-order in an object
       is not meaningful) and the second is dropped — but [1,2] and [2,1] are
       DIFFERENT (array order IS meaningful). Canonicalizing the key, not the line,
       is what makes "same record written two ways" deduplicate while still
       emitting the original line untouched.

     - --key FIELD (dedup by one field). The record's key is the value of the named
       top-level field, canonicalized the same way. The FIRST record carrying a
       given field value is kept; later records with that same field value are
       dropped even if the rest of the record differs. A record that LACKS the
       field is passed through unchanged and never dedups against anything (a
       missing key is not a value — it is not "the same" as another missing key).

   FIRST WINS, ORDER STABLE. The kept record for any key is always the FIRST one
   seen; the relative order of the kept records is exactly their input order. This
   is a deliberate, pinned choice (not "last wins", not "sorted"): it makes the
   fold one-pass and the output a stable, re-derivable subset of the input.

   INPUT HONESTY (the character of this gift). A filter is only trustworthy if it
   refuses to quietly mishandle a line:
     - Every non-blank line must be valid JSON (any JSON value: object, array,
       string, number, bool, null). A line that is not valid JSON is a HARD ERROR
       (exit 2) naming the line — never a silent skip and never passed through as
       raw text.
     - Blank lines are skipped (not emitted, not counted as records). A trailing
       \r (CRLF files) is trimmed before parsing; the emitted line preserves the
       original body without the trailing \r.
     - --key names a TOP-LEVEL field only (no dotted paths); it is meaningful only
       for records that are JSON objects. A --key applied to a non-object record
       (a bare number, string, array) means "no such field" -> that record is
       passed through and never dedups.

   USAGE
     printf '%s\n' '{"id":1}' '{"id":1}' '{"id":2}' | node dedup-filter.js
     node dedup-filter.js --key id events.jsonl        # dedup by the id field
     node dedup-filter.js --count < in.jsonl > out.jsonl   # report drops on stderr
     node dedup-filter.js --help

   Each non-blank line is one JSON record. Output is the kept records, one per line,
   each terminated by a newline, in input order.

   Exit codes: 0 success · 2 input error (missing file, a directory, an unknown
   option, or a line that is not valid JSON). Always a clean one-line message on
   stderr, never a stack trace. --count writes the drop tally to stderr; it never
   changes the exit code or the emitted stream.

   Released under MIT. Its edge is printed in the README: this drops EXACT
   duplicates (by canonical record, or by one field). It is NOT a fuzzy/near-dedup
   (no similarity, no normalization of values), does NOT collapse or merge the
   records it drops (it keeps the first verbatim and discards the rest), and keeps
   FIRST not last — it is not a "latest wins" upsert.
*/
"use strict";

// Canonical JSON: object keys sorted recursively, so key-order in objects does not
// make two equal records look different. Arrays keep their order (order IS data).
function canon(v) {
  if (v === null || typeof v !== "object") return JSON.stringify(v);
  if (Array.isArray(v)) {
    var parts = [];
    for (var i = 0; i < v.length; i++) parts.push(canon(v[i]));
    return "[" + parts.join(",") + "]";
  }
  var keys = Object.keys(v).sort();
  var out = [];
  for (var k = 0; k < keys.length; k++) {
    out.push(JSON.stringify(keys[k]) + ":" + canon(v[keys[k]]));
  }
  return "{" + out.join(",") + "}";
}

// Parse one input line into a JSON value, or throw a clean, line-named Error.
function parseRecord(line, lineNo) {
  try { return JSON.parse(line); }
  catch (e) {
    throw new Error("line " + lineNo + " is not valid JSON: " + JSON.stringify(line.slice(0, 40)));
  }
}

// The public filter: JSONL text + {key} -> { lines: [kept lines], dropped: N }.
// First occurrence of each key wins; kept lines are the ORIGINAL bodies, in order.
// A sentinel object identity is used for "record has no --key field" so that such
// records never collide with each other or with a real value.
var NO_KEY = { noKey: true };

function filter(text, opts) {
  opts = opts || {};
  var keyField = opts.key; // undefined => whole-record identity

  var lines = String(text).split("\n");
  var seen = Object.create(null);
  var kept = [];
  var dropped = 0;
  var i, line, rec, keyVal, k;

  for (i = 0; i < lines.length; i++) {
    line = lines[i];
    if (line.charCodeAt(line.length - 1) === 0x0d) line = line.slice(0, -1); // trim \r
    if (line.length === 0) continue; // blank line is not a record

    rec = parseRecord(line, i + 1);

    if (keyField === undefined) {
      k = canon(rec); // whole-record identity
    } else {
      // dedup by one top-level field; a record lacking it never dedups
      if (rec !== null && typeof rec === "object" && !Array.isArray(rec) &&
          Object.prototype.hasOwnProperty.call(rec, keyField)) {
        keyVal = rec[keyField];
        k = "K:" + canon(keyVal);
      } else {
        k = NO_KEY; // object identity => unique per line, never a duplicate
      }
    }

    if (k !== NO_KEY && seen[k]) { dropped += 1; continue; }
    if (k !== NO_KEY) seen[k] = true;
    kept.push(line);
  }

  return { lines: kept, dropped: dropped };
}

/* ---- exports (browser + Node) ------------------------------------ */
if (typeof window !== "undefined") {
  window.ForestGifts = window.ForestGifts || {};
  window.ForestGifts.dedupFilter = filter;
  window.ForestGifts.dedupCanon = canon;
}
if (typeof module !== "undefined" && module.exports) {
  module.exports = { filter: filter, canon: canon };
}

/* ---- CLI (runs only when invoked directly, never on require) ------ */
function run(text, opts) {
  var r = filter(text, opts);
  var body = r.lines.length ? r.lines.join("\n") + "\n" : "";
  return { out: body, dropped: r.dropped };
}

function main(argv) {
  var args = argv.slice(2);
  if (args.indexOf("--help") !== -1 || args.indexOf("-h") !== -1) {
    process.stdout.write(
      "dedup-filter.js — drop duplicate records from a JSONL stream, first one wins.\n\n" +
      "  printf '%s\\n' '{\"id\":1}' '{\"id\":1}' | node dedup-filter.js\n" +
      "  node dedup-filter.js --key id events.jsonl       dedup by the id field\n" +
      "  node dedup-filter.js --count < in > out          report drops on stderr\n" +
      "  node dedup-filter.js --help\n\n" +
      "Each non-blank line is one JSON record. By DEFAULT a duplicate is a record\n" +
      "with the same CANONICAL form (object key-order ignored, array order kept);\n" +
      "--key FIELD dedups by one top-level field instead. FIRST occurrence wins and\n" +
      "output is a stable SUBSET of the input in original order.\n\n" +
      "Edge: this drops EXACT duplicates. It is NOT fuzzy/near-dedup, does NOT merge\n" +
      "the records it drops, and keeps FIRST not last. Invalid JSON is a hard error,\n" +
      "never a silent skip.\n"
    );
    return 0;
  }

  var opts = {};
  var files = [];
  var countMode = false;
  var i;
  try {
    for (i = 0; i < args.length; i++) {
      if (args[i] === "--key") {
        opts.key = args[++i];
        if (opts.key === undefined || opts.key === "" || opts.key.charAt(0) === "-") {
          throw new Error("--key requires a field name");
        }
      }
      else if (args[i] === "--count") { countMode = true; }
      else if (args[i].charAt(0) === "-") { throw new Error("unknown option " + args[i]); }
      else { files.push(args[i]); }
    }
  } catch (e) {
    process.stderr.write("dedup-filter: " + e.message + "\n");
    return 2;
  }

  function emit(text) {
    try {
      var r = run(text, opts);
      process.stdout.write(r.out);
      if (countMode) process.stderr.write("dedup-filter: dropped " + r.dropped + " duplicate(s)\n");
      return 0;
    } catch (e) {
      process.stderr.write("dedup-filter: " + e.message + "\n");
      return 2;
    }
  }

  if (files.length > 0) {
    var fs = require("fs");
    var text;
    try { text = fs.readFileSync(files[0], "utf8"); }
    catch (e) {
      process.stderr.write("dedup-filter: cannot read " + files[0] +
        " (" + (e.code === "EISDIR" ? "is a directory" : (e.code || "read error")) + ")\n");
      return 2;
    }
    return emit(text);
  }

  // stdin
  var chunks = [];
  process.stdin.on("data", function (d) { chunks.push(d); });
  process.stdin.on("end", function () {
    process.exitCode = emit(Buffer.concat(chunks).toString("utf8"));
  });
  return 0;
}

if (typeof require !== "undefined" && require.main === module) {
  process.exitCode = main(process.argv);
}
test_dedup-filter.js201 lineson GitHub →
#!/usr/bin/env node
/* test_dedup-filter.js — golden battery for the dedup-filter gift.

   Out-of-band and self-verifying. The oracle is TWO independent things, neither a
   copy of the gift's canonical-string hashmap:

     (1) A NAIVE O(n^2) DEEP-EQUAL reference — walk the records in order and keep a
         record only if no EARLIER kept record is structurally deep-equal to it
         (for the default mode) or shares its key field value (for --key mode).
         deepEqual is written independently here (recursive, key-set compare); it
         does NOT canonicalize to a string, so it is a genuinely different way to
         decide "same record" than the gift's canon()/hashmap. The two must agree
         on which lines survive.

     (2) FROZEN hand-picked golden outputs, pinned by hand from the spec — the
         object-key-order case (§2), array-order sensitivity (§3), --key
         first-wins (§4), and the missing-key passthrough (§5).

   A planted mutation (the bite, §9) MUST be caught — if the suite passes with the
   mutation live, the suite proves nothing.

   Run:  node test_dedup-filter.js   -> exit 0 GREEN / non-zero RED
*/
"use strict";
var df = require("./dedup-filter.js");

var pass = 0, fail = 0;
function ok(name, cond) {
  if (cond) { pass++; }
  else { fail++; console.log("  FAIL  " + name); }
}
function J(v) { return JSON.stringify(v); }
function keptLines(records, opts) { return df.filter(records.join("\n"), opts).lines; }

/* ---- independent recursive deep-equal (NOT canon-string) ------------------ */
function deepEqual(a, b) {
  if (a === b) return true;
  if (a === null || b === null) return a === b;
  if (typeof a !== "object" || typeof b !== "object") return a === b;
  var aArr = Array.isArray(a), bArr = Array.isArray(b);
  if (aArr !== bArr) return false;
  if (aArr) {
    if (a.length !== b.length) return false;
    for (var i = 0; i < a.length; i++) if (!deepEqual(a[i], b[i])) return false;
    return true;
  }
  var ak = Object.keys(a), bk = Object.keys(b);
  if (ak.length !== bk.length) return false;
  for (var j = 0; j < ak.length; j++) {
    if (!Object.prototype.hasOwnProperty.call(b, ak[j])) return false;
    if (!deepEqual(a[ak[j]], b[ak[j]])) return false;
  }
  return true;
}

// naive O(n^2) oracle: returns the indices of kept lines
function oracleKeptIndices(records, keyField) {
  var parsed = records.map(function (l) { return JSON.parse(l); });
  var keptIdx = [];
  for (var i = 0; i < parsed.length; i++) {
    var rec = parsed[i];
    var isDup = false;
    var hasKey = keyField !== undefined &&
      rec !== null && typeof rec === "object" && !Array.isArray(rec) &&
      Object.prototype.hasOwnProperty.call(rec, keyField);
    if (keyField !== undefined && !hasKey) {
      keptIdx.push(i); // missing key never dedups
      continue;
    }
    for (var p = 0; p < keptIdx.length; p++) {
      var prev = parsed[keptIdx[p]];
      if (keyField === undefined) {
        if (deepEqual(rec, prev)) { isDup = true; break; }
      } else {
        var prevHasKey = prev !== null && typeof prev === "object" && !Array.isArray(prev) &&
          Object.prototype.hasOwnProperty.call(prev, keyField);
        if (prevHasKey && deepEqual(rec[keyField], prev[keyField])) { isDup = true; break; }
      }
    }
    if (!isDup) keptIdx.push(i);
  }
  return keptIdx;
}

/* ---- 1. Default whole-record dedup vs the independent oracle ------------- */
(function () {
  var recs = ['{"id":1}', '{"id":2}', '{"id":1}', '{"id":3}', '{"id":2}'];
  var kept = keptLines(recs);
  var oracleIdx = oracleKeptIndices(recs, undefined);
  var oracleLines = oracleIdx.map(function (i) { return recs[i]; });
  ok("default: kept == independent deep-equal oracle", J(kept) === J(oracleLines));
  ok("default: frozen golden [id1,id2,id3]", J(kept) === J(['{"id":1}', '{"id":2}', '{"id":3}']));
  ok("default: output is a subset of input", kept.every(function (l) { return recs.indexOf(l) !== -1; }));
  ok("default: order stable (kept indices ascending)",
     J(oracleIdx) === J(oracleIdx.slice().sort(function (a, b) { return a - b; })));
})();

/* ---- 2. Object key-order is canonicalized (same record two ways) -------- */
(function () {
  var recs = ['{"a":1,"b":2}', '{"b":2,"a":1}', '{"a":1,"b":3}'];
  var kept = keptLines(recs);
  // first two are the same record; third differs -> keep #1 and #3
  ok("key-order: {a,b}=={b,a} deduped -> 2 kept", kept.length === 2);
  ok("key-order: keeps the FIRST verbatim + the differing one",
     J(kept) === J(['{"a":1,"b":2}', '{"a":1,"b":3}']));
  // oracle agreement (deep-equal also treats key-order as equal)
  var oracleLines = oracleKeptIndices(recs, undefined).map(function (i) { return recs[i]; });
  ok("key-order: matches deep-equal oracle", J(kept) === J(oracleLines));
})();

/* ---- 3. Array order IS meaningful (not deduped) ------------------------- */
(function () {
  var recs = ['[1,2,3]', '[3,2,1]', '[1,2,3]'];
  var kept = keptLines(recs);
  ok("array-order: [1,2,3] and [3,2,1] both kept, dup [1,2,3] dropped",
     J(kept) === J(['[1,2,3]', '[3,2,1]']));
})();

/* ---- 4. --key first-wins, rest of record ignored ------------------------ */
(function () {
  var recs = ['{"id":1,"v":"a"}', '{"id":1,"v":"b"}', '{"id":2,"v":"c"}', '{"id":1,"v":"d"}'];
  var kept = keptLines(recs, { key: "id" });
  ok("--key: first per id wins",
     J(kept) === J(['{"id":1,"v":"a"}', '{"id":2,"v":"c"}']));
  var oracleLines = oracleKeptIndices(recs, "id").map(function (i) { return recs[i]; });
  ok("--key: matches independent oracle", J(kept) === J(oracleLines));
})();

/* ---- 5. --key missing field: those records never dedup ------------------ */
(function () {
  var recs = ['{"id":1}', '{"other":9}', '{"other":9}', '{"id":1}'];
  var kept = keptLines(recs, { key: "id" });
  // both {other:9} lack id -> both pass; the two {id:1} dedup to one
  ok("--key missing: no-key records all pass, keyed ones dedup",
     J(kept) === J(['{"id":1}', '{"other":9}', '{"other":9}']));
})();

/* ---- 6. --key value canonicalization (object key values) ---------------- */
(function () {
  // key field is an object; {x:1,y:2} vs {y:2,x:1} are the SAME key value
  var recs = ['{"k":{"x":1,"y":2},"n":1}', '{"k":{"y":2,"x":1},"n":2}'];
  var kept = keptLines(recs, { key: "k" });
  ok("--key object value: canonicalized, 2nd dropped", kept.length === 1);
  ok("--key object value: keeps first", J(kept) === J(['{"k":{"x":1,"y":2},"n":1}']));
})();

/* ---- 7. Hygiene: blank lines skipped, CRLF trimmed, verbatim subset ----- */
(function () {
  var r = df.filter('{"id": 1,  "v": "x"}\n\n{"id":1}\r\n{"id":2}\n', { key: "id" });
  // blank skipped; {id:1} appears twice (dedup to first, WITH its spacing); {id:2} kept
  ok("hygiene: blank skipped, CRLF trimmed, first kept verbatim with spacing",
     J(r.lines) === J(['{"id": 1,  "v": "x"}', '{"id":2}']));
  ok("hygiene: dropped count == 1", r.dropped === 1);
})();

/* ---- 8. Determinism: same input -> byte-identical kept lines ------------ */
(function () {
  var recs = ['{"x":1}', '{"y":2}', '{"x":1}', '{"z":3}', '{"y":2}'];
  var a = J(keptLines(recs));
  var b = J(keptLines(recs));
  ok("determinism: byte-identical across runs", a === b);
})();

/* ---- 9. Input honesty: bad JSON throws ---------------------------------- */
(function () {
  function throws(text, opts) { try { df.filter(text, opts); return false; } catch (e) { return true; } }
  ok("honesty: invalid JSON line throws", throws('{"ok":1}\n{oops\n'));
  ok("honesty: a lone bare token that is not JSON throws", throws("undefined\n"));
})();

/* ---- 10. THE BITE — a planted mutation the suite MUST catch -------------- *
   A "last wins" mutant in --key mode: for records that share a key value it keeps
   the LAST body instead of the first (and emits at the last position). The gift
   keeps the FIRST body at the first position. On records that share a key but
   differ elsewhere, the two disagree — so §4's frozen golden would catch a
   regression to last-wins. Here we assert the mutant genuinely differs, proving
   the checks have teeth. */
(function () {
  function lastWinsMutantByKey(records, keyField) {
    var lastBody = {}, order = [];
    for (var i = 0; i < records.length; i++) {
      var line = records[i]; if (line.length === 0) continue;
      var rec = JSON.parse(line);
      var k = "K:" + JSON.stringify(rec[keyField]);
      if (!(k in lastBody)) order.push(k);
      lastBody[k] = line; // last body wins
    }
    return order.map(function (k) { return lastBody[k]; });
  }
  // shared key id:1, bodies differ by tag
  var recs = ['{"id":1,"tag":"first"}', '{"id":1,"tag":"last"}'];
  var good = keptLines(recs, { key: "id" });               // first wins -> tag:first
  var mutant = lastWinsMutantByKey(recs, "id");            // last wins  -> tag:last
  ok("the bite: last-wins mutant differs from first-wins gift (checks have teeth)",
     J(good) !== J(mutant));
  ok("the bite: gift keeps FIRST body", J(good) === J(['{"id":1,"tag":"first"}']));
})();

/* ---- report -------------------------------------------------------------- */
console.log("\ndedup-filter battery: " + pass + " passed, " + fail + " failed");
process.exit(fail === 0 ? 0 : 1);
Take the whole folder → MIT Zero dependencies, Node or browser, deterministic