agentleFS
metarhiametarhia/metaskills/skills/metautil-data-structures/SKILL.md

metautil-data-structures

Skill61 starsChanged 8 months agoMITView on GitHub

Operate metautil package ADTs — ConsList, Struct, Queue, Stack, Deque, List, Trie, UnrolledList, CircularBuffer. Use when importing or calling these classes from metautil. For native Map/Object/Set/Array, use js-data-structures. For custom data structures, use data-structures.

The file

We split this skill into the rules it gives an agent

4 rules. About 0 are common, 0 are distinctive, and 4 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈0

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 4

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Import from the package root.

    line 8
  2. Very specific:

    Default value is true when omitted.

    line 199
  3. Very specific:

    Use has to distinguish missing from stored undefined

    line 202
  4. Very specific:

    Prefer structure APIs over inventing Array adapters.

    line 209

≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Metautil Data Structures

Import from the package root. Prefer these ADTs over ad-hoc Array/Object when the discipline (FIFO, LIFO, persistence, schema) matters.

const metautil = require('metautil');
const { ConsList, cons, uncons, Struct, Deque, Queue, Stack } = metautil;
const { List, ListNode, CircularBuffer, UnrolledList, Trie } = metautil;

Choose the right structure

  • FIFO jobs / BFS / sliding failure window: Queue
  • LIFO undo / nested layers / brackets: Stack
  • Both ends O(1), capped window: Deque or CircularBuffer
  • Index + mid-sequence edit/move/group: List
  • Immutable history / branching / sharing: ConsList
  • Typed sealed/frozen records: Struct
  • Prefix autocomplete / string keys: Trie
  • Extreme enqueue/dequeue volume only: UnrolledList (no peek / interop)
  • Random access on a ring: CircularBuffer.at (not Deque)

Complexity and surface

  • CircularBuffer — ends O(1), index O(1) via at
  • Deque / Queue / Stack — ends O(1); no index (at only on buffer)
  • UnrolledList — ends O(1); no peek / interop
  • List — ends O(1), index O(n)
  • ConsList — prepend O(1), walk O(n)
  • Trie — string-key prefix map

Who has what (beyond size / isEmpty where present):

  • fromArray / toArray / iterator: all except UnrolledList (and Trie)
  • peek: Queue, Stack
  • at: CircularBuffer only
  • every / reduce: CircularBuffer, Deque, Stack (not Queue)
  • clear / includes: ring family + List + Trie.clear; not ConsList / UnrolledList

Interoperability

Convert through Array:

const list = List.fromArray([1, 2, 3, 4, 5]);
const queue = Queue.fromArray(list.filter((n) => n % 2 === 0).toArray());
const consList = ConsList.fromArray(queue.toArray());

Deque, Queue, and Stack are thin facades over CircularBuffer (same ring storage).

Struct

Schema is inferred from default literals. Immutable uses Object.freeze; mutable uses Object.seal. Class name becomes constructor.name. Statics: fields, schema, mutable.

  • undefined: schema unknown, accepts anything
  • null: schema ref, accepts null, object, function
  • [] / {}: schema array / object, accepts arrays / plain objects (fresh copy per instance)
  • primitive: schema typeof, accepts that primitive

null (ref) ≠ {} (object); arrays fail object and plain objects fail array.

const Money = Struct.immutable('Money', { currency: 'USD', amountCents: 0 });
const a = Money.create({ amountCents: 500 });
const b = a.fork({ amountCents: 700 }); // new frozen record
// a.update(...) throws; use fork or branch

const Reading = Struct.mutable('Reading', {
  deviceId: '',
  millicelsius: 0,
  processed: false,
});
const r = new Reading({ deviceId: 'sensor-7' });
r.update({ processed: true }); // in-place, mutable only
  • fork(updates) — full shallow copy via toObject() + updates (independent instance)
  • branch(updates) — Object.create(this) overlay; cheap sharing of unset fields
  • On a mutable branch, update only works for fields the branch itself defined
  • Unknown field or wrong type throws TypeError

ConsList

Immutable singly-linked cons cells with structural sharing. prepend is O(1); never mutate. ConsList.empty is a singleton (===).

const shared = ConsList.of(3, 4, 5);
const branch1 = shared.prepend(2).prepend(1); // [1, 2, 3, 4, 5]
const branch2 = shared.prepend(99); // [99, 3, 4, 5]
// branch1.tail.tail === shared; branch2.tail === shared

