Context
Operator review on 2026-05-21 surfaced a small but real Neo.collection.Base edge case while discussing the current isItem() implementation:
isItem(value) {
return typeof value === 'object'
}The broad typeof value === 'object' predicate correctly preserves the post-#5954 behavior for Neo instances, but it also classifies null as an item because JavaScript reports typeof null === 'object'.
The Problem
Neo.collection.Base#isItem(null) currently returns true. Callers such as splice() use isItem(item) ? me.getKey(item) : item when removing keyed entries. A null remove entry therefore routes into getKey(null), which can throw a TypeError instead of treating null as a non-item key/value or gracefully no-oping through the map lookup.
This is the same failure class surfaced during recent graph-cleanup work: a null-like value entering a collection removal path should not be promoted into object/item handling.
The Architectural Reality
src/collection/Base.mjs:1294 defines isItem(value) as typeof value === 'object'.
src/collection/Base.mjs:1483 uses isItem(item) during splice() removal to decide whether to call getKey(item).
- Historical closed issue #5954 intentionally broadened
isItem() because collections can store Neo instances, so this ticket must preserve broad object-like support.
- Historical closed issue #5391 introduced the item-recognition helper for record/object handling. This ticket is not a reversion to
Neo.isObject() / Neo.isRecord().
- Historical closed issue #9311 handled sorting null/undefined behavior, but not item/key classification.
The Fix
Update Neo.collection.Base#isItem(value) to explicitly exclude null while retaining broad object-like instance support:
return value !== null && typeof value === 'object'
Add focused unit coverage under test/playwright/unit/collection/Base.spec.mjs or an adjacent collection unit spec proving:
collection.isItem(null) returns false.
- Existing object-like/Neo-instance item behavior is preserved.
- Removing
null through a keyed removal path does not call getKey(null) / does not throw.
Acceptance Criteria
Out of Scope
- Refactoring collection key semantics.
- Replacing
isItem() with Neo.isObject() / Neo.isRecord().
- Changing sorter null ordering or data-store hydration behavior.
Avoided Traps
- Reverting to strict object/record checks: rejected because #5954 documents that collections can store Neo instances, and removal of Neo instances must keep working.
- Treating this as a general null-safety sweep: rejected; the quick-win scope is the specific item/key classification edge.
Related
- #5954 — historical reason for broad object-like item handling.
- #5391 — historical item helper introduction.
- #9311 — related but separate null/undefined sorting behavior.
- PR #11700 — current session that surfaced nearby collection/null failure-mode sensitivity.
Origin Session ID: d13c94dd-e721-4e28-ac9e-4d0b3c0f66de
Retrieval Hint: query_raw_memories(query="collection Base isItem null getKey remove null TypeError quick win")
Context
Operator review on 2026-05-21 surfaced a small but real
Neo.collection.Baseedge case while discussing the currentisItem()implementation:isItem(value) { // We can not use Neo.isObject() || Neo.isRecord(), since collections can store neo instances too. return typeof value === 'object' }The broad
typeof value === 'object'predicate correctly preserves the post-#5954 behavior for Neo instances, but it also classifiesnullas an item because JavaScript reportstypeof null === 'object'.The Problem
Neo.collection.Base#isItem(null)currently returnstrue. Callers such assplice()useisItem(item) ? me.getKey(item) : itemwhen removing keyed entries. Anullremove entry therefore routes intogetKey(null), which can throw a TypeError instead of treatingnullas a non-item key/value or gracefully no-oping through the map lookup.This is the same failure class surfaced during recent graph-cleanup work: a null-like value entering a collection removal path should not be promoted into object/item handling.
The Architectural Reality
src/collection/Base.mjs:1294definesisItem(value)astypeof value === 'object'.src/collection/Base.mjs:1483usesisItem(item)duringsplice()removal to decide whether to callgetKey(item).isItem()because collections can store Neo instances, so this ticket must preserve broad object-like support.Neo.isObject()/Neo.isRecord().The Fix
Update
Neo.collection.Base#isItem(value)to explicitly excludenullwhile retaining broad object-like instance support:return value !== null && typeof value === 'object'Add focused unit coverage under
test/playwright/unit/collection/Base.spec.mjsor an adjacent collection unit spec proving:collection.isItem(null)returnsfalse.nullthrough a keyed removal path does not callgetKey(null)/ does not throw.Acceptance Criteria
Neo.collection.Base#isItem(null)returnsfalse.remove(null)or equivalentsplice()removal-path behavior without throwing.Out of Scope
isItem()withNeo.isObject()/Neo.isRecord().Avoided Traps
Related
Origin Session ID: d13c94dd-e721-4e28-ac9e-4d0b3c0f66de
Retrieval Hint:
query_raw_memories(query="collection Base isItem null getKey remove null TypeError quick win")