{"_id":"@astrapi69/tree-kit","_rev":"4-5dab0460eeecb65a359f486ddb64b2ad","name":"@astrapi69/tree-kit","dist-tags":{"latest":"0.3.1"},"versions":{"0.1.0":{"name":"@astrapi69/tree-kit","version":"0.1.0","keywords":["tree","forest","hierarchy","traversal","generator","iterator","immutable","serializable","typescript","framework-agnostic"],"author":{"name":"Asterios Raptis"},"license":"MIT","_id":"@astrapi69/tree-kit@0.1.0","maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"homepage":"https://github.com/astrapi69/tree-kit#readme","bugs":{"url":"https://github.com/astrapi69/tree-kit/issues"},"dist":{"shasum":"59153063da77c1e345c7bf272d77043e2288989a","tarball":"https://registry.npmjs.org/@astrapi69/tree-kit/-/tree-kit-0.1.0.tgz","fileCount":9,"integrity":"sha512-ePSH3t2jTwGs24IRH6/c7lNx7ekizdMbN8GFqc/p8yCG+WPM+3JNc3IsmpTWA2Q+0vvmAmNsYg+aWazuHukJfg==","signatures":[{"sig":"MEQCIGVKu31j6dVDAC2C0Yk2nQ8pj8J6WVbZjsTDuKlZYs8XAiBopyduZL9ciGr6pio8qmIrDYd9GjyMxoinu5hDO7EMKA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":91643},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"9b28bfaf5ef304e0d7307eb3bed7d3bf7639083b","scripts":{"dev":"tsup --watch","lint":"eslint src/ tests/","test":"vitest run","build":"tsup","lint:fix":"eslint src/ tests/ --fix","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"astrapi69","email":"asterios.raptis@gmx.net"},"repository":{"url":"git+https://github.com/astrapi69/tree-kit.git","type":"git"},"_npmVersion":"12.0.1","description":"Typed, serialisable tree structures for TypeScript. Immutable acyclic nodes, a navigating cursor, and generator-based traversal. Zero dependencies, framework-agnostic.","directories":{},"sideEffects":false,"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.4.1","vitest":"^4.1.8","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.2","typescript-eslint":"^8.60.1","@vitest/coverage-v8":"^4.1.8"},"_npmOperationalInternal":{"tmp":"tmp/tree-kit_0.1.0_1785829395181_0.5979150389566377","host":"s3://npm-registry-packages-npm-production"}},"0.2.0":{"name":"@astrapi69/tree-kit","version":"0.2.0","keywords":["tree","forest","hierarchy","traversal","generator","iterator","immutable","serializable","typescript","framework-agnostic"],"author":{"name":"Asterios Raptis"},"license":"MIT","_id":"@astrapi69/tree-kit@0.2.0","maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"homepage":"https://github.com/astrapi69/tree-kit#readme","bugs":{"url":"https://github.com/astrapi69/tree-kit/issues"},"dist":{"shasum":"7322ded69342dfd188ad01c0c41cb0309e71814e","tarball":"https://registry.npmjs.org/@astrapi69/tree-kit/-/tree-kit-0.2.0.tgz","fileCount":9,"integrity":"sha512-WpBr+FlUpxTSVYZCGH+tt1l83KQT+s9A7ezvuoRsF3v17UnnhUtxRStfMJqBogaswdTwyFdnxtIerNs/DrhXmw==","signatures":[{"sig":"MEUCIQClgIiBTjPyLylkAt5UuH6q4XeVhJ05mit/omeZyh+MsAIgajcWxyp5MlYqFv05ZPpcT0t64UYZOByobNVguoKHTM0=","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":107077},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"0b04bd046902adb452f461f64f942b2f5eadf8e3","scripts":{"dev":"tsup --watch","lint":"eslint src/ tests/","test":"vitest run","build":"tsup","lint:fix":"eslint src/ tests/ --fix","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"astrapi69","email":"asterios.raptis@gmx.net"},"repository":{"url":"git+https://github.com/astrapi69/tree-kit.git","type":"git"},"_npmVersion":"12.0.1","description":"Typed, serialisable tree structures for TypeScript. Immutable acyclic nodes, a navigating cursor, and generator-based traversal. Zero dependencies, framework-agnostic.","directories":{},"sideEffects":false,"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.4.1","vitest":"^4.1.8","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.2","typescript-eslint":"^8.60.1","@vitest/coverage-v8":"^4.1.8"},"_npmOperationalInternal":{"tmp":"tmp/tree-kit_0.2.0_1786700094550_0.9771143416970645","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"name":"@astrapi69/tree-kit","version":"0.3.0","keywords":["tree","forest","hierarchy","traversal","generator","iterator","immutable","serializable","typescript","framework-agnostic"],"author":{"name":"Asterios Raptis"},"license":"MIT","_id":"@astrapi69/tree-kit@0.3.0","maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"homepage":"https://github.com/astrapi69/tree-kit#readme","bugs":{"url":"https://github.com/astrapi69/tree-kit/issues"},"dist":{"shasum":"d8bbd7d08318320ac33ccdefb272a293517a422a","tarball":"https://registry.npmjs.org/@astrapi69/tree-kit/-/tree-kit-0.3.0.tgz","fileCount":9,"integrity":"sha512-TyaNpjmDcj4OHIKd1PBHN/oL5/9buUTNj+o/5+NnRSoxNJGus1S8eVv27f/5pwAla5OTO/aDiF+z8rt8PiHjrw==","signatures":[{"sig":"MEQCIGJq/BmijYgeYOioP0G+aLSyFrgue2MWGNq9qXT+0LIKAiAHUGCNmWKjgt/Tzc+24frg+T0kcRgPo0Ibm4gjAYlPpA==","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":205072},"main":"./dist/index.cjs","type":"module","types":"./dist/index.d.ts","module":"./dist/index.js","exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"gitHead":"7ca9944d4fd9a6a99a69f2442ece555feb0c0d23","scripts":{"dev":"tsup --watch","lint":"eslint src/ tests/","test":"vitest run","build":"tsup","lint:fix":"eslint src/ tests/ --fix","typecheck":"tsc --noEmit","test:watch":"vitest","test:coverage":"vitest run --coverage","prepublishOnly":"npm run build"},"_npmUser":{"name":"astrapi69","email":"asterios.raptis@gmx.net"},"repository":{"url":"git+https://github.com/astrapi69/tree-kit.git","type":"git"},"_npmVersion":"12.0.1","description":"Typed, serialisable tree structures for TypeScript. Immutable acyclic nodes, a navigating cursor, and generator-based traversal. Zero dependencies, framework-agnostic.","directories":{},"sideEffects":false,"_nodeVersion":"24.15.0","publishConfig":{"access":"public"},"_hasShrinkwrap":false,"devDependencies":{"tsup":"^8.5.1","eslint":"^10.4.1","vitest":"^4.1.8","@eslint/js":"^10.0.1","typescript":"^6.0.3","@types/node":"^26.1.2","typescript-eslint":"^8.60.1","@vitest/coverage-v8":"^4.1.8"},"_npmOperationalInternal":{"tmp":"tmp/tree-kit_0.3.0_1786782967770_0.2705034611559787","host":"s3://npm-registry-packages-npm-production"}},"0.3.1":{"name":"@astrapi69/tree-kit","version":"0.3.1","description":"Typed, serialisable tree structures for TypeScript. Immutable acyclic nodes, a navigating cursor, and generator-based traversal. Zero dependencies, framework-agnostic.","type":"module","main":"./dist/index.cjs","module":"./dist/index.js","types":"./dist/index.d.ts","license":"MIT","author":{"name":"Asterios Raptis"},"sideEffects":false,"repository":{"type":"git","url":"git+https://github.com/astrapi69/tree-kit.git"},"bugs":{"url":"https://github.com/astrapi69/tree-kit/issues"},"homepage":"https://github.com/astrapi69/tree-kit#readme","keywords":["tree","forest","hierarchy","traversal","generator","iterator","immutable","serializable","typescript","framework-agnostic"],"exports":{".":{"types":"./dist/index.d.ts","import":"./dist/index.js","require":"./dist/index.cjs"},"./package.json":"./package.json"},"scripts":{"build":"tsup","dev":"tsup --watch","test":"vitest run","test:watch":"vitest","test:coverage":"vitest run --coverage","typecheck":"tsc --noEmit","lint":"eslint src/ tests/","lint:fix":"eslint src/ tests/ --fix","prepublishOnly":"npm run build"},"publishConfig":{"access":"public"},"devDependencies":{"@eslint/js":"^10.0.1","@types/node":"^26.1.2","@vitest/coverage-v8":"^4.1.8","eslint":"^10.4.1","tsup":"^8.5.1","typescript":"^6.0.3","typescript-eslint":"^8.60.1","vitest":"^4.1.8"},"gitHead":"7bd0cf23884e17da79bc0ce724a1d5ae09c18380","_id":"@astrapi69/tree-kit@0.3.1","_nodeVersion":"24.15.0","_npmVersion":"12.0.1","dist":{"integrity":"sha512-TUafw/Aj02wDS8+PZf9CBSjjrj6eggJeTcSPtDqpy/IiFH2vgZZP7r8BHDLFtZLO9tGojBHNJxyLp5XAPV3Uzw==","shasum":"513258cbeae40fea068e5a6bf377633fc55b2eb7","tarball":"https://registry.npmjs.org/@astrapi69/tree-kit/-/tree-kit-0.3.1.tgz","fileCount":9,"unpackedSize":207229,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIQCAY7pg7lEeDS3ISisQfpXpscxWfeD88X/CYvMr/mZpSgIgVsSJa3RynZj2pmZxrvgyB4KbZ8ivO+5hl2uq6OVHET8="}]},"_npmUser":{"name":"astrapi69","email":"asterios.raptis@gmx.net"},"directories":{},"maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/tree-kit_0.3.1_1786786531469_0.14105167008773067"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-04T07:43:15.065Z","modified":"2026-08-15T09:35:31.815Z","0.1.0":"2026-08-04T07:43:15.307Z","0.2.0":"2026-08-14T09:34:54.697Z","0.3.0":"2026-08-15T08:36:07.931Z","0.3.1":"2026-08-15T09:35:31.632Z"},"bugs":{"url":"https://github.com/astrapi69/tree-kit/issues"},"author":{"name":"Asterios Raptis"},"license":"MIT","homepage":"https://github.com/astrapi69/tree-kit#readme","keywords":["tree","forest","hierarchy","traversal","generator","iterator","immutable","serializable","typescript","framework-agnostic"],"repository":{"type":"git","url":"git+https://github.com/astrapi69/tree-kit.git"},"description":"Typed, serialisable tree structures for TypeScript. Immutable acyclic nodes, a navigating cursor, and generator-based traversal. Zero dependencies, framework-agnostic.","maintainers":[{"name":"astrapi69","email":"asterios.raptis@gmx.net"}],"readme":"# @astrapi69/tree-kit\n\nTyped, serialisable tree structures for TypeScript. Zero runtime dependencies,\nframework-agnostic.\n\nA TypeScript port of the Java libraries [`astrapi69/tree-api`][tree-api] and\n[`astrapi69/gen-tree`][gen-tree], redesigned around TypeScript's own idioms\nrather than transliterated class-for-class.\n\n[tree-api]: https://github.com/astrapi69/tree-api\n[gen-tree]: https://github.com/astrapi69/gen-tree\n\n## Install\n\n```bash\nnpm install @astrapi69/tree-kit\n```\n\n## The two types\n\n**`TreeNode<V, K>`** is pure data: immutable, acyclic, and free of methods. It\nsurvives `JSON.stringify` and `structuredClone` untouched, so a whole tree goes\ninto `localStorage` or over the wire without a serialisation step.\n\n```ts\ninterface TreeNode<V, K = string> {\n  readonly id: K\n  readonly value: V\n  readonly children: readonly TreeNode<V, K>[]\n}\n```\n\n**`TreeCursor<V, K>`** is a transient pointer that knows its own position. It\nholds the parent reference the node deliberately does not, so `parent()`,\n`path()` and `depth()` work without making the object graph cyclic.\n\n```ts\nconst cursor = rootCursor(tree)\ncursor.children()[0].parent() === cursor   // true - shared instance\n```\n\n## Quick start\n\n```ts\nimport {buildTreeFromFlat, walkForest} from \"@astrapi69/tree-kit\"\n\ninterface Topic {\n  id: string\n  parentId: string | null\n  title: string\n  position: number\n}\n\nconst forest = buildTreeFromFlat<Topic, string>(rows, {\n  getId: (row) => row.id,\n  getParentId: (row) => row.parentId,\n  sort: (a, b) => a.position - b.position,\n})\n\nfor (const cursor of walkForest(forest)) {\n  console.log(\"  \".repeat(cursor.depth()) + cursor.value.title)\n}\n\nlocalStorage.setItem(\"topics\", JSON.stringify(forest))\n```\n\n## Strict by default, tolerant on request\n\n`buildTreeFromFlat` rejects structural defects loudly — the right answer\nfor data that is supposed to be sound, because a silent repair hides the\nupstream bug that produced it. Views are different: a page rendering\nFILTERED rows, or whatever a sync left behind, must show the data it has,\nnot crash on the row it cannot place. That is what\n`onInvalidParent: \"promoteToRoot\"` is for:\n\n```ts\nconst forest = buildTreeFromFlat<Category, string>(rows, {\n  getId: (row) => row.path,\n  getParentId: (row) => row.parentPath,\n  onInvalidParent: \"promoteToRoot\",\n})\n```\n\n| Defect | `\"throw\"` (default) | `\"promoteToRoot\"` |\n|---|---|---|\n| Parent id names no row | `Error: ... references unknown parent` | Row becomes a root |\n| Cycle (`a -> b -> a`) | `Error: cycle detected` | Every member becomes a root |\n| Row hanging below a cycle | `Error: cycle or orphan rows` | Becomes a root too — its chain never terminates either |\n| Duplicate id | `Error: duplicate id` | **Still throws.** Two rows with one id is corruption no placement can express |\n\nThe rule is one sentence: *a row is promoted to a root when its parent id\nnames no row, or its ancestor chain never reaches a root.* Valid nesting\nin the same input stays intact, promoted roots take part in the sibling\n`sort` like real ones, and the resolution is memoised so the build stays\nO(n).\n\nWhich mode belongs where:\n\n- **Ingest, import, persistence** — keep the default. If a workbook or\n  API response carries a dangling reference, you want the exception and\n  the offending id, not a silently reshaped tree.\n- **Rendering** — opt in. The two real-world shapes this option came\n  from: a category tree where a child can outlive its deleted parent\n  (orphan report exists, the view must still render), and an inventory\n  view over a FILTERED row list, where a visible child of a filtered-out\n  parent must not vanish.\n- **Custom degradation** — stays yours. `promoteToRoot` promotes to the\n  FOREST root; if your domain wants something else (Topos degrades\n  containers to their type/owner group instead), resolve parents\n  yourself before building and keep the option as a second net.\n\n## Traversal is `for...of`\n\nThere is no Visitor callback and no sentinel return value. Stop with `break`.\n\n```ts\nfor (const cursor of walk(tree, \"breadth\")) {\n  if (cursor.depth() > 2) break\n  render(cursor)\n}\n```\n\nThree orders: `pre` (default, parents first), `post` (children first), `breadth`\n(level by level). `walkForest` synchronises breadth-first levels across roots;\n`pre` and `post` finish one root's subtree before starting the next.\n\n`find` is lazy and stops at the first match:\n\n```ts\nconst match = find(tree, (cursor) => cursor.id === target)\nmatch?.path().map((node) => node.id)   // breadcrumb\n```\n\n## Typed ids\n\n`K` defaults to `string` but is free to be a number or a branded string, so ids\nfrom two different trees cannot silently cross at compile time.\n\n```ts\ntype TopicId = string & {readonly __brand: \"TopicId\"}\ntype LessonId = string & {readonly __brand: \"LessonId\"}\n\nconst topics: TreeNode<Topic, TopicId> = /* ... */\nfind(topics, (cursor) => cursor.id === someLessonId)   // compile error\n```\n\n## Editing is copy-on-write\n\n`TreeNode` is `readonly`, so editing a tree means building a new one. Every\nmutation takes a forest and a cursor at the target, and returns a **new**\nforest — the input is never touched. Only the nodes from a root down to the\nedit are re-allocated; every sibling subtree and untouched root comes back by\nidentity (`===`), so a deep change costs O(depth) new objects, not O(n). The\nresult stays `JSON.stringify`- and `structuredClone`-safe like everything the\nbuilder makes.\n\n```ts\nimport {addChild, moveNode, removeNode, updateValue, find} from \"@astrapi69/tree-kit\"\n\nconst target = find(forest[0], (c) => c.id === \"menu\")!\nconst withItem = addChild(forest, target, {id: \"help\", value: {label: \"Help\"}, children: []})\n\nconst renamed = updateValue(withItem, find(withItem[0], (c) => c.id === \"help\")!, {label: \"Support\"})\n\n// Drag & drop: reparent a subtree. Throws if the target is inside the source.\nconst dst = find(renamed[0], (c) => c.id === \"sidebar\")!\nconst src = find(renamed[0], (c) => c.id === \"help\")!\nconst moved = moveNode(renamed, src, dst)\n\nconst pruned = removeNode(moved, find(moved[0], (c) => c.id === \"help\")!)\n```\n\nUntouched subtrees keep their identity, which is exactly what memoised renders\nand cheap undo stacks rely on:\n\n```ts\nconst next = updateValue(forest, deepCursor, newValue)\nnext[0].children[1] === forest[0].children[1]   // true — off the edited path\n```\n\n`flatten` is the inverse of `buildTreeFromFlat`: a forest back to parent-linked\nrows, in pre-order, so the two round-trip.\n\n```ts\nconst rows = flatten(forest)          // [{id, parentId, value}, ...]\nconst again = buildTreeFromFlat(rows, {getId: (r) => r.id, getParentId: (r) => r.parentId})\n```\n\n## Transform and query\n\n`mapValues`, `filterTree` and `reduceTree` are the tree-shaped counterparts to\n`Array`'s `map` / `filter` / `reduce`. `filterTree` keeps the hierarchy of the\nsurvivors — dropping an intermediate node promotes its kept descendants rather\nthan deleting them — so it returns a forest, unlike `findAll`'s flat list.\n\n```ts\nconst titles = mapValues(forest[0], (topic) => topic.title)          // TreeNode<string>\nconst published = filterTree(forest[0], (n) => n.value.status !== \"draft\")\nconst total = reduceTree(forest[0], (sum, topic) => sum + topic.position, 0)\n```\n\nThe structural queries mirror the Java forebears' vocabulary\n(`getAllSiblings`, level/depth, ancestor tests) without their mutable node:\n`height`, `siblings`, `isAncestor` / `isDescendant`, `lowestCommonAncestor`,\n`extractSubtree`, `cloneSubtree`.\n\n```ts\nlowestCommonAncestor(childA, childB)?.id   // breadcrumb / permission scope\n```\n\n## API\n\n| Export | Kind | Purpose |\n|---|---|---|\n| `TreeNode<V, K>` | type | Immutable, acyclic, serialisable node |\n| `TreeCursor<V, K>` | type | Navigable position inside a tree |\n| `TraversalStrategy` | type | `\"pre\" \\| \"post\" \\| \"breadth\"` |\n| `DisplayFormatter<V>` | type | `(value: V) => string` |\n| `BuildTreeOptions<V, K>` | type | Key extractors, optional sibling sort, `onInvalidParent` |\n| `FlatNode<V, K>` | type | `{id, parentId, value}` row produced by `flatten` |\n| `buildTreeFromFlat` | fn | Flat `(id, parentId)` rows into a forest, O(n) |\n| `rootCursor` | fn | Cursor at a node, treated as a root |\n| `walk` | fn | Generator over one subtree |\n| `walkForest` | fn | Generator over several roots |\n| `find` / `findAll` | fn | First / all matching cursors |\n| `count` | fn | Node count including the root |\n| `displayValue` | fn | Label for a node, via an optional formatter |\n| `addChild` | fn | Append a child, copy-on-write → new forest |\n| `removeNode` | fn | Drop a node and its subtree → new forest |\n| `moveNode` | fn | Reparent a subtree; throws on a cyclic move |\n| `updateValue` | fn | Replace one node's value, keep its subtree |\n| `replaceSubtree` | fn | Swap the subtree at a cursor |\n| `flatten` | fn | Forest → parent-linked rows, inverse of the builder |\n| `mapValues` | fn | New tree with transformed values, ids preserved |\n| `filterTree` | fn | Keep matches, reparent survivors → forest |\n| `reduceTree` | fn | Fold all values into one accumulator |\n| `height` | fn | Deepest descent below a node |\n| `siblings` | fn | The cursor's same-parent neighbours |\n| `isAncestor` / `isDescendant` | fn | Proper ancestor / descendant test |\n| `lowestCommonAncestor` | fn | Nearest node above two cursors |\n| `extractSubtree` | fn | The subtree at a cursor as a standalone tree |\n| `cloneSubtree` | fn | Deep, independent copy with optional id remap |\n\n## Design notes\n\n**Why no parent pointer on the node.** It would make the object graph cyclic:\n`JSON.stringify` throws `TypeError: Converting circular structure to JSON`,\n`structuredClone` throws, and debuggers walk in circles. The parent lives on the\ncursor instead, which is transient and never serialised.\n\n**Why no `[Symbol.iterator]` on the node.** A method is an own property.\n`JSON.stringify` would drop it silently, but `structuredClone` refuses to clone\nfunctions outright — the node would stop being clonable. Iteration lives on the\ncursor: `for (const each of rootCursor(tree))`.\n\n**Why no formatter stored on the tree.** A label is a rendering concern, and\nstoring a function on the node would break clonability for the same reason.\n`displayValue(node, formatter)` takes it at call time.\n\n**Cursor identity is deliberate.** `cursor.parent()` returns the same instance\non every call and `children()` memoises, so sibling cursors compare equal on\ntheir parent and `Set` / memo dependencies behave. Compare `cursor.node` when\ncomparing positions across independent traversals.\n\n**Every pass is iterative.** Build, sort and all three traversals run on an\nexplicit stack rather than recursion, so a chain deeper than the call stack is\nhandled like any other input. Pinned by a 50 000-level test.\n\n**Construction is O(n).** One pass indexes rows by id in a `Map`, one pass links\neach row to its parent with an O(1) lookup. Duplicate ids, unknown parent\nreferences and cycles all throw with the offending ids named — a silent drop\nwould hide the upstream bug that produced them. The tolerant mode\n(`onInvalidParent: \"promoteToRoot\"`) trades the exception for a visible\ndegradation — promotion to a root — and only for the two defects a view can\nmeaningfully survive; duplicate ids stay fatal in both modes. Its chain\nresolution is memoised, so tolerance costs no complexity class.\n\n## Maturity: which surface is proven\n\nThe package ships three surfaces, and they do not carry the same weight\nof evidence. The version number says so on purpose.\n\n**`TreeNode` and `buildTreeFromFlat` are proven by use.** They were migrated\ninto a real application before this package was first published: five lines\nchanged, 735 deleted, no addition needed. The shape held against a consumer.\nThe tolerant mode (0.2.0) came the same way, from the consumer side: Topos had\nwritten the identical pre-sanitizer twice (a category tree tolerating orphans,\nan inventory tree over filtered rows), and adaptive-learner called the builder\nraw - a latent crash on the first dangling reference. The option replaced both\nTopos sanitizers with their behaviour pins staying green unchanged, and\nadaptive-learner closed its latent crash with the one-line opt-in (its\ncurriculum now renders an orphaned topic at top level instead of throwing) -\ntwo applications consume the tolerant mode in production.\n\n**`TreeCursor` is proven by tests only.** No consumer has used `parent()`,\n`path()` or `depth()` yet. It exists because the alternative was a parent\npointer on the node, which would have cost `JSON.stringify` and\n`structuredClone` — that reasoning stands on its own. But a surface without a\nuser is a prediction about what someone will need, and its first real use\n(keyboard focus and breadcrumbs in a menu engine) is still ahead. Expect it to\nmove before 1.0.\n\n**The mutation and query surface (0.3.0) is proven by tests only, and by\ndesign review.** 110 tests pin copy-on-write semantics, structural sharing\nand the cyclic-move guard, and the runnable example asserts the sharing\nproperty live (`===` on the untouched subtree). But no application edits\nits trees through this surface yet - the first candidate (reparenting\ntopics in a curriculum view) is known and still ahead. Same caveat as the\ncursor: a surface without a consumer is a prediction.\n\nThis is what `0.x` is for. One surface is verified by use, two are\nexpected, and the number should not claim otherwise.\n\n## Planned, awaiting a consumer\n\nSome API is deliberately not built yet. The rule (see\n[CONTRIBUTING](CONTRIBUTING.md)): new surface needs a real caller, not a\nuse case - a library grows credibility by what it refuses to predict.\nEach entry below is tracked, with the trigger that would turn it into\ncode:\n\n| Idea | Trigger that unlocks it | Tracked |\n|---|---|---|\n| `merge(forestA, forestB)` with a conflict strategy | Live collaboration or two-source sync in a consumer app | [#9](https://github.com/astrapi69/tree-kit/issues/9) |\n| `diff(forestA, forestB)` for incremental updates | Same - shipping deltas instead of whole forests | [#9](https://github.com/astrapi69/tree-kit/issues/9) |\n| Orphan-collection variant of `onInvalidParent` | A caller that wants to HANDLE invalid rows programmatically instead of rendering them promoted | [#10](https://github.com/astrapi69/tree-kit/issues/10) |\n\nTwo shipped surfaces are in the same waiting room, just further along:\nthe cursor (its first real use - keyboard focus and breadcrumbs - is\nstill ahead) and the 0.3.0 mutation surface, whose first named\ncandidate is reparenting curriculum topics via `moveNode` in\nadaptive-learner. The maturity section above tracks both honestly.\n\n## Changelog\n\nSee [CHANGELOG.md](CHANGELOG.md) for the release history.\n\n## Examples\n\nRunnable scripts under [`examples/`](examples/): the happy path\n(build, sort, cursor traversal, serialisation), the tolerant mode side\nby side with the strict default on the same defective rows, and the\ncopy-on-write surface - a move with the structural-sharing identity\ncheck, the cyclic-move refusal, and `flatten` closing the circle back\nto rows.\n\n```bash\nnpm run build && node examples/mutations.mjs\n```\n\n## Development\n\n```bash\nmake install     # install dependencies\nmake test        # run the suite\nmake check-all   # lint + typecheck + test + build\nmake inspect     # show exactly what would be published\n```\n\n## License\n\nMIT — Asterios Raptis\n","readmeFilename":"README.md"}