Functional Weave
Code in TypeScript

todo.merge

Merge imported todos into a list by id: an imported todo replaces the one with its id, new ids are appended.

1.0.0 · published 2026-10-03 by charlie · Anterra

Pinned by 11 tests, run in TypeScript, Python and Rust.

What it does

Merges todos brought in from elsewhere (an imported CSV file, another device) into a list, by id, instead of replacing the whole list.

| id is in | result | | --- | --- | | both lists | the **incoming** todo, in the place the current one had | | only `current` | kept as it is | | only `incoming` | appended after the current todos |

For example

  • mergeTodos(, ) → two empty lists merge to an empty list
  • mergeTodos(, incoming ×2) → ×2 into an empty list, the incoming todos arrive in their own manual order
  • mergeTodos(current ×2, ) → ×2 nothing incoming: the current list in manual order, renumbered

The function

The same function in TypeScript, Python and Rust, pinned by the same tests. Pick your language; the choice follows you around the registry.

export function mergeTodos(current: readonly Todo[], incoming: readonly Todo[]): readonly Todo[]
currentTodo[]the list as it is now
incomingTodo[]the todos being brought in, e.g. from todo.import-csv; they win for ids in both
returnsTodo[]in manual order with order renumbered 0 to n-1: the current todos where they were, then the new ones

Your code names it in one line, in the file that uses it

import { mergeTodos } from "#fune/todo.merge@^1";
impl/typescript.ts · 27 lines · open · raw

Imports name this capability’s declared dependencies, which fune builds next to it in your project; each one links to its page.

import { type Todo } from "./todo_item.ts";  ← from todo.item ^1.0.0 · built alongside by fune

/** Manual order: order ascending, ties by position in the array (a stable sort). */
function inManualOrder(todos: readonly Todo[], which: string): Todo[] {
  const seen = new Set<string>();
  for (const todo of todos) {
    if (seen.has(todo.id)) throw new Error(`duplicate id "${todo.id}" in the ${which} list`);
    seen.add(todo.id);
  }
  return [...todos].sort((a, b) => a.order - b.order);
}

/**
 * Merge by id. A todo in both lists is taken from `incoming` (the newer
 * copy: todo.item keeps no edit time to compare), in the place the current
 * one had, because that is where the person put it. Todos only in `incoming`
 * follow, in their own manual order. Todos only in `current` stay. The
 * result is in manual order, renumbered 0 to n-1.
 */
export function mergeTodos(current: readonly Todo[], incoming: readonly Todo[]): readonly Todo[] {
  const mine = inManualOrder(current, "current");
  const theirs = inManualOrder(incoming, "incoming");
  const byId = new Map(theirs.map((todo) => [todo.id, todo]));
  const kept = new Set(mine.map((todo) => todo.id));
  const merged = [...mine.map((todo) => byId.get(todo.id) ?? todo), ...theirs.filter((todo) => !kept.has(todo.id))];
  return merged.map((todo, order) => ({ ...todo, order }));
}

Install

fune build

With that line in your source, in a TypeScript project (language typescript in fune.project), fune build resolves it and its 1 dependency, pins them in fune.lock, downloads only the TypeScript package of each, and builds the code above into your project’s .fune/build, one readable file per capability with a header linking back here. Or pin a range in fune.project and build in one step:

fune add todo.merge
Download for TypeScript todo.merge-1.0.0-typescript.fune · 16,323 bytes sha256 6f013e98348d6f97e014f63ec4aea0190809d8429d6e15bfcc6bbcce327f011a

The manifest, vectors and README with only the TypeScript implementation. Install it without the registry with fune add ./todo.merge-1.0.0-typescript.fune, or fetch it from a terminal with fune pull todo.merge@1.0.0:typescript.

The whole function, every language, is one file too: todo.merge-1.0.0.fune, 19,135 bytes, sha256 70fd49a1e279c942df6f45cf86dbc03153376c38c62cd1836180141e1f1f11db. It installs into a project of any language.

Customise it in your app

The seams this capability offers. Put a marker directly above a function of your own and fune build wires it into the built code; the package on the registry is not changed, the built file’s header lists it under CUSTOMISED, and fune hooks lists every hook in the project. How hooks work.

before — your function gets the arguments and returns them, changed or not, or throws to refuse the call.

// fune: before todo.merge

after — your function gets the result and the arguments, and returns the final result.

// fune: after todo.merge

replace — inside this capability’s code only, calls to a dependency go to your function, with the same signature. Other capabilities that use it are unaffected; write in * to replace it everywhere.

// fune: replace todo.item in todo.merge

step — your function runs at a numbered point inside the function’s body, receives the in-scope values it names as parameters, and may return replacements. List the points with fune show todo.merge --steps.

// fune: step todo.merge after <n|label>

Tests

A version published now needs at least 8 tests for every function, and one that expects the error for each function that throws; the registry refuses it otherwise. fune verify --all runs each case in TypeScript, Python and Rust, and a project runs them again with fune verify. This page lists the cases; it does not run them. The exact JSON is vectors.json.

CaseArgumentsExpected
two empty lists merge to an empty list , →
into an empty list, the incoming todos arrive in their own manual order , incoming ×2 → ×2
nothing incoming: the current list in manual order, renumbered current ×2, → ×2
an id in both: the incoming todo wins, in the current one's place current ×2, incoming ×1 → ×2
new ids are appended after the current todos current ×2, incoming ×2 → ×3
equal orders keep their array order current ×2, → ×2
a new todo with order 0 still goes after the current ones, and gaps close current ×2, incoming ×1 → ×3
a done todo reopened in the file comes back open current ×1, incoming ×1 → ×1
a mixed merge: kept, replaced and new current ×3, incoming ×2 → ×4
an id twice in the incoming list is an error , incoming ×2 → error: duplicate id "a" in the incoming list
Show the other 1 test
CaseArgumentsExpected
an id twice in the current list is an error current ×2, → error: duplicate id "a" in the current list

More from the author

## Which copy wins

The incoming one. A todo (todo.item) records when it was created and completed but not when it was last edited, so there is no honest way to tell which of two copies is newer. Importing a file is the person saying "these are the versions I want", so the file's row is treated as the newer copy: every field comes from it, done state and timestamps included.

## Order

The result is in manual order (todo.sort's `manual`: `order` ascending, ties by array position), renumbered 0 to n-1, as todo.list's `moveTodo` leaves a list:

1. The current todos in their manual order, each replaced by its incoming copy where there is one. A replaced todo keeps its **current** position: the person arranged this list, and the file's `order` is a position in a different list. 2. Then the todos only in `incoming`, in the incoming list's own manual order, so a file's order among its new rows is kept.

So a new todo whose file `order` is 0 still goes after the existing ones, and gaps in the current orders close up.

## Errors

An id that appears twice in either list is an error (`duplicate id "a" in the incoming list`), because it is ambiguous which copy to keep. The todos are not otherwise checked: todo.import-csv has already checked a file's rows with todo.item's `validateTodo`.

Files

PathBytes
README.md1,659
impl/python.py1,049
impl/rust.rs1,646
impl/typescript.ts1,301
vectors.json10,258