Skip to content

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, or parentMap() from frontend/**/*.js except app.js and 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 a parentMap read.

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 -->