EidosTree (tree-structure store)¶
Verified against the code · 421c63ba3fab5894cdb9e707eed8ef694881d60d · 2026-10-20
956 D1b-1 (2026-10-20): 5 finding(s) fixed — _ingest(trees, freshIds) documented as the IAM /tree adapter into EidosNodeStore returning {trees, iamEnabled}, snapshot() copies _parentOf only, _rebuild(trees), one B2_BASELINE counter over six private maps, B1 scope. Re-verified against main 421c63ba.¶
Layer: frontend
EidosTree (frontend/eidos-tree.js) is the canonical owner of tree-structure
state in the browser. Before trin 5.2d (#899) five maps and one index lived
module-level in app.js: parentMap, nodeToTreeIndex, childrenCache,
allTrees, nodeExpandFns, activeTreeIndex. All six moved into one module
across 5.2d → 5.2f. Spec SSOT: docs/specs/eidos-nodestore-tree.md §4.2,
§5.2d, §5.2e, §8.
What it owns¶
| Backing store | Purpose |
|---|---|
_parentOf |
childId -> parentId — the parent walk. |
_rootOf |
nodeId -> treeIndex — which tab a node belongs to. |
_childrenOf |
uid -> [{id,label,description,hasChildren}] — pre-materialised children for the detail panel's chip; populated by _cacheChildren. |
_trees |
The trees array (one entry per tab). |
_expandFns |
nodeId -> async fn that expands that node in the tree. |
_activeIdx |
Index of the active tab (primitive — exposed via getter). |
The live-alias contract¶
app.js holds const aliases to the SAME backing objects:
const parentMap = window.EidosTree._parentOf
const childrenCache = window.EidosTree._childrenOf
const allTrees = window.EidosTree._trees
const nodeExpandFns = window.EidosTree._expandFns
All mutating methods (_rebuild, _cacheChildren, _setTrees,
_clearExpandFns, _registerExpand) therefore mutate IN PLACE — they never
reassign the backing map or array. A reassignment would leave app.js
holding a stale reference. The one primitive — _activeIdx — is a number;
an export literal would freeze the initial 0, so it is exposed via an
Object.defineProperty getter that routes window.EidosTree._activeIdx
through the closure variable.
The thin-proxy getters that used to live on EidosCore (parentMap,
allTrees, nodeCache) were retired in 5.2f (#906). Plugins read
EidosTree.* directly.
The B2 ratchet¶
tests/test_frontend_node_tree_ratchet.py freezes one B2 counter
(B2_BASELINE), the sum of two pattern families:
- Cross-file reads of
window.EidosCore.parentMap,.allTrees,core.parentMap, orparentMap()fromfrontend/**/*.jsexceptapp.jsand the stores. - Direct private-map access (
_B2_TREE_PRIVATE:window.EidosTree._parentOf,._rootOf,._childrenOf,._trees,._expandFns,._activeIdx) from the same scope — exposing a private map under a new name is the same coupling as aparentMapread.
The counter was migrated to 0 in 5.2d (#899). A new plugin read bumps
it and trips the ratchet at equality (not <=).
B1 (WRITE-bypass — EidosCore.{nodeCache,parentMap,allTrees}[x] = y,
.push(...) on those, and EidosNodeStore._nodes[x] = y) is also frozen at
0 since 5.2b. Writes to the EidosTree private maps are not B1; B2's
_B2_TREE_PRIVATE already catches any access to them.
The _ingest / _rebuild seam¶
_ingest(trees, freshIds) is the IAM /tree adapter: app.js's
normalizeTreeResponse calls it (app.js:3134) with the whole /tree
array. It converts the IAM-format trees to the old format, upserts or patches
each node's metadata into EidosNodeStore, and returns
{trees, iamEnabled}. It never touches _parentOf / _rootOf.
_rebuild(trees) is the only writer of those two maps: a full rewrite from
the passed trees array (buildParentMap calls _rebuild(allTrees), reset
calls _rebuild([])), which also sets _trees via _setTrees. It mutates
the maps in place. _cacheChildren(uid, children)
writes the detail-panel children chip cache. The coupling to
EidosNodeStore is one-directional: _ingest writes nodes INTO the node
store so a GET /tree server round-trip populates both stores from the
single response.
Load order¶
<script src="frontend/eidos-node-store.js"> <!-- EidosNodeStore -->
<script src="frontend/eidos-tree.js"> <!-- EidosTree; may _ingest nodes INTO node store -->
<script src="frontend/app.js"> <!-- const parentMap = window.EidosTree._parentOf -->