const { value, tail } = branch1.uncons(); // same as uncons(branch1)
const rebuilt = cons(value, tail); // same as tail.prepend(value)

let history = ConsList.of('draft v1');
history = history.prepend('draft v2');
const undone = history.tail; // O(1) restore; suffix still shared
  • merge(...lists) — join in argument order; shares the last list as suffix; O(n) over all but the last
  • member(value) — first shared suffix whose head === value, or empty (not a boolean)
  • equals / includes — element-wise ===
  • Also: of / fromArray / fromIterable, map / filter / find / some / every, reverse, toArray
  • Empty reduce without seed throws TypeError; empty every is true

CircularBuffer / Deque / Queue / Stack

Growable power-of-two ring (initial capacity 16, doubles on full). End ops O(1); CircularBuffer.at(i) is O(1) (-1 = last). clear() resets capacity to initial.

const WINDOW = 5;
const trace = new Deque();
trace.push(point);
if (trace.size > WINDOW) trace.shift();

const q = Queue.fromArray(jobs);
q.enqueue(job);
const next = q.dequeue(); // undefined if empty
q.peek();

const undo = new Stack();
undo.push(command);
const last = undo.pop();
undo.peek();
  • CircularBuffer / Deque: unshift / push, shift / pop; index only on buffer via at
  • Queue: enqueue / dequeue / peek (front)
  • Stack: push / pop / peek (back)
  • Empty every is true; empty reduce without seed throws TypeError

List / ListNode

Mutable doubly-linked sequence. Ends O(1); index ops O(n). Public API is value/index based. Non-integer indexes are no-ops; negatives count from the end.

const route = List.of(a, b, c);
route.insert(1, rush);
route.move(from, to);
route.rotate(1); // default 1; positive = left
const { before, after } = route.splitAt(2);
const byZone = route.groupBy((stop) => stop.zone);
const removed = route.remove(x, y); // count removed
route.sum((x) => x.cents);

Mutating vs copying:

  • Mutate: append / prepend / insert / delete / drop / rotate / swap / move / reverse / sort / replace / clear
  • Copy: take / slice / splitAt / map / flatMap / filter / toReversed / toSorted / clone / merge
  • take / slice return null for empty range, take(0), or bad bounds; drop mutates in place
  • insert clamps out-of-range indexes; delete / swap / move out-of-range are no-ops
  • reduce(fn, initial) — initial is required (unlike ConsList / CircularBuffer)
  • avg on empty is 0; min / max on empty are undefined
  • Also: at / set, indexOf / lastIndexOf, includes, some / every / find / findIndex

ListNode is low-level (create / append / prepend / unlink / seek / fromArray / copy / link) for custom structures that own head/tail/size. seek non-integer returns null. Do not mutate prev/next of nodes owned by a List.

UnrolledList

High-throughput FIFO: pooled fixed-size array nodes. Use when volume is high and you only need enqueue/dequeue.

const pending = new UnrolledList({ nodeSize: 256, poolSize: 4 });
pending.enqueue(task);
let task = pending.dequeue();
while (task !== undefined) {
  task();
  task = pending.dequeue();
}
  • Defaults: nodeSize: 1024, poolSize: 2
  • Raise nodeSize for larger bursts; raise poolSize if drain/refill churns nodes
  • No peek, fromArray, iterator, or includes
  • enqueue(undefined) is a real item — empty dequeue() is indistinguishable from that value

Trie

Prefix tree for string keys.

const trie = new Trie();
trie.insert('cat');
trie.insert('car', 42);
trie.insert('gone', undefined);
trie.has('gone'); // true — present; get returns undefined
trie.get('car'); // 42
trie.complete('ca'); // order may vary
trie.delete('car'); // prunes empty branches
  • Default value is true when omitted; re-insert same key does not bump size
  • Empty string '' is a valid key
  • insert non-string throws TypeError; has / get / delete non-string soft-fail (false / undefined / false)
  • Use has to distinguish missing from stored undefined

Contracts

  • Empty pop / dequeue / shift return undefined
  • Empty reduce without seed throws TypeError (ConsList / CircularBuffer family); List always needs a seed
  • Equality / includes / member use strict === (NaN does not match)
  • Prefer structure APIs over inventing Array adapters; convert via toArray / fromArray when crossing types

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.