diff -pruN 0.2.99-1/array.js 0.2.114-1/array.js
--- 0.2.99-1/array.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/array.js	2025-07-16 19:28:19.000000000 +0000
@@ -55,11 +55,10 @@ export const from = Array.from
  * True iff condition holds on every element in the Array.
  *
  * @function
- * @template ITEM
- * @template {ArrayLike<ITEM>} ARR
+ * @template {ArrayLike<any>} ARR
  *
  * @param {ARR} arr
- * @param {function(ITEM, number, ARR):boolean} f
+ * @param {ARR extends ArrayLike<infer S> ? ((value:S, index:number, arr:ARR) => boolean) : any} f
  * @return {boolean}
  */
 export const every = (arr, f) => {
@@ -75,10 +74,10 @@ export const every = (arr, f) => {
  * True iff condition holds on some element in the Array.
  *
  * @function
- * @template S
- * @template {ArrayLike<S>} ARR
+ * @template {ArrayLike<any>} ARR
+ *
  * @param {ARR} arr
- * @param {function(S, number, ARR):boolean} f
+ * @param {ARR extends ArrayLike<infer S> ? ((value:S, index:number, arr:ARR) => boolean) : never} f
  * @return {boolean}
  */
 export const some = (arr, f) => {
@@ -182,3 +181,39 @@ export const map = (arr, mapper) => {
   }
   return /** @type {any} */ (res)
 }
+
+/**
+ * This function bubble-sorts a single item to the correct position. The sort happens in-place and
+ * might be useful to ensure that a single item is at the correct position in an otherwise sorted
+ * array.
+ *
+ * @example
+ *  const arr = [3, 2, 5]
+ *  arr.sort((a, b) => a - b)
+ *  arr // => [2, 3, 5]
+ *  arr.splice(1, 0, 7)
+ *  array.bubbleSortItem(arr, 1, (a, b) => a - b)
+ *  arr // => [2, 3, 5, 7]
+ *
+ * @template T
+ * @param {Array<T>} arr
+ * @param {number} i
+ * @param {(a:T,b:T) => number} compareFn
+ */
+export const bubblesortItem = (arr, i, compareFn) => {
+  const n = arr[i]
+  let j = i
+  // try to sort to the right
+  while (j + 1 < arr.length && compareFn(n, arr[j + 1]) > 0) {
+    arr[j] = arr[j + 1]
+    arr[++j] = n
+  }
+  if (i === j && j > 0) { // no change yet
+    // sort to the left
+    while (j > 0 && compareFn(arr[j - 1], n) > 0) {
+      arr[j] = arr[j - 1]
+      arr[--j] = n
+    }
+  }
+  return j
+}
diff -pruN 0.2.99-1/array.test.js 0.2.114-1/array.test.js
--- 0.2.99-1/array.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/array.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -1,5 +1,6 @@
 import * as array from './array.js'
 import * as t from './testing.js'
+import * as prng from './prng.js'
 
 /**
  * @param {t.TestCase} _tc
@@ -117,3 +118,39 @@ export const testUnique = _tc => {
   t.compare([{ el: 1 }], array.uniqueBy([{ el: 1 }, { el: 1 }], o => o.el))
   t.compare([], array.uniqueBy([], o => o))
 }
+
+/**
+ * @param {t.TestCase} tc
+ */
+export const testBubblesortItemEdgeCases = tc => {
+  // does not throw..
+  array.bubblesortItem([1], 0, (a, b) => a - b)
+  array.bubblesortItem([2, 1], 1, (a, b) => a - b)
+  array.bubblesortItem([2, 1], 0, (a, b) => a - b)
+}
+
+/**
+ * @param {t.TestCase} tc
+ */
+export const testRepeatBubblesortItem = tc => {
+  const arr = Array.from(prng.uint8Array(tc.prng, 10))
+  arr.sort((a, b) => a - b)
+  const newItem = prng.uint32(tc.prng, 0, 256)
+  const pos = prng.uint32(tc.prng, 0, arr.length)
+  arr.splice(pos, newItem)
+  const arrCopySorted = arr.slice().sort((a, b) => a - b)
+  array.bubblesortItem(arr, pos, (a, b) => a - b)
+  t.compare(arr, arrCopySorted)
+}
+
+/**
+ * @param {t.TestCase} tc
+ */
+export const testRepeatBubblesort = tc => {
+  const arr = Array.from(prng.uint8Array(tc.prng, 10))
+  const arrCopySorted = arr.slice().sort((a, b) => a - b)
+  for (let i = arr.length - 1; i >= 0; i--) {
+    while (array.bubblesortItem(arr, i, (a, b) => a - b) !== i) { /* nop */ }
+  }
+  t.compare(arr, arrCopySorted)
+}
diff -pruN 0.2.99-1/bin/0serve.js 0.2.114-1/bin/0serve.js
--- 0.2.99-1/bin/0serve.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/bin/0serve.js	2025-07-16 19:28:19.000000000 +0000
@@ -1,4 +1,11 @@
 #!/usr/bin/env node
+
+/**
+ * Simple http server implementation.
+ * Optionally, you may set `DEBUG_BROWSER` environment variable to use a different browser to debug
+ * web apps.
+ */
+
 import * as http from 'http'
 import * as path from 'path'
 import * as fs from 'fs'
@@ -9,6 +16,7 @@ import * as logging from 'lib0/logging'
 const host = env.getParam('--host', 'localhost')
 const port = number.parseInt(env.getParam('--port', '8000'))
 const paramOpenFile = env.getParam('-o', '')
+const debugBrowser = env.getConf('DEBUG_BROWSER')
 
 /**
  * @type {Object<string,string>}
@@ -81,7 +89,7 @@ const server = http.createServer((req, r
 server.listen(port, host, () => {
   logging.print(logging.BOLD, logging.ORANGE, `Server is running on http://${host}:${port}`)
   if (paramOpenFile) {
-    const start = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open'
+    const start = debugBrowser || (process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open')
     import('child_process').then(cp => {
       cp.exec(`${start} http://${host}:${port}/${paramOpenFile}`)
     })
diff -pruN 0.2.99-1/bin/gentesthtml.js 0.2.114-1/bin/gentesthtml.js
--- 0.2.99-1/bin/gentesthtml.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/bin/gentesthtml.js	2025-07-16 19:28:19.000000000 +0000
@@ -4,6 +4,8 @@ import * as object from '../object.js'
 import * as env from '../environment.js'
 
 const script = env.getParam('--script', './test.js')
+const includeDeps = env.getParam('--include-dependencies', '').split(',').filter(d => d.length)
+const customImportsPath = env.getParam('--custom-imports', '')
 
 /**
  * @type {Object<string,string>}
@@ -24,21 +26,29 @@ const scopes = {}
 const extractModMap = (v, k, pkgName, pathPrefix, importMap) => {
   if (k[0] !== '.') return
   if (typeof v === 'object') {
-    extractModMap(v.browser || v.module || v.default || v.import, k, pkgName, pathPrefix, importMap)
+    extractModMap(v.browser || v.import || v.module || v.default, k, pkgName, pathPrefix, importMap)
   } else if (v && v[0] === '.') {
     importMap[pkgName + k.slice(1)] = pathPrefix + v.slice(1)
   }
 }
 
 /**
+ * @param {string} s
+ */
+const _maybeAddRelPrefix = s => (s[0] !== '.' ? './' : '') + s
+
+/**
  * @param {any} pkgJson
  * @param {string} pathPrefix
  * @param {Object<string,string>} importMap
  */
 const readPkg = (pkgJson, pathPrefix, importMap) => {
+  if (pkgJson.exports == null && pkgJson.main != null) {
+    importMap[pkgJson.name] = pathPrefix + _maybeAddRelPrefix(pkgJson.main).slice(1)
+  }
   object.forEach(pkgJson.exports, (v, k) => extractModMap(v, k, pkgJson.name, pathPrefix, importMap))
   object.forEach(pkgJson.dependencies, (_v, depName) => {
-    const nextImportMap = pathPrefix === '.' ? exports : (scopes[pathPrefix + '/'] = {})
+    const nextImportMap = pathPrefix === '.' ? exports : (scopes[pathPrefix + '/'] || (scopes[pathPrefix + '/'] = {}))
     const prefix = `./node_modules/${depName}`
     const depPkgJson = JSON.parse(fs.readFileSync(prefix + '/package.json', { encoding: 'utf8' }))
     readPkg(depPkgJson, prefix, nextImportMap)
@@ -47,6 +57,16 @@ const readPkg = (pkgJson, pathPrefix, im
 
 const rootPkgJson = JSON.parse(fs.readFileSync('./package.json', { encoding: 'utf8' }))
 readPkg(rootPkgJson, '.', exports)
+includeDeps.forEach(depName => {
+  const prefix = `./node_modules/${depName}`
+  const depPkgJson = JSON.parse(fs.readFileSync(`${prefix}/package.json`, { encoding: 'utf8' }))
+  readPkg(depPkgJson, prefix, exports)
+})
+
+const customImports = {}
+if (customImportsPath !== '') {
+  object.assign(customImports, JSON.parse(fs.readFileSync(customImportsPath, { encoding: 'utf8' })))
+}
 
 const testHtml = `
 <!DOCTYPE html>
@@ -55,7 +75,7 @@ const testHtml = `
   <title>Testing ${rootPkgJson.name}</title>
   <script type="importmap">
     {
-      "imports": ${JSON.stringify(exports, null, 2)},
+      "imports": ${JSON.stringify(object.assign({}, exports, customImports), null, 2)},
       "scopes": ${JSON.stringify(scopes, null, 2)}
     }
   </script>
diff -pruN 0.2.99-1/component.js 0.2.114-1/component.js
--- 0.2.99-1/component.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/component.js	2025-07-16 19:28:19.000000000 +0000
@@ -101,6 +101,7 @@ const parseAttrVal = (val, type) => {
 }
 
 /**
+ * @template S
  * @typedef {Object} CONF
  * @property {string?} [CONF.template] Template for the shadow dom.
  * @property {string} [CONF.style] shadow dom style. Is only used when
@@ -118,7 +119,6 @@ const parseAttrVal = (val, type) => {
  * to event listener.
  * @property {function(S, S, Lib0Component<S>):Object<string,string>} [CONF.slots] Fill slots
  * automatically when state changes. Maps from slot-name to slot-html.
- * @template S
  */
 
 /**
diff -pruN 0.2.99-1/debian/changelog 0.2.114-1/debian/changelog
--- 0.2.99-1/debian/changelog	2025-02-06 11:01:04.000000000 +0000
+++ 0.2.114-1/debian/changelog	2025-09-15 08:27:48.000000000 +0000
@@ -1,3 +1,12 @@
+node-lib0 (0.2.114-1) unstable; urgency=medium
+
+  * Declare compliance with policy 4.7.2
+  * debian/watch version 5
+  * New upstream version 0.2.114
+  * Update test
+
+ -- Yadd <yadd@debian.org>  Mon, 15 Sep 2025 10:27:48 +0200
+
 node-lib0 (0.2.99-1) unstable; urgency=medium
 
   * Team upload
diff -pruN 0.2.99-1/debian/control 0.2.114-1/debian/control
--- 0.2.99-1/debian/control	2025-02-06 10:51:40.000000000 +0000
+++ 0.2.114-1/debian/control	2025-09-15 08:18:46.000000000 +0000
@@ -13,7 +13,7 @@ Build-Depends:
  , node-typescript
  , node-rollup-pluginutils
  , rollup
-Standards-Version: 4.7.0
+Standards-Version: 4.7.2
 Homepage: https://github.com/dmonad/lib0#readme
 Vcs-Git: https://salsa.debian.org/js-team/node-lib0.git
 Vcs-Browser: https://salsa.debian.org/js-team/node-lib0
diff -pruN 0.2.99-1/debian/tests/pkg-js/test 0.2.114-1/debian/tests/pkg-js/test
--- 0.2.99-1/debian/tests/pkg-js/test	2025-02-06 10:51:40.000000000 +0000
+++ 0.2.114-1/debian/tests/pkg-js/test	2025-09-15 08:19:59.000000000 +0000
@@ -1 +1 @@
-node ./test.js --repetition-time 50 --production
+node ./test.js --repetition-time 50
diff -pruN 0.2.99-1/debian/watch 0.2.114-1/debian/watch
--- 0.2.99-1/debian/watch	2025-02-06 10:51:40.000000000 +0000
+++ 0.2.114-1/debian/watch	2025-09-15 08:18:46.000000000 +0000
@@ -1,5 +1,5 @@
-version=4
-opts=\
-dversionmangle=auto,\
-filenamemangle=s/.*?(\d[\d\.-]*@ARCHIVE_EXT@)/node-lib0-$1/ \
- https://github.com/dmonad/lib0/tags .*/archive.*/v?([\d\.]+).tar.gz
+Version: 5
+
+Template: Github
+Owner: dmonad
+Project: lib0
diff -pruN 0.2.99-1/diff/patience.js 0.2.114-1/diff/patience.js
--- 0.2.99-1/diff/patience.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/diff/patience.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,226 @@
+/**
+ * A very simple diff algorithm. Slightly adapted to support splitting at different stages (e.g.
+ * first diff lines, then diff words)
+ *
+ * https://bramcohen.livejournal.com/73318.html
+ *
+ * @experiemantal This API will likely change.
+ */
+
+import * as map from '../map.js'
+import * as math from '../math.js'
+import * as array from '../array.js'
+
+/**
+ * Implementation of patience diff. Expects that content is pre-split (e.g. by newline).
+ *
+ * @param {Array<string>} as
+ * @param {Array<string>} bs
+ * @return {Array<{ index: number, remove: Array<string>, insert: Array<string>}>} changeset @todo should use delta instead
+ */
+export const diff = (as, bs) => {
+  const {
+    middleAs,
+    middleBs,
+    commonPrefix
+  } = removeCommonPrefixAndSuffix(as, bs)
+  return lcs(middleAs, middleBs, commonPrefix)
+}
+
+/**
+ * @param {string} a
+ * @param {string} b
+ * @param {RegExp|string} _regexp
+ */
+export const diffSplitBy = (a, b, _regexp) => {
+  const isStringSeparator = typeof _regexp === 'string'
+  const separator = isStringSeparator ? _regexp : ''
+  const regexp = isStringSeparator ? new RegExp(_regexp, 'g') : _regexp
+  const as = splitByRegexp(a, regexp, !isStringSeparator)
+  const bs = splitByRegexp(b, regexp, !isStringSeparator)
+  const changes = diff(as, bs)
+  let prevSplitIndex = 0
+  let prevStringIndex = 0
+  return changes.map(change => {
+    for (; prevSplitIndex < change.index; prevSplitIndex++) {
+      prevStringIndex += as[prevSplitIndex].length
+    }
+    return {
+      index: prevStringIndex,
+      remove: change.remove.join(separator),
+      insert: change.insert.join(separator)
+    }
+  })
+}
+
+/**
+ * Sensible default for diffing strings using patience (it's fast though).
+ *
+ * Perform different types of patience diff on the content. Diff first by newline, then paragraphs, then by word
+ * (split by space, brackets, punctuation)
+ *
+ * @param {string} a
+ * @param {string} b
+ */
+export const diffAuto = (a, b) =>
+  diffSplitBy(a, b, '\n').map(d =>
+    diffSplitBy(d.remove, d.insert, /\. |[a-zA-Z0-9]+|[. ()[\],;{}]/g).map(dd => ({
+      insert: dd.insert,
+      remove: dd.remove,
+      index: dd.index + d.index
+    }))
+  ).flat()
+
+/**
+ * @param {Array<string>} as
+ * @param {Array<string>} bs
+ */
+const removeCommonPrefixAndSuffix = (as, bs) => {
+  const commonLen = math.min(as.length, bs.length)
+  let commonPrefix = 0
+  let commonSuffix = 0
+  // match start
+  for (; commonPrefix < commonLen && as[commonPrefix] === bs[commonPrefix]; commonPrefix++) { /* nop */ }
+  // match end
+  for (; commonSuffix < commonLen - commonPrefix && as[as.length - 1 - commonSuffix] === bs[bs.length - 1 - commonSuffix]; commonSuffix++) { /* nop */ }
+  const middleAs = as.slice(commonPrefix, as.length - commonSuffix)
+  const middleBs = bs.slice(commonPrefix, bs.length - commonSuffix)
+  return {
+    middleAs, middleBs, commonPrefix, commonSuffix
+  }
+}
+
+/**
+ * Splits string by regex and returns all strings as an array. The matched parts are also returned.
+ *
+ * @param {string} str
+ * @param {RegExp} regexp
+ * @param {boolean} includeSeparator
+ */
+const splitByRegexp = (str, regexp, includeSeparator) => {
+  const matches = [...str.matchAll(regexp)]
+  let prevIndex = 0
+  /**
+   * @type {Array<string>}
+   */
+  const res = []
+  matches.forEach(m => {
+    prevIndex < (m.index || 0) && res.push(str.slice(prevIndex, m.index))
+    includeSeparator && res.push(m[0]) // is always non-empty
+    prevIndex = /** @type {number} */ (m.index) + m[0].length
+  })
+  const end = str.slice(prevIndex)
+  end.length > 0 && res.push(end)
+  return res
+}
+
+/**
+ * An item may have multiple occurances (not when matching unique entries). It also may have a
+ * reference to the stack of other items (from as to bs).
+ */
+class Item {
+  constructor () {
+    /**
+     * @type {Array<number>}
+     */
+    this.indexes = []
+    /**
+     * The matching item from the other side
+     * @type {Item?}
+     */
+    this.match = null
+    /**
+     * For patience sort. Reference (index of the stack) to the previous pile.
+     *
+     * @type {Item?}
+     */
+    this.ref = null
+  }
+}
+
+/**
+ * @param {Array<string>} xs
+ */
+const partition = xs => {
+  /**
+   * @type {Map<string,Item>}
+   */
+  const refs = map.create()
+  xs.forEach((x, index) => {
+    map.setIfUndefined(refs, x, () => new Item()).indexes.push(index)
+  })
+  return refs
+}
+
+/**
+ * Find the longest common subsequence of items using patience sort.
+ *
+ * @param {Array<string>} as
+ * @param {Array<string>} bs
+ * @param {number} indexAdjust
+ */
+const lcs = (as, bs, indexAdjust) => {
+  if (as.length === 0 && bs.length === 0) return []
+  const aParts = partition(as)
+  const bParts = partition(bs)
+  /**
+   * @type {Array<Array<Item>>} I.e. Array<Pile<Item>>
+   */
+  const piles = []
+  aParts.forEach((aItem, aKey) => {
+    // skip if no match or if either item is not unique
+    if (aItem.indexes.length > 1 || (aItem.match = bParts.get(aKey) || null) == null || aItem.match.indexes.length > 1) return
+    for (let i = 0; i < piles.length; i++) {
+      const pile = piles[i]
+      if (aItem.match.indexes[0] < /** @type {Item} */ (pile[pile.length - 1].match).indexes[0]) {
+        pile.push(aItem)
+        if (i > 0) aItem.ref = array.last(piles[i - 1])
+        return
+      }
+    }
+    piles.length > 0 && (aItem.ref = array.last(piles[piles.length - 1]))
+    piles.push([aItem])
+  })
+  /**
+   * References to all matched items
+   *
+   * @type {Array<Item>}
+   */
+  const matches = []
+  /**
+   * @type {Item?}
+   */
+  let currPileItem = piles[piles.length - 1]?.[0]
+  while (currPileItem != null) {
+    matches.push(currPileItem)
+    currPileItem = currPileItem.ref
+  }
+  matches.reverse()
+  // add pseude match (assume the string terminal always matches)
+  const pseudoA = new Item()
+  const pseudoB = new Item()
+  pseudoA.match = pseudoB
+  pseudoA.indexes.push(as.length)
+  pseudoB.indexes.push(bs.length)
+  matches.push(pseudoA)
+  /**
+   * @type {Array<{ index: number, remove: Array<string>, insert: Array<string>}>}
+   */
+  const changeset = []
+  let diffAStart = 0
+  let diffBStart = 0
+  for (let i = 0; i < matches.length; i++) {
+    const m = matches[i]
+    const delLength = m.indexes[0] - diffAStart
+    const insLength = /** @type {Item} */ (m.match).indexes[0] - diffBStart
+    if (delLength !== 0 || insLength !== 0) {
+      const stripped = removeCommonPrefixAndSuffix(as.slice(diffAStart, diffAStart + delLength), bs.slice(diffBStart, diffBStart + insLength))
+      if (stripped.middleAs.length !== 0 || stripped.middleBs.length !== 0) {
+        changeset.push({ index: diffAStart + indexAdjust + stripped.commonPrefix, remove: stripped.middleAs, insert: stripped.middleBs })
+      }
+    }
+    diffAStart = m.indexes[0] + 1
+    diffBStart = /** @type {Item} */ (m.match).indexes[0] + 1
+  }
+  return changeset
+}
diff -pruN 0.2.99-1/diff/patience.test.js 0.2.114-1/diff/patience.test.js
--- 0.2.99-1/diff/patience.test.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/diff/patience.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,173 @@
+import * as prng from '../prng.js'
+import * as t from '../testing.js'
+import * as patience from './patience.js'
+
+/**
+ * @param {string} a
+ * @param {string} b
+ * @param {Array<{ insert: string, remove: string, index: number }>} expect
+ */
+const testDiffAuto = (a, b, expect) => {
+  const res = patience.diffAuto(a, b)
+  t.info(`Diffing "${a}" with "${b}"`)
+  console.log(res)
+  t.compare(res, expect)
+}
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testDiffing = _tc => {
+  testDiffAuto(
+    'z d a b c',
+    'y d b a c',
+    [{
+      insert: 'y',
+      remove: 'z',
+      index: 0
+    }, {
+      insert: 'b ',
+      remove: '',
+      index: 4
+    }, {
+      insert: '',
+      remove: ' b',
+      index: 5
+    }]
+  )
+  testDiffAuto(
+    'a b c',
+    'b a c',
+    [{
+      insert: 'b ',
+      remove: '',
+      index: 0
+    }, {
+      insert: '',
+      remove: ' b',
+      index: 1
+    }]
+  )
+  testDiffAuto(
+    'x  ',
+    ' ',
+    [{
+      insert: '',
+      remove: 'x ',
+      index: 0
+    }]
+  )
+  // no change
+  testDiffAuto(
+    'testa',
+    'testa',
+    []
+  )
+  // single char change
+  testDiffAuto(
+    'testa',
+    'testb',
+    [{
+      insert: 'testb',
+      remove: 'testa',
+      index: 0
+    }]
+  )
+  // single word change
+  testDiffAuto(
+    'The rabbit jumped over the fence.\n',
+    'The dog jumped over the fence.\n',
+    [{
+      insert: 'dog',
+      remove: 'rabbit',
+      index: 4
+    }]
+  )
+  // similar sentences.
+  testDiffAuto(
+    'the dog. the cat.',
+    'the cat. the rabbit.',
+    [{
+      insert: 'cat',
+      remove: 'dog',
+      index: 4
+    }, {
+      insert: 'rabbit',
+      remove: 'cat',
+      index: 13
+    }]
+  )
+  testDiffAuto(
+    'cat food',
+    'my cat food',
+    [{
+      insert: 'my ',
+      remove: '',
+      index: 0
+    }]
+  )
+  testDiffAuto(
+    'the cat stuff',
+    'my cat food',
+    [{
+      insert: 'my',
+      remove: 'the',
+      index: 0
+    }, {
+      insert: 'food',
+      remove: 'stuff',
+      index: 8
+    }]
+  )
+}
+
+/**
+ * @param {t.TestCase} tc
+ */
+export const testRepeatRandomWordReplace = tc => {
+  const NWords = 600
+  const NReplacements = Math.floor(NWords / 20)
+  const NInserts = Math.floor(NWords / 20) + 1
+  const NDeletes = Math.floor(NWords / 20) + 1
+  const MaxWordLen = 6
+
+  t.group(`Diff on changed list of words (#words=${NWords},#replacements=${NReplacements},#inserts=${NInserts},#deletes=${NDeletes}})`, () => {
+    const words = []
+    for (let i = 0; i < NWords; i++) {
+      words.push(prng.word(tc.prng, 0, MaxWordLen))
+    }
+    const newWords = words.slice()
+    for (let i = 0; i < NReplacements; i++) {
+      const pos = prng.int32(tc.prng, 0, words.length - 1)
+      newWords[pos] = prng.word(tc.prng, 0, MaxWordLen)
+    }
+    for (let i = 0; i < NInserts; i++) {
+      const pos = prng.int32(tc.prng, 0, words.length - 1)
+      newWords.splice(pos, 0, prng.word(tc.prng, 0, MaxWordLen))
+    }
+    for (let i = 0; i < NDeletes; i++) {
+      const pos = prng.int32(tc.prng, 0, words.length - 1)
+      newWords.splice(pos, 1)
+    }
+    const before = words.join(' ')
+    const after = newWords.join(' ')
+    /**
+     * @type {Array<{ insert: string, remove: string, index: number }>}
+     */
+    let d = []
+    t.measureTime(`time to calculate diff (a.length=${before.length},b.length=${after.length})`, () => {
+      d = patience.diffAuto(before, after)
+    })
+    let updating = before
+    console.log({ words, newWords, diff: d })
+    // verify by applying
+    for (let i = d.length - 1; i >= 0; i--) {
+      const change = d[i]
+      const spliced = updating.split('')
+      spliced.splice(change.index, change.remove.length, change.insert)
+      updating = spliced.join('')
+    }
+    t.compare(updating, after)
+    t.assert(d.length <= NReplacements + 1 + NInserts + NDeletes) // Sanity check: A maximum of one fault
+  })
+}
diff -pruN 0.2.99-1/diff.js 0.2.114-1/diff.js
--- 0.2.99-1/diff.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/diff.js	2025-07-16 19:28:19.000000000 +0000
@@ -18,14 +18,12 @@ import { equalityStrict } from './functi
  * a === b // values match
  * ```
  *
+ * @template {string} T
  * @typedef {Object} SimpleDiff
  * @property {Number} index The index where changes were applied
  * @property {Number} remove The number of characters to delete starting
  *                                  at `index`.
  * @property {T} insert The new text to insert at `index` after applying
- *                           `delete`
- *
- * @template T
  */
 
 const highSurrogateRegex = /[\uD800-\uDBFF]/
diff -pruN 0.2.99-1/eventloop.js 0.2.114-1/eventloop.js
--- 0.2.99-1/eventloop.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/eventloop.js	2025-07-16 19:28:19.000000000 +0000
@@ -1,5 +1,7 @@
 /* global requestIdleCallback, requestAnimationFrame, cancelIdleCallback, cancelAnimationFrame */
 
+import * as time from './time.js'
+
 /**
  * Utility module to work with EcmaScript's event loop.
  *
@@ -93,14 +95,34 @@ export const idleCallback = cb => typeof
 
 /**
  * @param {number} timeout Timeout of the debounce action
- * @return {function(function():void):void}
+ * @param {number} triggerAfter Optional. Trigger callback after a certain amount of time
+ *                              without waiting for debounce.
  */
-export const createDebouncer = timeout => {
+export const createDebouncer = (timeout, triggerAfter = -1) => {
   let timer = -1
-  return f => {
+  /**
+   * @type {number?}
+    */
+  let lastCall = null
+  /**
+   * @param {((...args: any)=>void)?} cb function to trigger after debounce. If null, it will reset the
+   *                         debounce.
+   */
+  return cb => {
     clearTimeout(timer)
-    if (f) {
-      timer = /** @type {any} */ (setTimeout(f, timeout))
+    if (cb) {
+      if (triggerAfter >= 0) {
+        const now = time.getUnixTime()
+        if (lastCall === null) lastCall = now
+        if (now - lastCall > triggerAfter) {
+          lastCall = null
+          timer = /** @type {any} */ (setTimeout(cb, 0))
+          return
+        }
+      }
+      timer = /** @type {any} */ (setTimeout(() => { lastCall = null; cb() }, timeout))
+    } else {
+      lastCall = null
     }
   }
 }
diff -pruN 0.2.99-1/eventloop.test.js 0.2.114-1/eventloop.test.js
--- 0.2.99-1/eventloop.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/eventloop.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -81,13 +81,69 @@ export const testIdleCallback = async _t
 export const testDebouncer = async _tc => {
   const debounce = eventloop.createDebouncer(10)
   let calls = 0
+  debounce((_x) => {
+    calls++
+  })
+  debounce((_y, _z) => {
+    calls++
+  })
+  t.assert(calls === 0)
+  await promise.wait(20)
+  t.assert(calls === 1)
+}
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testDebouncerTriggerAfter = async _tc => {
+  const debounce = eventloop.createDebouncer(100, 100)
+  let calls = 0
   debounce(() => {
     calls++
   })
+  await promise.wait(40)
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(30)
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(50)
   debounce(() => {
     calls++
   })
   t.assert(calls === 0)
-  await promise.wait(20)
+  await promise.wait(0)
+  t.assert(calls === 1)
+  await promise.wait(30)
   t.assert(calls === 1)
 }
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testDebouncerClear = async _tc => {
+  const debounce = eventloop.createDebouncer(10)
+  let calls = 0
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(5)
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(5)
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(5)
+  debounce(() => {
+    calls++
+  })
+  await promise.wait(5)
+  debounce(null)
+  t.assert(calls === 0)
+  await promise.wait(30)
+  t.assert(calls === 0)
+}
diff -pruN 0.2.99-1/function.js 0.2.114-1/function.js
--- 0.2.99-1/function.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/function.js	2025-07-16 19:28:19.000000000 +0000
@@ -6,6 +6,7 @@
 
 import * as array from './array.js'
 import * as object from './object.js'
+import * as traits from './traits.js'
 
 /**
  * Calls all functions in `fs` with args. Only throws after all functions were called.
@@ -68,14 +69,14 @@ export const equalityFlat = (a, b) => a
  * @return {boolean}
  */
 export const equalityDeep = (a, b) => {
-  if (a == null || b == null) {
-    return equalityStrict(a, b)
+  if (a === b) {
+    return true
   }
-  if (a.constructor !== b.constructor) {
+  if (a == null || b == null || a.constructor !== b.constructor) {
     return false
   }
-  if (a === b) {
-    return true
+  if (a[traits.EqualityTraitSymbol] != null) {
+    return a[traits.EqualityTraitSymbol](b)
   }
   switch (a.constructor) {
     case ArrayBuffer:
diff -pruN 0.2.99-1/hash/sha256.js 0.2.114-1/hash/sha256.js
--- 0.2.99-1/hash/sha256.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/hash/sha256.js	2025-07-16 19:28:19.000000000 +0000
@@ -117,6 +117,8 @@ class Hasher {
   }
 
   /**
+   * Returns a 32-byte hash.
+   *
    * @param {Uint8Array} data
    */
   digest (data) {
@@ -167,6 +169,8 @@ class Hasher {
 }
 
 /**
+ * Returns a 32-byte hash.
+ *
  * @param {Uint8Array} data
  */
 export const digest = data => new Hasher().digest(data)
diff -pruN 0.2.99-1/object.js 0.2.114-1/object.js
--- 0.2.99-1/object.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/object.js	2025-07-16 19:28:19.000000000 +0000
@@ -21,6 +21,13 @@ export const keys = Object.keys
 
 /**
  * @template V
+ * @param {{[key:string]: V}} obj
+ * @return {Array<V>}
+ */
+export const values = Object.values
+
+/**
+ * @template V
  * @param {{[k:string]:V}} obj
  * @param {function(V,string):any} f
  */
@@ -60,8 +67,9 @@ export const length = obj => keys(obj).l
 export const size = obj => keys(obj).length
 
 /**
- * @param {Object<string,any>} obj
- * @param {function(any,string):boolean} f
+ * @template {{ [key:string|number|symbol]: any }} T
+ * @param {T} obj
+ * @param {(v:T[keyof T],k:keyof T)=>boolean} f
  * @return {boolean}
  */
 export const some = (obj, f) => {
@@ -74,10 +82,10 @@ export const some = (obj, f) => {
 }
 
 /**
- * @param {Object|undefined} obj
+ * @param {Object|null|undefined} obj
  */
 export const isEmpty = obj => {
-  // eslint-disable-next-line
+  // eslint-disable-next-line no-unreachable-loop
   for (const _k in obj) {
     return false
   }
@@ -85,8 +93,9 @@ export const isEmpty = obj => {
 }
 
 /**
- * @param {Object<string,any>} obj
- * @param {function(any,string):boolean} f
+ * @template {{ [key:string|number|symbol]: any }} T
+ * @param {T} obj
+ * @param {(v:T[keyof T],k:keyof T)=>boolean} f
  * @return {boolean}
  */
 export const every = (obj, f) => {
@@ -102,7 +111,7 @@ export const every = (obj, f) => {
  * Calls `Object.prototype.hasOwnProperty`.
  *
  * @param {any} obj
- * @param {string|symbol} key
+ * @param {string|number|symbol} key
  * @return {boolean}
  */
 export const hasProperty = (obj, key) => Object.prototype.hasOwnProperty.call(obj, key)
@@ -137,3 +146,16 @@ export const deepFreeze = (o) => {
   }
   return freeze(o)
 }
+
+/**
+ * Get object property. Create T if property is undefined and set T on object.
+ *
+ * @function
+ * @template {object} KV
+ * @template {keyof KV} [K=keyof KV]
+ * @param {KV} o
+ * @param {K} key
+ * @param {() => KV[K]} createT
+ * @return {KV[K]}
+ */
+export const setIfUndefined = (o, key, createT) => hasProperty(o, key) ? o[key] : (o[key] = createT())
diff -pruN 0.2.99-1/object.test.js 0.2.114-1/object.test.js
--- 0.2.99-1/object.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/object.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -15,6 +15,7 @@ export const testObject = _tc => {
   t.assert(object.equalFlat({ x: undefined }, { x: undefined }), 'flatEqual handles undefined')
   t.assert(!object.equalFlat({ x: undefined }, { y: {} }), 'flatEqual handles undefined')
   t.describe('object.every')
+  // @ts-expect-error k has no overlap with "c"
   t.assert(object.every({ a: 1, b: 3 }, (v, k) => (v % 2) === 1 && k !== 'c'))
   t.assert(!object.every({ a: 1, b: 3, c: 5 }, (v, k) => (v % 2) === 1 && k !== 'c'))
   t.describe('object.some')
@@ -34,6 +35,18 @@ export const testObject = _tc => {
   t.assert(object.length({ x: 1 }) === 1)
   t.assert(object.isEmpty({}))
   t.assert(!object.isEmpty({ a: 3 }))
+  t.assert(object.isEmpty(null))
+  t.assert(object.isEmpty(undefined))
+  /**
+   * @type {Array<string>}
+   */
+  const keys = object.keys({ a: 1, b: 2 })
+  t.compare(keys, ['a', 'b'])
+  /**
+   * @type {Array<number>}
+   */
+  const vals = object.values({ a: 1 })
+  t.compare(vals, [1])
 }
 
 /**
@@ -61,3 +74,22 @@ export const testFreeze = _tc => {
     o2[2].a = 'hello'
   })
 }
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testSetifundefined = _tc => {
+  const o = { a: 42, b: '42' }
+  object.setIfUndefined(o, 'a', () => 43)
+  object.setIfUndefined(o, 'b', () => '43')
+  /**
+   * @type {{ [key: number]: string}}
+   */
+  const o2 = {}
+  object.setIfUndefined(o2, 42, () => '52')
+  /**
+   * @type {{ [key: string]: number}}
+   */
+  const o3 = {}
+  object.setIfUndefined(o3, '42', () => 52)
+}
diff -pruN 0.2.99-1/package-lock.json 0.2.114-1/package-lock.json
--- 0.2.99-1/package-lock.json	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/package-lock.json	2025-07-16 19:28:19.000000000 +0000
@@ -1,28 +1,29 @@
 {
   "name": "lib0",
-  "version": "0.2.99",
+  "version": "0.2.114",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "lib0",
-      "version": "0.2.99",
+      "version": "0.2.114",
       "license": "MIT",
       "dependencies": {
         "isomorphic.js": "^0.2.4"
       },
       "bin": {
+        "0ecdsa-generate-keypair": "bin/0ecdsa-generate-keypair.js",
         "0gentesthtml": "bin/gentesthtml.js",
         "0serve": "bin/0serve.js"
       },
       "devDependencies": {
-        "@types/node": "^18.14.0",
-        "c8": "^7.13.0",
+        "@types/node": "^24.0.14",
+        "c8": "^10.1.3",
         "jsdoc-api": "^8.0.0",
         "jsdoc-plugin-typescript": "^2.2.1",
         "rollup": "^2.42.1",
         "standard": "^17.1.0",
-        "typescript": "^5.0.2"
+        "typescript": "^5.8.3"
       },
       "engines": {
         "node": ">=16"
@@ -54,10 +55,14 @@
       }
     },
     "node_modules/@bcoe/v8-coverage": {
-      "version": "0.2.3",
-      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz",
-      "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
-      "dev": true
+      "version": "1.0.2",
+      "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz",
+      "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=18"
+      }
     },
     "node_modules/@eslint-community/eslint-utils": {
       "version": "4.4.0",
@@ -148,11 +153,115 @@
       "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==",
       "dev": true
     },
+    "node_modules/@isaacs/cliui": {
+      "version": "8.0.2",
+      "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
+      "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "string-width": "^5.1.2",
+        "string-width-cjs": "npm:string-width@^4.2.0",
+        "strip-ansi": "^7.0.1",
+        "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
+        "wrap-ansi": "^8.1.0",
+        "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-regex": {
+      "version": "6.1.0",
+      "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz",
+      "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
+      "version": "6.2.1",
+      "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz",
+      "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==",
+      "dev": true,
+      "license": "MIT",
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/emoji-regex": {
+      "version": "9.2.2",
+      "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
+      "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+      "dev": true,
+      "license": "MIT"
+    },
+    "node_modules/@isaacs/cliui/node_modules/string-width": {
+      "version": "5.1.2",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
+      "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "eastasianwidth": "^0.2.0",
+        "emoji-regex": "^9.2.2",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/strip-ansi": {
+      "version": "7.1.0",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz",
+      "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+      }
+    },
+    "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
+      "version": "8.1.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
+      "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^6.1.0",
+        "string-width": "^5.0.1",
+        "strip-ansi": "^7.0.1"
+      },
+      "engines": {
+        "node": ">=12"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
     "node_modules/@istanbuljs/schema": {
       "version": "0.1.3",
       "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
       "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -229,6 +338,17 @@
         "node": ">= 8"
       }
     },
+    "node_modules/@pkgjs/parseargs": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
+      "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
+      "dev": true,
+      "license": "MIT",
+      "optional": true,
+      "engines": {
+        "node": ">=14"
+      }
+    },
     "node_modules/@types/istanbul-lib-coverage": {
       "version": "2.0.4",
       "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.4.tgz",
@@ -264,10 +384,14 @@
       "dev": true
     },
     "node_modules/@types/node": {
-      "version": "18.15.3",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.3.tgz",
-      "integrity": "sha512-p6ua9zBxz5otCmbpb5D3U4B5Nanw6Pk3PPyX05xnxbB/fRv71N7CPmORg7uAD5P70T0xmx1pzAx/FUfa5X+3cw==",
-      "dev": true
+      "version": "24.0.14",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-24.0.14.tgz",
+      "integrity": "sha512-4zXMWD91vBLGRtHK3YbIoFMia+1nqEz72coM42C5ETjnNCa/heoj7NT1G67iAfOqMmcfhuCZ4uNpyz8EjlAejw==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "undici-types": "~7.8.0"
+      }
     },
     "node_modules/acorn": {
       "version": "8.10.0",
@@ -504,29 +628,37 @@
       }
     },
     "node_modules/c8": {
-      "version": "7.13.0",
-      "resolved": "https://registry.npmjs.org/c8/-/c8-7.13.0.tgz",
-      "integrity": "sha512-/NL4hQTv1gBL6J6ei80zu3IiTrmePDKXKXOTLpHvcIWZTVYQlDhVWjjWvkhICylE8EwwnMVzDZugCvdx0/DIIA==",
+      "version": "10.1.3",
+      "resolved": "https://registry.npmjs.org/c8/-/c8-10.1.3.tgz",
+      "integrity": "sha512-LvcyrOAaOnrrlMpW22n690PUvxiq4Uf9WMhQwNJ9vgagkL/ph1+D4uvjvDA5XCbykrc0sx+ay6pVi9YZ1GnhyA==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "@bcoe/v8-coverage": "^0.2.3",
+        "@bcoe/v8-coverage": "^1.0.1",
         "@istanbuljs/schema": "^0.1.3",
         "find-up": "^5.0.0",
-        "foreground-child": "^2.0.0",
+        "foreground-child": "^3.1.1",
         "istanbul-lib-coverage": "^3.2.0",
-        "istanbul-lib-report": "^3.0.0",
-        "istanbul-reports": "^3.1.4",
-        "rimraf": "^3.0.2",
-        "test-exclude": "^6.0.0",
+        "istanbul-lib-report": "^3.0.1",
+        "istanbul-reports": "^3.1.6",
+        "test-exclude": "^7.0.1",
         "v8-to-istanbul": "^9.0.0",
-        "yargs": "^16.2.0",
-        "yargs-parser": "^20.2.9"
+        "yargs": "^17.7.2",
+        "yargs-parser": "^21.1.1"
       },
       "bin": {
         "c8": "bin/c8.js"
       },
       "engines": {
-        "node": ">=10.12.0"
+        "node": ">=18"
+      },
+      "peerDependencies": {
+        "monocart-coverage-reports": "^2"
+      },
+      "peerDependenciesMeta": {
+        "monocart-coverage-reports": {
+          "optional": true
+        }
       }
     },
     "node_modules/cache-point": {
@@ -603,14 +735,18 @@
       }
     },
     "node_modules/cliui": {
-      "version": "7.0.4",
-      "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
-      "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
+      "version": "8.0.1",
+      "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+      "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "string-width": "^4.2.0",
-        "strip-ansi": "^6.0.0",
+        "strip-ansi": "^6.0.1",
         "wrap-ansi": "^7.0.0"
+      },
+      "engines": {
+        "node": ">=12"
       }
     },
     "node_modules/collect-all": {
@@ -657,10 +793,11 @@
       "dev": true
     },
     "node_modules/cross-spawn": {
-      "version": "7.0.3",
-      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
-      "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+      "version": "7.0.6",
+      "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+      "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
         "path-key": "^3.1.0",
         "shebang-command": "^2.0.0",
@@ -721,11 +858,19 @@
         "node": ">=6.0.0"
       }
     },
+    "node_modules/eastasianwidth": {
+      "version": "0.2.0",
+      "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
+      "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/emoji-regex": {
       "version": "8.0.0",
       "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
       "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/entities": {
       "version": "2.1.0",
@@ -834,10 +979,11 @@
       }
     },
     "node_modules/escalade": {
-      "version": "3.1.1",
-      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz",
-      "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==",
+      "version": "3.2.0",
+      "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+      "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=6"
       }
@@ -1442,16 +1588,20 @@
       }
     },
     "node_modules/foreground-child": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-2.0.0.tgz",
-      "integrity": "sha512-dCIq9FpEcyQyXKCkyzmlPTFNgrCzPudOe+mhvJU5zAtlBnGVy2yKxtfsxK2tQBThwq225jcvBjpw1Gr40uzZCA==",
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
+      "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
-        "cross-spawn": "^7.0.0",
-        "signal-exit": "^3.0.2"
+        "cross-spawn": "^7.0.6",
+        "signal-exit": "^4.0.1"
       },
       "engines": {
-        "node": ">=8.0.0"
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/fs-then-native": {
@@ -1521,6 +1671,7 @@
       "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
       "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": "6.* || 8.* || >= 10.*"
       }
@@ -1739,7 +1890,8 @@
       "version": "2.0.2",
       "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
       "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==",
-      "dev": true
+      "dev": true,
+      "license": "MIT"
     },
     "node_modules/ignore": {
       "version": "5.2.4",
@@ -1906,6 +2058,7 @@
       "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
       "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=8"
       }
@@ -2063,33 +2216,36 @@
       }
     },
     "node_modules/istanbul-lib-coverage": {
-      "version": "3.2.0",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
-      "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==",
+      "version": "3.2.2",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz",
+      "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "engines": {
         "node": ">=8"
       }
     },
     "node_modules/istanbul-lib-report": {
-      "version": "3.0.0",
-      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz",
-      "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==",
+      "version": "3.0.1",
+      "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz",
+      "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "istanbul-lib-coverage": "^3.0.0",
-        "make-dir": "^3.0.0",
+        "make-dir": "^4.0.0",
         "supports-color": "^7.1.0"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
       }
     },
     "node_modules/istanbul-reports": {
-      "version": "3.1.5",
-      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.5.tgz",
-      "integrity": "sha512-nUsEMa9pBt/NOHqbcbeJEgqIlY/K7rVWUX6Lql2orY5e9roQOthbR3vtY4zzf2orPELg80fnxxk9zUyPlgwD1w==",
+      "version": "3.1.7",
+      "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz",
+      "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==",
       "dev": true,
+      "license": "BSD-3-Clause",
       "dependencies": {
         "html-escaper": "^2.0.0",
         "istanbul-lib-report": "^3.0.0"
@@ -2098,6 +2254,22 @@
         "node": ">=8"
       }
     },
+    "node_modules/jackspeak": {
+      "version": "3.4.3",
+      "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
+      "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "@isaacs/cliui": "^8.0.2"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      },
+      "optionalDependencies": {
+        "@pkgjs/parseargs": "^0.11.0"
+      }
+    },
     "node_modules/js-tokens": {
       "version": "4.0.0",
       "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
@@ -2336,20 +2508,34 @@
       }
     },
     "node_modules/make-dir": {
-      "version": "3.1.0",
-      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz",
-      "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==",
+      "version": "4.0.0",
+      "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz",
+      "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "semver": "^6.0.0"
+        "semver": "^7.5.3"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=10"
       },
       "funding": {
         "url": "https://github.com/sponsors/sindresorhus"
       }
     },
+    "node_modules/make-dir/node_modules/semver": {
+      "version": "7.7.2",
+      "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
+      "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
+      "dev": true,
+      "license": "ISC",
+      "bin": {
+        "semver": "bin/semver.js"
+      },
+      "engines": {
+        "node": ">=10"
+      }
+    },
     "node_modules/markdown-it": {
       "version": "12.3.2",
       "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz",
@@ -2415,6 +2601,16 @@
         "url": "https://github.com/sponsors/ljharb"
       }
     },
+    "node_modules/minipass": {
+      "version": "7.1.2",
+      "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
+      "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      }
+    },
     "node_modules/mkdirp": {
       "version": "1.0.4",
       "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
@@ -2637,6 +2833,13 @@
         "node": ">=6"
       }
     },
+    "node_modules/package-json-from-dist": {
+      "version": "1.0.1",
+      "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
+      "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
+      "dev": true,
+      "license": "BlueOak-1.0.0"
+    },
     "node_modules/parent-module": {
       "version": "1.0.1",
       "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
@@ -2695,6 +2898,30 @@
       "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
       "dev": true
     },
+    "node_modules/path-scurry": {
+      "version": "1.11.1",
+      "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
+      "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+      "dev": true,
+      "license": "BlueOak-1.0.0",
+      "dependencies": {
+        "lru-cache": "^10.2.0",
+        "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.18"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/path-scurry/node_modules/lru-cache": {
+      "version": "10.4.3",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
+      "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
+      "dev": true,
+      "license": "ISC"
+    },
     "node_modules/pify": {
       "version": "4.0.1",
       "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz",
@@ -2867,6 +3094,7 @@
       "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
       "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
       "dev": true,
+      "license": "MIT",
       "engines": {
         "node": ">=0.10.0"
       }
@@ -3028,10 +3256,17 @@
       }
     },
     "node_modules/signal-exit": {
-      "version": "3.0.7",
-      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz",
-      "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
-      "dev": true
+      "version": "4.1.0",
+      "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+      "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+      "dev": true,
+      "license": "ISC",
+      "engines": {
+        "node": ">=14"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
     },
     "node_modules/standard": {
       "version": "17.1.0",
@@ -3137,6 +3372,23 @@
       "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
       "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "emoji-regex": "^8.0.0",
+        "is-fullwidth-code-point": "^3.0.0",
+        "strip-ansi": "^6.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
+    "node_modules/string-width-cjs": {
+      "name": "string-width",
+      "version": "4.2.3",
+      "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+      "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "emoji-regex": "^8.0.0",
         "is-fullwidth-code-point": "^3.0.0",
@@ -3222,6 +3474,20 @@
         "node": ">=8"
       }
     },
+    "node_modules/strip-ansi-cjs": {
+      "name": "strip-ansi",
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+      "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-regex": "^5.0.1"
+      },
+      "engines": {
+        "node": ">=8"
+      }
+    },
     "node_modules/strip-bom": {
       "version": "3.0.0",
       "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -3274,17 +3540,65 @@
       "dev": true
     },
     "node_modules/test-exclude": {
-      "version": "6.0.0",
-      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz",
-      "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==",
+      "version": "7.0.1",
+      "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.1.tgz",
+      "integrity": "sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==",
       "dev": true,
+      "license": "ISC",
       "dependencies": {
         "@istanbuljs/schema": "^0.1.2",
-        "glob": "^7.1.4",
-        "minimatch": "^3.0.4"
+        "glob": "^10.4.1",
+        "minimatch": "^9.0.4"
       },
       "engines": {
-        "node": ">=8"
+        "node": ">=18"
+      }
+    },
+    "node_modules/test-exclude/node_modules/brace-expansion": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
+      "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
+      "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "balanced-match": "^1.0.0"
+      }
+    },
+    "node_modules/test-exclude/node_modules/glob": {
+      "version": "10.4.5",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
+      "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "foreground-child": "^3.1.0",
+        "jackspeak": "^3.1.2",
+        "minimatch": "^9.0.4",
+        "minipass": "^7.1.2",
+        "package-json-from-dist": "^1.0.0",
+        "path-scurry": "^1.11.1"
+      },
+      "bin": {
+        "glob": "dist/esm/bin.mjs"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
+      }
+    },
+    "node_modules/test-exclude/node_modules/minimatch": {
+      "version": "9.0.5",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
+      "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
+      "dev": true,
+      "license": "ISC",
+      "dependencies": {
+        "brace-expansion": "^2.0.1"
+      },
+      "engines": {
+        "node": ">=16 || 14 >=14.17"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/isaacs"
       }
     },
     "node_modules/text-table": {
@@ -3344,16 +3658,17 @@
       }
     },
     "node_modules/typescript": {
-      "version": "5.0.2",
-      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.0.2.tgz",
-      "integrity": "sha512-wVORMBGO/FAs/++blGNeAVdbNKtIh1rbBL2EyQ1+J9lClJ93KiiKe8PmFIVdXhHcyv44SL9oglmfeSsndo0jRw==",
+      "version": "5.8.3",
+      "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz",
+      "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==",
       "dev": true,
+      "license": "Apache-2.0",
       "bin": {
         "tsc": "bin/tsc",
         "tsserver": "bin/tsserver"
       },
       "engines": {
-        "node": ">=12.20"
+        "node": ">=14.17"
       }
     },
     "node_modules/typical": {
@@ -3389,6 +3704,13 @@
       "integrity": "sha512-+A5Sja4HP1M08MaXya7p5LvjuM7K6q/2EaC0+iovj/wOcMsTzMvDFbasi/oSapiwOlt252IqsKqPjCl7huKS0A==",
       "dev": true
     },
+    "node_modules/undici-types": {
+      "version": "7.8.0",
+      "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.8.0.tgz",
+      "integrity": "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw==",
+      "dev": true,
+      "license": "MIT"
+    },
     "node_modules/uri-js": {
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
@@ -3486,6 +3808,26 @@
       "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
       "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
       "dev": true,
+      "license": "MIT",
+      "dependencies": {
+        "ansi-styles": "^4.0.0",
+        "string-width": "^4.1.0",
+        "strip-ansi": "^6.0.0"
+      },
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+      }
+    },
+    "node_modules/wrap-ansi-cjs": {
+      "name": "wrap-ansi",
+      "version": "7.0.0",
+      "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+      "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+      "dev": true,
+      "license": "MIT",
       "dependencies": {
         "ansi-styles": "^4.0.0",
         "string-width": "^4.1.0",
@@ -3524,6 +3866,7 @@
       "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
       "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
       "dev": true,
+      "license": "ISC",
       "engines": {
         "node": ">=10"
       }
@@ -3535,30 +3878,32 @@
       "dev": true
     },
     "node_modules/yargs": {
-      "version": "16.2.0",
-      "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
-      "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
+      "version": "17.7.2",
+      "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+      "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
       "dev": true,
+      "license": "MIT",
       "dependencies": {
-        "cliui": "^7.0.2",
+        "cliui": "^8.0.1",
         "escalade": "^3.1.1",
         "get-caller-file": "^2.0.5",
         "require-directory": "^2.1.1",
-        "string-width": "^4.2.0",
+        "string-width": "^4.2.3",
         "y18n": "^5.0.5",
-        "yargs-parser": "^20.2.2"
+        "yargs-parser": "^21.1.1"
       },
       "engines": {
-        "node": ">=10"
+        "node": ">=12"
       }
     },
     "node_modules/yargs-parser": {
-      "version": "20.2.9",
-      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
-      "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
+      "version": "21.1.1",
+      "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+      "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
       "dev": true,
+      "license": "ISC",
       "engines": {
-        "node": ">=10"
+        "node": ">=12"
       }
     },
     "node_modules/yocto-queue": {
diff -pruN 0.2.99-1/package.json 0.2.114-1/package.json
--- 0.2.99-1/package.json	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/package.json	2025-07-16 19:28:19.000000000 +0000
@@ -1,6 +1,6 @@
 {
   "name": "lib0",
-  "version": "0.2.99",
+  "version": "0.2.114",
   "description": "",
   "sideEffects": false,
   "type": "module",
@@ -75,9 +75,9 @@
     "./conditions.js": "./conditions.js",
     "./dist/conditions.cjs": "./dist/conditions.cjs",
     "./conditions": {
-      "types": "./condititons.d.ts",
-      "module": "./condititons.js",
-      "import": "./condititons.js",
+      "types": "./conditions.d.ts",
+      "module": "./conditions.js",
+      "import": "./conditions.js",
       "require": "./dist/conditions.cjs"
     },
     "./crypto/jwt": {
@@ -135,6 +135,12 @@
       "import": "./decoding.js",
       "require": "./dist/decoding.cjs"
     },
+    "./diff/patience": {
+      "types": "./diff/patience.d.ts",
+      "module": "./diff/patience.js",
+      "import": "./diff/patience.js",
+      "require": "./dist/patience.cjs"
+    },
     "./diff.js": "./diff.js",
     "./dist/diff.cjs": "./dist/diff.cjs",
     "./diff": {
@@ -397,6 +403,14 @@
       "import": "./symbol.js",
       "require": "./dist/symbol.cjs"
     },
+    "./traits.js": "./traits.js",
+    "./dist/traits.cjs": "./dist/traits.cjs",
+    "./traits": {
+      "types": "./traits.d.ts",
+      "module": "./traits.js",
+      "import": "./traits.js",
+      "require": "./dist/traits.cjs"
+    },
     "./testing.js": "./testing.js",
     "./dist/testing.cjs": "./dist/testing.cjs",
     "./testing": {
@@ -479,28 +493,34 @@
         "require": "./dist/performance.cjs",
         "default": "./performance.js"
       }
+    },
+    "./schema": {
+      "types": "./schema.d.ts",
+      "module": "./schema.js",
+      "import": "./schema.js",
+      "require": "./dist/schema.cjs"
     }
   },
   "dependencies": {
     "isomorphic.js": "^0.2.4"
   },
   "devDependencies": {
-    "@types/node": "^18.14.0",
-    "c8": "^7.13.0",
+    "@types/node": "^24.0.14",
+    "c8": "^10.1.3",
     "jsdoc-api": "^8.0.0",
     "jsdoc-plugin-typescript": "^2.2.1",
     "rollup": "^2.42.1",
     "standard": "^17.1.0",
-    "typescript": "^5.0.2"
+    "typescript": "^5.8.3"
   },
   "scripts": {
     "clean": "rm -rf dist *.d.ts */*.d.ts *.d.ts.map */*.d.ts.map",
     "types": "tsc --outDir .",
     "dist": "rollup -c",
     "debug": "npm run gentesthtml && node ./bin/0serve.js -o test.html",
-    "test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node --unhandled-rejections=strict ./test.js --repetition-time 50 --production",
-    "test-inspect": "node --inspect-brk --unhandled-rejections=strict ./test.js --repetition-time 50 --production",
-    "test-extensive": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node test.js --repetition-time 30000 --extensive --production",
+    "test": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node --unhandled-rejections=strict ./test.js --repetition-time 50",
+    "test-inspect": "node --inspect-brk --unhandled-rejections=strict ./test.js --repetition-time 50",
+    "test-extensive": "c8 --check-coverage --lines 100 --branches 100 --functions 100 --statements 100 node test.js --repetition-time 30000 --extensive",
     "trace-deopt": "clear && rollup -c  && node --trace-deopt dist/test.cjs",
     "trace-opt": "clear && rollup -c  && node --trace-opt dist/test.cjs",
     "lint": "standard && tsc",
diff -pruN 0.2.99-1/prng.test.js 0.2.114-1/prng.test.js
--- 0.2.99-1/prng.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/prng.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -6,7 +6,7 @@ import * as t from './testing.js'
 import { Xorshift32 } from './prng/Xorshift32.js'
 import { Mt19937 } from './prng/Mt19937.js'
 import * as dom from './dom.js'
-import { isBrowser, production } from './environment.js'
+import { isBrowser } from './environment.js'
 import * as math from './math.js'
 
 const genTestData = 5000
@@ -192,7 +192,6 @@ export const testGeneratorXoroshiro128pl
  * @param {t.TestCase} tc
  */
 export const testGeneratorXorshift32 = tc => {
-  t.skip(!production)
   runGenTest(tc, new Xorshift32(tc.seed))
 }
 
@@ -200,7 +199,6 @@ export const testGeneratorXorshift32 = t
  * @param {t.TestCase} tc
  */
 export const testGeneratorMt19937 = tc => {
-  t.skip(!production)
   runGenTest(tc, new Mt19937(tc.seed))
 }
 
diff -pruN 0.2.99-1/random.test.js 0.2.114-1/random.test.js
--- 0.2.99-1/random.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/random.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -80,7 +80,6 @@ export const testUuidv4 = tc => {
  * @param {t.TestCase} tc
  */
 export const testUuidv4Overlaps = tc => {
-  t.skip(!t.production)
   const iterations = t.extensive ? 1000000 : 10000
   const uuids = new Set()
   for (let i = 0; i < iterations; i++) {
diff -pruN 0.2.99-1/rollup.config.js 0.2.114-1/rollup.config.js
--- 0.2.99-1/rollup.config.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/rollup.config.js	2025-07-16 19:28:19.000000000 +0000
@@ -1,6 +1,6 @@
 import * as fs from 'fs'
 
-const roots = ['./', './crypto/', './hash/']
+const roots = ['./', './crypto/', './hash/', './diff/']
 
 const files = roots.map(root => fs.readdirSync(root).map(f => root + f)).flat().filter(file => /(?<!(test|config))\.js$/.test(file))
 console.log(files)
@@ -14,5 +14,5 @@ export default [{
     entryFileNames: '[name].cjs',
     chunkFileNames: '[name]-[hash].cjs'
   },
-  external: ['isomorphic.js', 'node:crypto', 'lib0/webcrypto', 'lib0/performance', 'perf_hooks', 'isomorphic-webcrypto', 'node:perf_hooks', 'lib0/logging']
+  external: ['isomorphic.js', 'node:crypto', 'lib0/webcrypto', 'lib0/performance', 'perf_hooks', 'isomorphic-webcrypto/src/react-native', 'node:perf_hooks', 'lib0/logging']
 }]
diff -pruN 0.2.99-1/schema.js 0.2.114-1/schema.js
--- 0.2.99-1/schema.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/schema.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,533 @@
+/**
+ * @experimental WIP
+ *
+ * Simple & efficient schemas for your data.
+ */
+
+import * as obj from './object.js'
+import * as arr from './array.js'
+import * as error from './error.js'
+import * as env from './environment.js'
+
+/**
+ * @typedef {string|number|bigint|boolean|null|undefined} LiteralType
+ */
+
+/**
+ * @typedef {{ [k:string|number|symbol]: any }} AnyObject
+ */
+
+/**
+ * @template T
+ * @typedef {T extends $Schema<infer X> ? X : T} Unwrap
+ */
+
+/**
+ * @template {readonly unknown[]} T
+ * @typedef {T extends readonly [$Schema<infer First>, ...infer Rest] ? [First, ...UnwrapArray<Rest>] : [] } UnwrapArray
+ */
+
+/**
+ * @template T
+ * @typedef {T extends $Schema<infer S> ? $Schema<S> : never} CastToSchema
+ */
+
+/**
+ * @template {unknown[]} Arr
+ * @typedef {Arr extends [...unknown[], infer L] ? L : never} TupleLast
+ */
+
+/**
+ * @template {unknown[]} Arr
+ * @typedef {Arr extends [...infer Fs, unknown] ? Fs : never} TuplePop
+ */
+
+/**
+ * @template {readonly unknown[]} T
+ * @typedef {T extends []
+ *   ? {}
+ *   : T extends [infer First]
+ *   ? First
+ *   : T extends [infer First, ...infer Rest]
+ *   ? First & Intersect<Rest>
+ *   : never
+ * } Intersect
+ */
+
+const schemaSymbol = Symbol('0schema')
+
+/**
+ * @template T
+ */
+export class $Schema {
+  get [schemaSymbol] () { return true }
+  /**
+   * Use `schema.validate(obj)` with a typed parameter that is already of typed to be an instance of
+   * Schema. Validate will check the structure of the parameter and return true iff the instance
+   * really is an instance of Schema.
+   *
+   * @param {T} o
+   * @return {boolean}
+   */
+  validate (o) {
+    return this.check(o)
+  }
+
+  /* c8 ignore start */
+  /**
+   * Similar to validate, but this method accepts untyped parameters.
+   *
+   * @param {any} _o
+   * @return {_o is T}
+   */
+  check (_o) {
+    error.methodUnimplemented()
+  }
+  /* c8 ignore stop */
+
+  /**
+   * @type {$Schema<T?>}
+   */
+  get nullable () {
+    return union(this, $null)
+  }
+
+  /**
+   * @type {$Optional<$Schema<T>>}
+   */
+  get optional () {
+    return new $Optional(/** @type {$Schema<T>} */ (this))
+  }
+
+  /**
+   * Cast a variable to a specific type. Returns the casted value, or throws an exception otherwise.
+   * Use this if you know that the type is of a specific type and you just want to convince the type
+   * system.
+   *
+   * **Do not rely on these error messages!**
+   * Performs an assertion check only if not in a production environment.
+   *
+   * @param {any} o
+   * @return {o extends T ? T : never}
+   */
+  cast (o) {
+    assert(o, this)
+    return o
+  }
+
+  /**
+   * Ensures that a variable is a a specific type. Returns the value, or throws an exception if the assertion check failed.
+   * Use this if you know that the type is of a specific type and you just want to convince the type
+   * system.
+   *
+   * Can be useful when defining lambdas: `s.lambda(s.$number, s.$void).ensure((n) => n + 1)`
+   *
+   * **Do not rely on these error messages!**
+   * Performs an assertion check if not in a production environment.
+   *
+   * @param {T} o
+   * @return {o extends T ? T : never}
+   */
+  ensure (o) {
+    assert(o, this)
+    return o
+  }
+}
+
+/**
+ * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C
+ * @extends {$Schema<C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType<C> : never)>}
+ */
+export class $ConstructedBy extends $Schema {
+  /**
+   * @param {C} c
+   */
+  constructor (c) {
+    super()
+    this.v = c
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is C extends ((...args:any[]) => infer T) ? T : (C extends (new (...args:any[]) => any) ? InstanceType<C> : never)} o
+   */
+  check (o) {
+    return o?.constructor === this.v
+  }
+}
+
+/**
+ * @template {(new (...args:any[]) => any) | ((...args:any[]) => any)} C
+ * @param {C} c
+ * @return {CastToSchema<$ConstructedBy<C>>}
+ */
+export const constructedBy = c => new $ConstructedBy(c)
+
+/**
+ * @template {LiteralType} T
+ * @extends {$Schema<T>}
+ */
+export class $Literal extends $Schema {
+  /**
+   * @param {Array<T>} literals
+   */
+  constructor (literals) {
+    super()
+    this.v = literals
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is T}
+   */
+  check (o) {
+    return this.v.some(a => a === o)
+  }
+}
+
+/**
+ * @template {LiteralType[]} T
+ * @param {T} literals
+ * @return {CastToSchema<$Literal<T[number]>>}
+ */
+export const literal = (...literals) => new $Literal(literals)
+
+const isOptionalSymbol = Symbol('optional')
+/**
+ * @template {$Schema<any>} S
+ * @extends $Schema<Unwrap<S>|undefined>
+ */
+class $Optional extends $Schema {
+  /**
+   * @param {S} s
+   */
+  constructor (s) {
+    super()
+    this.s = s
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is (Unwrap<S>|undefined)}
+   */
+  check (o) {
+    return o === undefined || this.s.check(o)
+  }
+
+  get [isOptionalSymbol] () { return true }
+}
+
+/**
+ * @template {{ [key: string|symbol|number]: $Schema<any> }} S
+ * @typedef {{ [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? Key : never]?: S[Key] extends $Optional<$Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? never : Key]: S[Key] extends $Schema<infer Type> ? Type : never }} $ObjectToType
+ */
+
+/**
+ * @template {{[key:string|symbol|number]: $Schema<any>}} S
+ * @extends {$Schema<$ObjectToType<S>>}
+ */
+export class $Object extends $Schema {
+  /**
+   * @param {S} v
+   */
+  constructor (v) {
+    super()
+    this.v = v
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is $ObjectToType<S>}
+   */
+  check (o) {
+    return o != null && obj.every(this.v, (vv, vk) => vv.check(o[vk]))
+  }
+}
+
+// I used an explicit type annotation instead of $ObjectToType, so that the user doesn't see the
+// weird type definitions when inspecting type definions.
+/**
+ * @template {{ [key:string|symbol|number]: $Schema<any> }} S
+ * @param {S} def
+ * @return {$Schema<{ [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? Key : never]?: S[Key] extends $Optional<$Schema<infer Type>> ? Type : never } & { [Key in keyof S as S[Key] extends $Optional<$Schema<any>> ? never : Key]: S[Key] extends $Schema<infer Type> ? Type : never }>}
+ */
+export const object = def => /** @type {any} */ (new $Object(def))
+
+/**
+ * @template {$Schema<string|number|symbol>} Keys
+ * @template {$Schema<any>} Values
+ * @extends {$Schema<Record<Keys extends $Schema<infer K> ? K : never,Values extends $Schema<infer T> ? T : never>>}
+ */
+export class $Record extends $Schema {
+  /**
+   * @param {Keys} keys
+   * @param {Values} values
+   */
+  constructor (keys, values) {
+    super()
+    this.keys = keys
+    this.values = values
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is Record<Keys extends $Schema<infer K> ? K : never,Values extends $Schema<infer T> ? T : never>}
+   */
+  check (o) {
+    return o != null && obj.every(o, (vv, vk) => this.keys.check(vk) && this.values.check(vv))
+  }
+}
+
+/**
+ * @template {$Schema<string|number|symbol>} Keys
+ * @template {$Schema<any>} Values
+ * @param {Keys} keys
+ * @param {Values} values
+ * @return {CastToSchema<$Record<Keys,Values>>}
+ */
+export const record = (keys, values) => new $Record(keys, values)
+
+/**
+ * @template {$Schema<any>[]} S
+ * @extends {$Schema<{ [Key in keyof S]: S[Key] extends $Schema<infer Type> ? Type : never }>}
+ */
+export class $Tuple extends $Schema {
+  /**
+   * @param {S} v
+   */
+  constructor (v) {
+    super()
+    this.v = v
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is { [K in keyof S]: S[K] extends $Schema<infer Type> ? Type : never }}
+   */
+  check (o) {
+    return o != null && obj.every(this.v, (vv, vk) => /** @type {$Schema<any>} */ (vv).check(o[vk]))
+  }
+}
+
+/**
+ * @template {Array<$Schema<any>>} T
+ * @param {T} def
+ * @return {CastToSchema<$Tuple<T>>}
+ */
+export const tuple = (...def) => new $Tuple(def)
+
+/**
+ * @template {$Schema<any>} S
+ * @extends {$Schema<Array<S extends $Schema<infer T> ? T : never>>}
+ */
+export class $Array extends $Schema {
+  /**
+   * @param {Array<S>} v
+   */
+  constructor (v) {
+    super()
+    /**
+     * @type {$Schema<S extends $Schema<infer T> ? T : never>}
+     */
+    this.v = v.length === 1 ? v[0] : new $Union(v)
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is Array<S extends $Schema<infer T> ? T : never>} o
+   */
+  check (o) {
+    return arr.isArray(o) && arr.every(o, oi => this.v.check(oi))
+  }
+}
+
+/**
+ * @template {Array<$Schema<any>>} T
+ * @param {T} def
+ * @return {$Schema<Array<T extends Array<$Schema<infer S>> ? S : never>>}
+ */
+export const array = (...def) => new $Array(def)
+
+/**
+ * @template T
+ * @extends {$Schema<T>}
+ */
+export class $InstanceOf extends $Schema {
+  /**
+   * @param {new (...args:any) => T} constructor
+   */
+  constructor (constructor) {
+    super()
+    this.v = constructor
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is T}
+   */
+  check (o) {
+    return o instanceof this.v
+  }
+}
+
+/**
+ * @template T
+ * @param {new (...args:any) => T} c
+ * @return {$Schema<T>}
+ */
+export const instance = c => new $InstanceOf(c)
+
+/**
+ * @template {$Schema<any>[]} Args
+ * @typedef {(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>} _LArgsToLambdaDef
+ */
+
+/**
+ * @template {Array<$Schema<any>>} Args
+ * @extends {$Schema<_LArgsToLambdaDef<Args>>}
+ */
+export class $Lambda extends $Schema {
+  /**
+   * @param {Args} args
+   */
+  constructor (args) {
+    super()
+    this.len = args.length - 1
+    this.args = tuple(...args.slice(-1))
+    this.res = args[this.len]
+  }
+
+  /**
+   * @param {any} f
+   * @return {f is _LArgsToLambdaDef<Args>}
+   */
+  check (f) {
+    return f.constructor === Function && f.length <= this.len
+  }
+}
+
+/**
+ * @template {$Schema<any>[]} Args
+ * @param {Args} args
+ * @return {$Schema<(...args:UnwrapArray<TuplePop<Args>>)=>Unwrap<TupleLast<Args>>>}
+ */
+export const lambda = (...args) => new $Lambda(args.length > 0 ? args : [$void])
+
+/**
+ * @template {Array<$Schema<any>>} T
+ * @extends {$Schema<Intersect<UnwrapArray<T>>>}
+ */
+export class $Intersection extends $Schema {
+  /**
+   * @param {T} v
+   */
+  constructor (v) {
+    super()
+    /**
+     * @type {T}
+     */
+    this.v = v
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is Intersect<UnwrapArray<T>>}
+   */
+  check (o) {
+    // @ts-ignore
+    return arr.every(this.v, check => check.check(o))
+  }
+}
+
+/**
+ * @template {$Schema<any>[]} T
+ * @param {T} def
+ * @return {CastToSchema<$Intersection<T>>}
+ */
+export const intersect = (...def) => new $Intersection(def)
+
+/**
+ * @template S
+ * @extends {$Schema<S>}
+ */
+export class $Union extends $Schema {
+  /**
+   * @param {Array<$Schema<S>>} v
+   */
+  constructor (v) {
+    super()
+    this.v = v
+  }
+
+  /**
+   * @param {any} o
+   * @return {o is S}
+   */
+  check (o) {
+    return arr.some(this.v, (vv) => vv.check(o))
+  }
+
+  static schema = constructedBy($Union)
+}
+
+/**
+ * @template {Array<$Schema<any>>} T
+ * @param {T} def
+ * @return {CastToSchema<$Union<T extends [] ? never : (T extends Array<$Schema<infer S>> ? S : never)>>}
+ */
+export const union = (...def) => $Union.schema.check(def[0]) ? new $Union([...def[0].v, ...def.slice(1)]) : new $Union(def)
+
+/**
+ * @type {$Schema<any>}
+ */
+export const any = intersect()
+
+/**
+ * @type {$Schema<bigint>}
+ */
+export const bigint = constructedBy(BigInt)
+
+/**
+ * @type {$Schema<Symbol>}
+ */
+export const symbol = constructedBy(Symbol)
+
+/**
+ * @type {$Schema<number>}
+ */
+export const number = constructedBy(Number)
+
+/**
+ * @type {$Schema<string>}
+ */
+export const string = constructedBy(String)
+
+/**
+ * @type {$Schema<undefined>}
+ */
+const $undefined = literal(undefined)
+
+export { $undefined as undefined }
+
+/**
+ * @type {$Schema<void>}
+ */
+export const $void = literal(undefined)
+
+export const $null = /** @type {$Schema<null>} */ (literal(null))
+
+/* c8 ignore start */
+/**
+ * Assert that a variable is of this specific type.
+ * The assertion check is only performed in non-production environments.
+ *
+ * @type {<T>(o:any,schema:$Schema<T>) => asserts o is T}
+ */
+export const assert = env.production
+  ? () => {}
+  : (o, schema) => {
+      if (!schema.check(o)) {
+        throw error.create(`Expected value to be of type ${schema.constructor.name}.`)
+      }
+    }
+/* c8 ignore end */
diff -pruN 0.2.99-1/schema.test.js 0.2.114-1/schema.test.js
--- 0.2.99-1/schema.test.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/schema.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,253 @@
+import * as t from './testing.js'
+import * as s from './schema.js'
+import * as env from './environment.js'
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testSchemas = _tc => {
+  t.group('number', () => {
+    t.assert(s.number.validate(42))
+    // @ts-expect-error
+    t.assert(!s.number.validate(BigInt(42)))
+    // @ts-expect-error
+    t.assert(!s.number.validate(undefined))
+    // @ts-expect-error
+    t.assert(!s.number.validate(new Date()))
+  })
+  t.group('bigint', () => {
+    t.assert(s.bigint.validate(BigInt(42)))
+    // @ts-expect-error
+    t.assert(!s.bigint.validate([BigInt(42)]))
+    // @ts-expect-error
+    t.assert(!s.bigint.validate(undefined))
+    // @ts-expect-error
+    t.assert(!s.bigint.validate(new Date()))
+  })
+  t.group('symbol', () => {
+    t.assert(s.symbol.validate(Symbol('random symbol')))
+    // @ts-expect-error
+    t.assert(!s.symbol.validate({}))
+    // @ts-expect-error
+    t.assert(!s.symbol.validate(undefined))
+    // @ts-expect-error
+    t.assert(!s.symbol.validate(new Date()))
+  })
+  t.group('literal', () => {
+    const myliterals = s.literal('hi', 4)
+    myliterals.validate('hi')
+    // @ts-expect-error
+    t.assert(!myliterals.validate(undefined))
+    // @ts-expect-error
+    t.assert(!myliterals.validate(new Date()))
+  })
+  t.group('object', () => {
+    const myobject = s.object({
+      num: s.number
+    })
+    const q = /** @type {number} */ (/** @type {any} */ ({ num: 42, x: 9 }))
+    if (myobject.check(q)) {
+      s.number.validate(q)
+      myobject.validate(q)
+    } else {
+      // q is a number now
+      s.number.validate(q)
+    }
+    t.assert(myobject.check({ num: 42, x: 9 }))
+    // @ts-expect-error
+    t.assert(!myobject.validate(undefined))
+    // @ts-expect-error
+    t.assert(!myobject.validate(new Date()))
+  })
+  t.group('record', () => {
+    const myrecord = s.record(s.number, s.string)
+    // @ts-expect-error
+    t.assert(!myrecord.validate({ a: 'a' }))
+    const myrecord2 = s.record(s.string, s.number)
+    const o = { a: 42 }
+    t.assert(myrecord2.validate(o))
+  })
+  t.group('tuple', () => {
+    const mytuple = s.tuple(s.number, s.string)
+    t.assert(mytuple.validate([4, '5']))
+    // @ts-expect-error
+    t.assert(mytuple.validate([4, '5', 6]))
+    // @ts-expect-error
+    t.assert(!mytuple.validate(['4', 5]))
+    // @ts-expect-error
+    t.assert(!mytuple.validate(undefined))
+    // @ts-expect-error
+    t.assert(!mytuple.validate(new Date()))
+  })
+  t.group('instance', () => {
+    class Base { x () {} }
+    class BetterBase extends Base {
+      y () {}
+    }
+    class BetterBetterBase extends BetterBase { }
+    const z = s.instance(Base)
+    t.assert(z.validate(new Base()))
+    t.assert(z.validate(new BetterBase()))
+    // @ts-expect-error
+    t.assert(!z.validate(4))
+    t.assert(!s.instance(BetterBetterBase).validate(new BetterBase()))
+    // @ts-expect-error
+    t.assert(!z.validate(undefined))
+    // @ts-expect-error
+    t.assert(!z.validate(new Date()))
+  })
+  t.group('string', () => {
+    class BetterString extends String { }
+    // @ts-expect-error
+    t.assert(!s.string.validate(new BetterString()))
+    t.assert(s.string.validate('hi'))
+    // @ts-expect-error
+    t.assert(!s.string.validate(undefined))
+    // @ts-expect-error
+    t.assert(!s.string.validate(new Date()))
+  })
+  t.group('array', () => {
+    const myarray = s.array(s.number, s.string)
+    t.assert(myarray.validate([4, '5']))
+    t.assert(myarray.validate(['4', 5]))
+    // @ts-expect-error
+    t.assert(!myarray.validate(['x', new Date()]))
+    // @ts-expect-error
+    t.assert(!myarray.validate(undefined))
+    // @ts-expect-error
+    t.assert(!myarray.validate(new Date()))
+    {
+      const mysimplearray = s.array(s.object({}))
+      // @ts-expect-error
+      if (env.production) t.fails(() => t.assert(mysimplearray.ensure({ x: 4 })))
+      mysimplearray.cast([{}])
+    }
+  })
+  t.group('union', () => {
+    const myunion = s.union(s.number, s.string)
+    t.assert(myunion.validate(42))
+    t.assert(myunion.validate('str'))
+    // @ts-expect-error
+    t.assert(!myunion.validate(['str']))
+    // @ts-expect-error
+    t.assert(!myunion.validate(undefined))
+    // @ts-expect-error
+    t.assert(!myunion.validate(new Date()))
+    // @ts-expect-error
+    t.assert(!s.union().validate(42))
+    t.assert(s.union(s.number).validate(42))
+    // @ts-expect-error
+    t.assert(!s.union(s.number).validate('forty'))
+    t.assert(/** @type {s.$Union<any>} */ (s.union(s.union(s.number), s.string)).v.length === 2)
+  })
+  t.group('intersection', () => {
+    const myintersectionNever = s.intersect(s.number, s.string)
+    // @ts-expect-error
+    t.assert(!myintersectionNever.validate(42))
+    // @ts-expect-error
+    t.assert(!myintersectionNever.validate('str'))
+    const myintersection = s.intersect(s.object({ a: s.number }), s.object({ b: s.number }))
+    t.assert(myintersection.validate({ a: 42, b: 42 }))
+    // @ts-expect-error
+    t.assert(!myintersection.validate({ a: 42 }))
+    // @ts-expect-error
+    t.assert(!myintersection.validate({ b: 42 }))
+    // @ts-expect-error
+    t.assert(!myintersection.validate({ c: 42 }))
+    t.assert(myintersection.check({ a: 42, b: 42, c: 42 }))
+    // @ts-expect-error
+    t.assert(!myintersectionNever.validate(['str']))
+    // @ts-expect-error
+    t.assert(!myintersectionNever.validate(undefined))
+    // @ts-expect-error
+    t.assert(!myintersectionNever.validate(new Date()))
+  })
+  t.group('assert', () => {
+    const x = /** @type {unknown} */ (42)
+    // @ts-expect-error
+    s.number.validate(x)
+    s.assert(x, s.number)
+    s.number.validate(x)
+  })
+  t.group('fails on assert/cast/ensure', () => {
+    if (env.production) return t.info('[running in env.production] skipping fail tests because they are skipped in production')
+    t.fails(() => {
+      s.number.cast('42')
+    })
+    t.fails(() => {
+      // @ts-expect-error
+      s.number.ensure('42')
+    })
+    t.fails(() => {
+      s.assert('42', s.number)
+    })
+  })
+  t.group('$Schema', () => {
+    const nullVal = /** @type {unknown} */ (null)
+    const numVal = /** @type {unknown} */ (42)
+    const schema = s.number.nullable
+    // @ts-expect-error
+    schema.validate(nullVal)
+    // @ts-expect-error
+    schema.validate(nullVal) // check twice to confirm that validate does not asserts
+    // @ts-expect-error
+    schema.validate(numVal)
+    s.assert(nullVal, schema)
+    s.assert(numVal, schema)
+    schema.validate(nullVal)
+    schema.validate(numVal)
+    s.assert(undefined, schema.optional)
+  })
+  t.group('schema.cast / schema.ensure', () => {
+    const unknown = /** @type {unknown} */ (42)
+    const known = s.number.cast(unknown)
+    s.number.validate(known)
+    // @ts-expect-error
+    s.number.validate(unknown)
+    const f = s.lambda(s.number, s.$void).ensure((_x) => {})
+    // should match a function with more parameters
+    t.assert(s.lambda(s.number, s.string, s.$void).validate(f))
+    // should still not match a different function
+    // @ts-expect-error
+    s.lambda(s.string, s.$void).validate(f)
+    const x = s.object({ f: s.lambda(s.string, s.$void), n: s.number }).ensure({ f: () => {}, n: 99 })
+    t.assert(x.n === 99)
+    s.lambda().cast(() => {})
+  })
+  t.group('lambda', () => {
+    const $fun = s.lambda(s.number, s.string, s.string)
+    t.assert($fun.validate(() => ''))
+    t.assert($fun.validate(/** @param {number} _n */ (_n) => ''))
+    // @ts-expect-error
+    $fun.validate(/** @param {number} n */ (n) => n) // expected string result
+    const $fun2 = s.lambda(s.number, s.string, s.$void)
+    t.assert($fun2.validate(() => ''))
+    t.assert($fun2.validate(/** @param {number} n */ (n) => n + ''))
+    t.assert($fun2.validate(/** @param {number} n */ (n) => n)) // this works now, because void is the absense of value
+    const $fun3 = s.lambda(s.number, s.undefined)
+    // @ts-expect-error
+    $fun3.validate(/** @param {number} n */ (n) => n) // this doesn't work, because expected the literal undefined.
+    // @ts-expect-error
+    t.assert(!$fun3.validate(/** @type {(a: number, b: number) => undefined} */ (_a, _b) => undefined)) // too many parameters
+  })
+}
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testObjectSchemaOptionals = _tc => {
+  const schema = s.object({ a: s.number.optional, b: s.string.optional })
+  t.assert(schema.validate({ })) // should work
+  // @ts-expect-error
+  t.assert(!schema.validate({ a: 'str' })) // should throw a type error
+  const def = s.union(s.string, s.array(s.number))
+  const defOptional = def.optional
+  const defObject = s.object({ j: defOptional, k: def })
+  // @ts-expect-error
+  t.assert(!defObject.validate({ k: undefined }))
+  t.assert(defObject.validate({ k: [42] }))
+  // @ts-expect-error
+  t.assert(!defObject.validate({ k: [42], j: 42 }))
+  t.assert(defObject.validate({ k: [42], j: 'str' }))
+  t.assert(defObject.validate({ k: [42], j: undefined }))
+}
diff -pruN 0.2.99-1/set.js 0.2.114-1/set.js
--- 0.2.99-1/set.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/set.js	2025-07-16 19:28:19.000000000 +0000
@@ -16,10 +16,9 @@ export const toArray = set => Array.from
 /**
  * @template T
  * @param {Set<T>} set
- * @return {T}
+ * @return {T|undefined}
  */
-export const first = set =>
-  set.values().next().value ?? undefined
+export const first = set => set.values().next().value
 
 /**
  * @template T
diff -pruN 0.2.99-1/sort.test.js 0.2.114-1/sort.test.js
--- 0.2.99-1/sort.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/sort.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -67,7 +67,6 @@ const createSortTest = (tc, createArray,
  * @param {t.TestCase} tc
  */
 export const testSortUint8 = tc => {
-  t.skip(!t.production)
   /**
    * @param {number} i
    * @return {number}
@@ -91,7 +90,6 @@ export const testSortUint8 = tc => {
  * @param {t.TestCase} tc
  */
 export const testSortUint32 = tc => {
-  t.skip(!t.production)
   /**
    * @param {number} i
    * @return {number}
@@ -115,7 +113,6 @@ export const testSortUint32 = tc => {
  * @param {t.TestCase} tc
  */
 export const testSortUint16 = tc => {
-  t.skip(!t.production)
   /**
    * @param {number} i
    * @return {number}
diff -pruN 0.2.99-1/string.js 0.2.114-1/string.js
--- 0.2.99-1/string.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/string.js	2025-07-16 19:28:19.000000000 +0000
@@ -135,3 +135,31 @@ export const splice = (str, index, remov
  * @param {number} n
  */
 export const repeat = (source, n) => array.unfold(n, () => source).join('')
+
+/**
+ * Escape HTML characters &,<,>,'," to their respective HTML entities &amp;,&lt;,&gt;,&#39;,&quot;
+ *
+ * @param {string} str
+ */
+export const escapeHTML = str =>
+  str.replace(/[&<>'"]/g, r => /** @type {string} */ ({
+    '&': '&amp;',
+    '<': '&lt;',
+    '>': '&gt;',
+    "'": '&#39;',
+    '"': '&quot;'
+  }[r]))
+
+/**
+ * Reverse of `escapeHTML`
+ *
+ * @param {string} str
+ */
+export const unescapeHTML = str =>
+  str.replace(/&amp;|&lt;|&gt;|&#39;|&quot;/g, r => /** @type {string} */ ({
+    '&amp;': '&',
+    '&lt;': '<',
+    '&gt;': '>',
+    '&#39;': "'",
+    '&quot;': '"'
+  }[r]))
diff -pruN 0.2.99-1/string.test.js 0.2.114-1/string.test.js
--- 0.2.99-1/string.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/string.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -3,18 +3,18 @@ import * as string from './string.js'
 import * as t from './testing.js'
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testUtilities = tc => {
+export const testUtilities = _tc => {
   t.assert(string.repeat('1', 3) === '111')
   t.assert(string.repeat('1', 0) === '')
   t.assert(string.repeat('1', 1) === '1')
 }
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testLowercaseTransformation = tc => {
+export const testLowercaseTransformation = _tc => {
   t.compareStrings(string.fromCamelCase('ThisIsATest', ' '), 'this is a test')
   t.compareStrings(string.fromCamelCase('Testing', ' '), 'testing')
   t.compareStrings(string.fromCamelCase('testingThis', ' '), 'testing this')
@@ -54,9 +54,9 @@ export const testRepeatStringUtf8Decodin
 }
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testBomEncodingDecoding = tc => {
+export const testBomEncodingDecoding = _tc => {
   const bomStr = '﻿bom'
   t.assert(bomStr.length === 4)
   const polyfilledResult = string._decodeUtf8Polyfill(string._encodeUtf8Polyfill(bomStr))
@@ -69,10 +69,25 @@ export const testBomEncodingDecoding = t
 }
 
 /**
- * @param {t.TestCase} tc
+ * @param {t.TestCase} _tc
  */
-export const testSplice = tc => {
+export const testSplice = _tc => {
   const initial = 'xyz'
   t.compareStrings(string.splice(initial, 0, 2), 'z')
   t.compareStrings(string.splice(initial, 0, 2, 'u'), 'uz')
 }
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testHtmlEscape = _tc => {
+  const cases = [
+    { s: 'hello <script> &', e: 'hello &lt;script&gt; &amp;' },
+    { s: '', e: '' },
+    { s: '\'"', e: '&#39;&quot;' }
+  ]
+  cases.forEach((c) => {
+    t.compare(string.escapeHTML(c.s), c.e)
+    t.compare(string.unescapeHTML(c.e), c.s)
+  })
+}
diff -pruN 0.2.99-1/symbol.js 0.2.114-1/symbol.js
--- 0.2.99-1/symbol.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/symbol.js	2025-07-16 19:28:19.000000000 +0000
@@ -6,8 +6,6 @@
 
 /**
  * Return fresh symbol.
- *
- * @return {Symbol}
  */
 export const create = Symbol
 
diff -pruN 0.2.99-1/test.html 0.2.114-1/test.html
--- 0.2.99-1/test.html	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/test.html	2025-07-16 19:28:19.000000000 +0000
@@ -28,7 +28,7 @@
   "lib0/component": "./component.js",
   "lib0/conditions.js": "./conditions.js",
   "lib0/dist/conditions.cjs": "./dist/conditions.cjs",
-  "lib0/conditions": "./condititons.js",
+  "lib0/conditions": "./conditions.js",
   "lib0/crypto/jwt": "./crypto/jwt.js",
   "lib0/crypto/aes-gcm": "./crypto/aes-gcm.js",
   "lib0/crypto/ecdsa": "./crypto/ecdsa.js",
@@ -38,6 +38,7 @@
   "lib0/decoding.js": "./decoding.js",
   "lib0/dist/decoding.cjs": "./dist/decoding.cjs",
   "lib0/decoding": "./decoding.js",
+  "lib0/diff/patience": "./diff/patience.js",
   "lib0/diff.js": "./diff.js",
   "lib0/dist/diff.cjs": "./dist/diff.cjs",
   "lib0/diff": "./diff.js",
@@ -131,6 +132,9 @@
   "lib0/symbol.js": "./symbol.js",
   "lib0/dist/symbol.cjs": "./dist/symbol.cjs",
   "lib0/symbol": "./symbol.js",
+  "lib0/traits.js": "./traits.js",
+  "lib0/dist/traits.cjs": "./dist/traits.cjs",
+  "lib0/traits": "./traits.js",
   "lib0/testing.js": "./testing.js",
   "lib0/dist/testing.cjs": "./dist/testing.cjs",
   "lib0/testing": "./testing.js",
@@ -150,6 +154,7 @@
   "lib0/performance.js": "./performance.js",
   "lib0/dist/performance.cjs": "./dist/performance.node.cjs",
   "lib0/performance": "./performance.js",
+  "lib0/schema": "./schema.js",
   "isomorphic.js": "./node_modules/isomorphic.js/browser.mjs",
   "isomorphic.js/package.json": "./node_modules/isomorphic.js/package.json"
 },
diff -pruN 0.2.99-1/test.js 0.2.114-1/test.js
--- 0.2.99-1/test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/test.js	2025-07-16 19:28:19.000000000 +0000
@@ -8,6 +8,7 @@ import * as logging from './logging.test
 import * as string from './string.test.js'
 import * as encoding from './encoding.test.js'
 import * as diff from './diff.test.js'
+import * as patienceDiff from './diff/patience.test.js'
 import * as testing from './testing.test.js'
 import * as indexeddb from './indexeddb.test.js'
 import * as indexeddbV2 from './indexeddbV2.test.js'
@@ -37,7 +38,8 @@ import * as storage from './storage.test
 import * as list from './list.test.js'
 import * as cache from './cache.test.js'
 import * as symbol from './symbol.test.js'
-
+import * as traits from './traits.test.js'
+import * as schema from './schema.test.js'
 import { isBrowser, isNode } from './environment.js'
 
 /* c8 ignore next */
@@ -55,6 +57,7 @@ runTests({
   string,
   encoding,
   diff,
+  patienceDiff,
   testing,
   indexeddb,
   indexeddbV2,
@@ -82,7 +85,9 @@ runTests({
   storage,
   list,
   cache,
-  symbol
+  symbol,
+  traits,
+  schema
 }).then(success => {
   /* c8 ignore next */
   if (isNode) {
diff -pruN 0.2.99-1/testing.js 0.2.114-1/testing.js
--- 0.2.99-1/testing.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/testing.js	2025-07-16 19:28:19.000000000 +0000
@@ -31,7 +31,7 @@
  *  * runTests automatically tests all exported functions that start with "test".
  *  * The name of the function should be in camelCase and is used for the logging output.
  *  *
- *  * @param {t.TestCase} tc
+ *  * @ param {t.TestCase} tc
  *  *\/
  * export const testMyFirstTest = tc => {
  *   t.compare({ a: 4 }, { a: 4 }, 'objects are equal')
@@ -57,9 +57,9 @@ import * as json from './json.js'
 import * as time from './time.js'
 import * as promise from './promise.js'
 import * as performance from 'lib0/performance'
+import * as traits from './traits.js'
 
 export { production } from './environment.js'
-
 export const extensive = env.hasConf('extensive')
 
 /* c8 ignore next */
@@ -435,6 +435,13 @@ const _compare = (a, b, path, message, c
   if (a == null || b == null) {
     return compareValues(null, a, b, path)
   }
+  if (a[traits.EqualityTraitSymbol] != null) {
+    if (a[traits.EqualityTraitSymbol](b)) {
+      return true
+    } else {
+      _failMessage(message, 'Not equal by equality trait', path)
+    }
+  }
   if (a.constructor !== b.constructor) {
     _failMessage(message, 'Constructors don\'t match', path)
   }
@@ -478,6 +485,7 @@ const _compare = (a, b, path, message, c
       })
       break
     }
+    case undefined: // undefined is often set as a constructor for objects
     case Object:
       if (object.length(a) !== object.length(b)) {
         _failMessage(message, 'Objects have a different number of attributes', path)
diff -pruN 0.2.99-1/testing.test.js 0.2.114-1/testing.test.js
--- 0.2.99-1/testing.test.js	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/testing.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -36,7 +36,6 @@ export const testComparing = _tc => {
   map2.set('x', {})
   map2.set(98, 'tst')
   t.compare(map1, map2, 'compare Maps')
-
   t.describe('The following errors are expected!')
   t.fails(() => {
     t.compare({ a: 4 }, { b: 5 }, 'childs are not equal')
@@ -114,6 +113,13 @@ export const testComparing = _tc => {
   t.fails(() => {
     t.compare(new Set([1]), new Set([2])) // childs have different length (array) -- no message
   })
+  t.group('test object with constructor set to `undefined`', () => {
+    const a = Object.create(null)
+    const b = Object.create(null)
+    a.x = 42
+    b.x = 42
+    t.compare(a, b)
+  })
 }
 
 export const testFailing = async () => {
diff -pruN 0.2.99-1/traits.js 0.2.114-1/traits.js
--- 0.2.99-1/traits.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/traits.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,5 @@
+export const EqualityTraitSymbol = Symbol('Equality')
+
+/**
+ * @typedef {{ [EqualityTraitSymbol]:(other:EqualityTrait)=>boolean }} EqualityTrait
+ */
diff -pruN 0.2.99-1/traits.test.js 0.2.114-1/traits.test.js
--- 0.2.99-1/traits.test.js	1970-01-01 00:00:00.000000000 +0000
+++ 0.2.114-1/traits.test.js	2025-07-16 19:28:19.000000000 +0000
@@ -0,0 +1,53 @@
+import * as t from './testing.js'
+import * as fun from './function.js'
+import * as traits from './traits.js'
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testEqualityTrait1 = _tc => {
+  /**
+   * @implements traits.EqualityTrait
+   */
+  class X {
+    /**
+     * @param {object} other
+     * @return boolean
+     */
+    [traits.EqualityTraitSymbol] (other) { return this === other }
+  }
+  const x = new X()
+  const y = new X()
+  t.assert(!x[traits.EqualityTraitSymbol](y))
+  t.assert(x[traits.EqualityTraitSymbol](x))
+}
+
+/**
+ * @param {t.TestCase} _tc
+ */
+export const testEqualityTrait2 = _tc => {
+  /**
+   * @implements traits.EqualityTrait
+   */
+  class X {
+    /**
+     * @param {any} other
+     * @return boolean
+     */
+    [traits.EqualityTraitSymbol] (other) { return this.constructor === other.constructor }
+  }
+  class Y {}
+  const x = new X()
+  const x2 = new X()
+  const y = new Y()
+  t.assert(!x[traits.EqualityTraitSymbol](y))
+  t.assert(x[traits.EqualityTraitSymbol](x))
+  t.assert(x[traits.EqualityTraitSymbol](x2))
+  t.compare(x, x2)
+  t.fails(() => {
+    t.compare(x, y)
+  })
+  t.assert(fun.equalityDeep(x, x))
+  t.assert(fun.equalityDeep(x, x2))
+  t.assert(!fun.equalityDeep(x, y))
+}
diff -pruN 0.2.99-1/tsconfig.json 0.2.114-1/tsconfig.json
--- 0.2.99-1/tsconfig.json	2024-12-04 22:48:08.000000000 +0000
+++ 0.2.114-1/tsconfig.json	2025-07-16 19:28:19.000000000 +0000
@@ -1,8 +1,8 @@
 {
   "compilerOptions": {
-    "target": "ES2022",
-    "lib": ["ES2022", "dom"],
-    "module": "ES2022",
+    "target": "esnext",
+    "lib": ["dom", "ES2022"],
+    "module": "nodenext",
     "allowJs": true,
     "checkJs": true,
     "declaration": true,
@@ -21,6 +21,6 @@
       "lib0/logging": ["./logging.node.js"]
     }
   },
-  "include": ["./*.js", "./hash/*.js", "./crypto/*.js", "./bin/*.js"],
+  "include": ["./*.js", "./diff/*.js", "./hash/*.js", "./crypto/*.js", "./bin/*.js"],
   "exclude": ["./dist/**/*"]
 }
