A consumer that renders a designed list through Neo.grid.Container (a single component column, no header, cards as cells — the Fleet Manager's memories registers, neomjs/neo-agent-institution#131; the mailbox and catch-up surfaces follow the same law) has no selection semantics: its only act is a click inside the card. The engine still marks the clicked row: grid.Body#beforeSetSelectionModel runs ClassSystemUtil.beforeSetInstance(value, RowModel), and beforeSetInstance instantiates the default class for a falsy value — so the body's selectionModel_: null default becomes a RowModel at construction, grid.Container#applyViewSelectionModel(me.view.selectionModel || me.body?.selectionModel) hoists it to the grid.View, and every click paints .neo-selected. Measured 2026-09-12 on the memories register at pin dev@28e56e1543 (the Institution's Neural Link witness: the clicked row's neo-selected is true with no selection model declared anywhere by the consumer).
The Problem
There is no way to say "this grid selects nothing", and the reason is structural: the selection model is still CONFIGURED on grid.Body, while the multi-body design (#9486, phase #9492; the View-owned single instance of #12758 / PR #12784) says the View owns selection and the bodies are render/event delegates. The body-level config survived that refactor as a forwarding seam — body.selectionModel hoists to the View, the locked bodies are created with selectionModel: null "so they do not clone", which the body defaults to a transient RowModel each (measured 2026-09-13: two extra instances per grid on dev@5dc6b80fef), and BaseModel.getActivePeers() still fans row updates out to peers that no longer exist. Every attempt to express "none" at the body level is a sentinel on the wrong owner (PR #18656, drafted and superseded: false on the body — the direction the maintainer ruled out on 2026-09-13: a VIEW is in charge of selections, not bodies; grid bodies should not get selection models at all).
The Architectural Reality
src/grid/View.mjs — selectionModel_: null (~75) with beforeSetSelectionModel keeping a falsy value and afterSetSelectionModel → value?.register(this): the View already owns the instance's lifecycle (destroy on swap, registration, keyboard keys). The static config declares keys before selectionModel_, so a model registered at construction finds the key-navigation carrier.
src/grid/Body.mjs — selectionModel_: null (~222) + beforeSetSelectionModel (instantiates the default) + afterSetSelectionModel (forwards a post-construction swap up); readers: the selectedCells / selectedRows getters, onStoreRecordChange, toJSON; src/grid/Row.mjs destructures selectionModel / selectedCells off the body for the paint.
src/grid/Container.mjs — creates the View with a fixed config (construct, ~284: no consumer seam), the locked bodies with selectionModel: null (849, 874), and hoists in createOrUpdateSubGrids → applyViewSelectionModel (~904–934).
src/selection/grid/BaseModel.mjs — getActivePeers() (~252) reads container.bodyStart?.selectionModel … and seven call sites fan updateRows out to peers (ColumnModel.mjs:79 too) — dead since the single instance, still executed.
Consumers configure through the body: body: {selectionModel: X} (examples/grid/container, examples/grid/cellEditing, apps/email/view/MainView.mjs), grid.body.selectionModel = X (examples/grid/bigData, examples/grid/treeBigData controls), setProperties(gridId, {'body.selectionModel': …}) (test/playwright/e2e/grid/SelectionMultiBody.spec.mjs, GridViewFocus.spec.mjs); the guides learn/guides/datahandling/Grids.md:589 and learn/guides/fundamentals/InstanceLifecycle.md:268–275 teach the body as the owner.
The Fix
Complete the View ownership. The View is the only place a selection model is configured, instantiated, registered and destroyed; the bodies carry none.
grid.View — selectionModel_ defaults to the RowModel class (the default lives with the owner); null means NONE — no instance, no row ever marked, no selection handler; a config or instance is the explicit model. beforeSetSelectionModel keeps normalizing every falsy value to null.
grid.Container — a viewConfig (plain object, the …Config idiom) merged into the View's creation is the consumer seam: viewConfig: {selectionModel: null} / {selectionModel: {module: CellModel}}; grid.view.selectionModel = X is the dynamic write. applyViewSelectionModel and the locked bodies' selectionModel lines are deleted — there is nothing to hoist.
grid.Body — selectionModel_, its two hooks, selectedRecordField and the selectedCells / selectedRows readers are deleted. A body exposes no model — there is no accessor; a setter throws the migration pointer, so a leftover body: {selectionModel: …} fails loudly at construction instead of silently defaulting. Row paints the View's projected selection (view.selectedRows / view.selectedCells) and never mutates it; Body#onStoreRecordChange only repaints the row it renders.
The selection policy leaves the render delegates. The View owns selectedRecordField and follows the store once (bindStore, from onConstructed and from grid.Container#afterSetStore on a store swap): records flagged in the data are adopted through the model's own selectRow when the store binds or loads — the model's policy applies, singleSelect included — and a flag change reaches the model exactly once, however many bodies render the record. View#createViewData re-projects the bodies for the column-selecting models, which call it on the View they register with; View#destroy retires the model and the store listeners. The flag is followed under the row-selecting models — RowModel and the combined CellRowModel / CellColumnRowModel, the set the old Body#selectedRows getter served (ntype containing row) — through one predicate, View#rowSelectionModel, that the paint getters share; a cell or column model leaves a record's flag alone.
selection/grid/BaseModel — getActivePeers() and its fan-out call sites (and ColumnModel's) are deleted: one instance spans all bodies through updateRows already.
Consumers and guides — the three body: {selectionModel} configs move to viewConfig, the two controls and the two e2e specs write view.selectionModel, the unit spec drops the body-level selectionModel: null, the two guides name the View as the owner.
Contract Ledger
Source of Authority for every row: this ticket, the maintainer's ruling on PR #18656 (2026-09-13), the multi-body design #9486 / #12758. Surface-Anchor V-B-A: every existing name below was looked up at head 479337210c of agent/18626-view-owned-selection (2026-09-13); the line numbers are that head's.
Target Surface
Proposed Behavior
Fallback / Edge Case
Docs
Evidence
grid.Container#viewConfig — new config (Container.mjs:115)
a plain object spread first into the View's construction config (Container.mjs:293); the container's own keys (appName, flex, gridContainer, items, …) win
null (the default) merges nothing; after construction the View is written directly (grid.view.<config>), viewConfig is not a live proxy
the default is the RowModel CLASS: one instance, created and registered by the View in onConstructed (after keys and the bodies); a config or instance is that one model; a post-construction swap destroys the previous instance and registers the new one at once
any falsy value → null: no instance, no handler, no selection wrapper cls, no row marked — a flagged record included; a bare grid.View without a container says selectionModel: null
grid.View#selectedRecordField — moved from grid.Body (View.mjs:86)
'annotations.selected'; the View follows the store once per grid — bindStore (View.mjs:235) from onConstructed, re-bound by Container#afterSetStore (Container.mjs:569): flagged records are adopted through the model's own selectRow at bind and on load, a flag change makes one selectRow / deselectRow — under the row-selecting models RowModel, CellRowModel, CellColumnRowModel
no row-selecting model (null, a cell or a column model) → the flag is left alone; the model's policy applies (singleSelect keeps the last flagged record)
JSDoc
unit flag arms: once per change with three bodies; adoption under policy and never under null; every row-selecting model, with cell / column controls
grid.View#rowSelectionModel, #selectedRows, #selectedCells — new readers, the latter two moved from grid.Body (View.mjs:126, 144, 135)
the one model when it selects rows, else null; the model's selectedRows / items under a row- / cell-selecting model
null / [] under none and under the other axis
JSDoc
Row.mjs:359, 411 paint from them; unit AC1 + "none" arms
grid.View#createViewData(silent, force) — new (View.mjs:253)
fans to every body's createViewData; the column-selecting models call it on the View they register with (ColumnModel.mjs:78, 119, 146; CellColumnModel.mjs:46–97; CellColumnRowModel.mjs:46–77)
no bodies → no-op
JSDoc
unit ColumnModel → null arm
grid.Body#selectionModel — REMOVED config; a throwing setter remains (Body.mjs:653)
body.selectionModel reads undefined; an assignment — a leftover body: {selectionModel: …} included — throws grid.Body carries no selection model: configure it on the View — grid.Container#viewConfig.selectionModel, or grid.view.selectionModel at runtime
gone: one instance spans the bodies through updateRows
—
—
unit null arm asserts both undefined; grep -rn over src returns nothing
grid.view.selectionModel = X — the runtime write (existing setter)
the swap above, from consumer code (examples/grid/bigData / treeBigData controls)
null clears the grid
Grids.md:576
unit AC2; e2e setProperties(gridId, {'view.selectionModel': …})
Acceptance Criteria
AC-1 viewConfig: {selectionModel: null} yields a grid with no selection model (view.selectionModel === null), no RowModel instantiated anywhere, no selection wrapper cls on the View, and no rendered row carrying neo-selected / aria-selected — a record flagged in the data before construction included.
AC-2 The default (no viewConfig) instantiates exactly ONE RowModel for the whole grid — on the View — registered there (model.view === grid.view), with locked start and end bodies present; a RowModel construction spy counts one; a selection made through the model paints in every body.
AC-3 An explicit model (viewConfig: {selectionModel: {module: CellModel}} at construction, grid.view.selectionModel = … afterwards) is the one instance the View holds; a swap destroys the previous instance; no RowModel is created beside it; a column-selecting model swaps to null without a throw and re-projects every body.
AC-4 grid.Body declares no selectionModel config and exposes none (body.selectionModel reads undefined); a body: {selectionModel: …} config throws at construction with the migration message; BaseModel.getActivePeers and grid.Container#applyViewSelectionModel no longer exist.
AC-5 Unit arms for AC-1–AC-4 and AC-6, running in the unit CI job (the spec carries no NEO_TEST_SKIP_CI guard — a guarded arm is green in CI by not running); AC-1, AC-2's instance count and AC-4 are red on the current code. The existing multi-body arms (ViewOwnedSelectionModel.spec AC1/AC2 restated on the View, SelectionMultiBody, GridViewFocus, BigDataMultiBodyNL) stay green; cross-body row selection and keyboard selection through the View's default model are exercised by the e2e arms in CI.
AC-6 The selection policy lives with the owner: a record flag change reaches the model exactly once with three bodies present; records flagged before construction are adopted through the model under its own policy and never under null; CellRowModel and CellColumnRowModel follow the flag exactly as RowModel does — a pre-flagged record adopted at bind, a flag set after the render followed under the model's policy — while a cell or column model leaves it alone; lock / unlock churn recreates bodies and never a model; grid.destroy() destroys the model (gone from the instance manager) while removing a locked body does not.
Out of Scope
The Institution's skin-layer neutralization (neomjs/neo-agent-institution#131) — it retires at the next engine pin with viewConfig: {selectionModel: null} on the registers; that is its named retirement trigger.
Neo.table.Body's own selectionModel_ (the table family), Neo.list.Base selection.
The selection-model class hierarchy refactor (#9075) and keyboard navigation across bodies (#9496).
Avoided Traps
A sentinel on the body (false = none). It keeps the body as a configuration owner and needs the hoist to translate it; the View already keeps null as null. Superseded PR #18656 is the measured dead end.
A selectionModel proxy config on grid.Container. Two stored truths for one instance; the View is the owner, viewConfig only passes construction config.
Registering the default too early. The View's keys config precedes selectionModel_, so RowModel.register finds view.keys at construction; the e2e keyboard arm is the tripwire.
A silent legacy path. Without the throwing setter, body: {selectionModel: X} would assign a dead property and the grid would silently run the default.
A readable alias on the body, and a policy in the render. The first cut kept a read-only Body.selectionModel and left Row#createVdom pushing flagged records into the live selectedRows array (which marked a pre-flagged record even under an explicit null) and Body#onStoreRecordChange calling selectRow per body (three calls per change). @neo-gpt-emmy's ownership audit measured all of it on real instances (https://github.com/neomjs/neo/issues/18626#issuecomment-5653062788, the exact-head replay https://github.com/neomjs/neo/issues/18626#issuecomment-5653280033); the paint reads the View, the policy runs there once.
Adopting flagged records outside the model's policy. The render-time adoption marked every flagged record regardless of singleSelect; the owner adopts through selectRow, so the model decides.
An adoption gate narrower than the paint's. The second cut adopted the flag only under ntype === 'selection-grid-rowmodel' while the deleted render push had served every model whose ntype contains row: CellRowModel and CellColumnRowModel lost annotation-driven selection, the gesture path included (CellRowModel#onCellClick → updateAnnotations → recordChange → the View). Measured by @neo-gpt-emmy's expanded probe, baseline 5dc6b80fef against 8d607b944f. One predicate (View#rowSelectionModel) now feeds the paint and the adoption; a declared row-selection trait on the model classes belongs to the hierarchy refactor #9075.
A CI skip guard copied from the siblings. The spec's first cuts carried the directory's NEO_TEST_SKIP_CI "bucket B" guard, which the unit CI job sets — the arms were green in CI by not running while the PR called them CI-covered. Nothing in the spec needs a browser; the guard is gone (BodyWindowIsolation.spec is the precedent).
Decision Record impact
aligned-with#9486 (the multi-body architecture) and the #9868 R&D direction; completes #12758.
Related
#9486 (epic) · #9492 (phase 6, the open selection sub) · #12758 / PR #12784 (the View-owned single instance) · #9075 (the model-hierarchy refactor, separate) · PR #18656 (superseded) · neomjs/neo-agent-institution#131 (the consumer that measured it) · neomjs/neo-agent-institution#44 · neomjs/neo-agent-institution#24.
Live latest-open sweep (2026-09-12T17:06Z, at creation): no equivalent among the latest 20 open issues; #9492 adapts selection for split rows, it does not remove the body seam. 2026-09-13 re-sweep: #9075 / #9492 / #9872 open and adjacent, none owns the body-level config removal. A2A in-flight claim sweep: no [lane-claim] on grid selection other than mine. Own-assignment sweep: #18604 (a different surface).
Body revised 2026-09-13 after the maintainer's design ruling on PR #18656 (bodies carry no selection model; the View is in charge): Problem, Reality, Fix, ACs and Avoided Traps rewritten; the first Fix (a false sentinel on grid.Body) is recorded as the superseded shape. Revised again the same day after @neo-gpt-emmy's ownership audit: Fix 3 loses the readable accessor, the render-time and per-body selection policy moves to the View (Fix 4), AC-6 added. Revised a third time the same day after her expanded probe (the combined row models lost the flag at 8d607b944f) and her evidence-precision catch (the inherited CI skip guard): Contract Ledger added, Fix 4 / AC-5 / AC-6 name the row-selecting set and the CI admission, two traps added.
Context
A consumer that renders a designed list through
Neo.grid.Container(a single component column, no header, cards as cells — the Fleet Manager's memories registers, neomjs/neo-agent-institution#131; the mailbox and catch-up surfaces follow the same law) has no selection semantics: its only act is a click inside the card. The engine still marks the clicked row:grid.Body#beforeSetSelectionModelrunsClassSystemUtil.beforeSetInstance(value, RowModel), andbeforeSetInstanceinstantiates the default class for a falsy value — so the body'sselectionModel_: nulldefault becomes aRowModelat construction,grid.Container#applyViewSelectionModel(me.view.selectionModel || me.body?.selectionModel)hoists it to thegrid.View, and every click paints.neo-selected. Measured 2026-09-12 on the memories register at pin dev@28e56e1543 (the Institution's Neural Link witness: the clicked row'sneo-selectedistruewith no selection model declared anywhere by the consumer).The Problem
There is no way to say "this grid selects nothing", and the reason is structural: the selection model is still CONFIGURED on
grid.Body, while the multi-body design (#9486, phase #9492; the View-owned single instance of #12758 / PR #12784) says the View owns selection and the bodies are render/event delegates. The body-level config survived that refactor as a forwarding seam —body.selectionModelhoists to the View, the locked bodies are created withselectionModel: null"so they do not clone", which the body defaults to a transientRowModeleach (measured 2026-09-13: two extra instances per grid on dev@5dc6b80fef), andBaseModel.getActivePeers()still fans row updates out to peers that no longer exist. Every attempt to express "none" at the body level is a sentinel on the wrong owner (PR #18656, drafted and superseded:falseon the body — the direction the maintainer ruled out on 2026-09-13: a VIEW is in charge of selections, not bodies; grid bodies should not get selection models at all).The Architectural Reality
src/grid/View.mjs—selectionModel_: null(~75) withbeforeSetSelectionModelkeeping a falsy value andafterSetSelectionModel→value?.register(this): the View already owns the instance's lifecycle (destroy on swap, registration, keyboard keys). The static config declareskeysbeforeselectionModel_, so a model registered at construction finds the key-navigation carrier.src/grid/Body.mjs—selectionModel_: null(~222) +beforeSetSelectionModel(instantiates the default) +afterSetSelectionModel(forwards a post-construction swap up); readers: theselectedCells/selectedRowsgetters,onStoreRecordChange,toJSON;src/grid/Row.mjsdestructuresselectionModel/selectedCellsoff the body for the paint.src/grid/Container.mjs— creates the View with a fixed config (construct, ~284: no consumer seam), the locked bodies withselectionModel: null(849, 874), and hoists increateOrUpdateSubGrids→applyViewSelectionModel(~904–934).src/selection/grid/BaseModel.mjs—getActivePeers()(~252) readscontainer.bodyStart?.selectionModel… and seven call sites fanupdateRowsout to peers (ColumnModel.mjs:79too) — dead since the single instance, still executed.body: {selectionModel: X}(examples/grid/container,examples/grid/cellEditing,apps/email/view/MainView.mjs),grid.body.selectionModel = X(examples/grid/bigData,examples/grid/treeBigDatacontrols),setProperties(gridId, {'body.selectionModel': …})(test/playwright/e2e/grid/SelectionMultiBody.spec.mjs,GridViewFocus.spec.mjs); the guideslearn/guides/datahandling/Grids.md:589andlearn/guides/fundamentals/InstanceLifecycle.md:268–275teach the body as the owner.The Fix
Complete the View ownership. The View is the only place a selection model is configured, instantiated, registered and destroyed; the bodies carry none.
grid.View—selectionModel_defaults to theRowModelclass (the default lives with the owner);nullmeans NONE — no instance, no row ever marked, no selection handler; a config or instance is the explicit model.beforeSetSelectionModelkeeps normalizing every falsy value tonull.grid.Container— aviewConfig(plain object, the…Configidiom) merged into the View's creation is the consumer seam:viewConfig: {selectionModel: null}/{selectionModel: {module: CellModel}};grid.view.selectionModel = Xis the dynamic write.applyViewSelectionModeland the locked bodies'selectionModellines are deleted — there is nothing to hoist.grid.Body—selectionModel_, its two hooks,selectedRecordFieldand theselectedCells/selectedRowsreaders are deleted. A body exposes no model — there is no accessor; a setter throws the migration pointer, so a leftoverbody: {selectionModel: …}fails loudly at construction instead of silently defaulting.Rowpaints the View's projected selection (view.selectedRows/view.selectedCells) and never mutates it;Body#onStoreRecordChangeonly repaints the row it renders.selectedRecordFieldand follows the store once (bindStore, fromonConstructedand fromgrid.Container#afterSetStoreon a store swap): records flagged in the data are adopted through the model's ownselectRowwhen the store binds or loads — the model's policy applies,singleSelectincluded — and a flag change reaches the model exactly once, however many bodies render the record.View#createViewDatare-projects the bodies for the column-selecting models, which call it on the View they register with;View#destroyretires the model and the store listeners. The flag is followed under the row-selecting models —RowModeland the combinedCellRowModel/CellColumnRowModel, the set the oldBody#selectedRowsgetter served (ntypecontainingrow) — through one predicate,View#rowSelectionModel, that the paint getters share; a cell or column model leaves a record's flag alone.selection/grid/BaseModel—getActivePeers()and its fan-out call sites (andColumnModel's) are deleted: one instance spans all bodies throughupdateRowsalready.body: {selectionModel}configs move toviewConfig, the two controls and the two e2e specs writeview.selectionModel, the unit spec drops the body-levelselectionModel: null, the two guides name the View as the owner.Contract Ledger
Source of Authority for every row: this ticket, the maintainer's ruling on PR #18656 (2026-09-13), the multi-body design #9486 / #12758. Surface-Anchor V-B-A: every existing name below was looked up at head
479337210cofagent/18626-view-owned-selection(2026-09-13); the line numbers are that head's.grid.Container#viewConfig— new config (Container.mjs:115)Container.mjs:293); the container's own keys (appName,flex,gridContainer,items, …) winnull(the default) merges nothing; after construction the View is written directly (grid.view.<config>),viewConfigis not a live proxylearn/guides/datahandling/Grids.md:576–590grid.View#selectionModel_— existing config, default changed (View.mjs:78)RowModelCLASS: one instance, created and registered by the View inonConstructed(afterkeysand the bodies); a config or instance is that one model; a post-construction swap destroys the previous instance and registers the new one at oncenull: no instance, no handler, no selection wrapper cls, no row marked — a flagged record included; a baregrid.Viewwithout a container saysselectionModel: nullInstanceLifecycle.md:268–275SelectionMultiBody,GridViewFocus,BigDataMultiBodyNLgrid.View#selectedRecordField— moved fromgrid.Body(View.mjs:86)'annotations.selected'; the View follows the store once per grid —bindStore(View.mjs:235) fromonConstructed, re-bound byContainer#afterSetStore(Container.mjs:569): flagged records are adopted through the model's ownselectRowat bind and onload, a flag change makes oneselectRow/deselectRow— under the row-selecting modelsRowModel,CellRowModel,CellColumnRowModelnull, a cell or a column model) → the flag is left alone; the model's policy applies (singleSelectkeeps the last flagged record)null; every row-selecting model, with cell / column controlsgrid.View#rowSelectionModel,#selectedRows,#selectedCells— new readers, the latter two moved fromgrid.Body(View.mjs:126, 144, 135)null; the model'sselectedRows/itemsunder a row- / cell-selecting modelnull/[]under none and under the other axisRow.mjs:359, 411paint from them; unit AC1 + "none" armsgrid.View#createViewData(silent, force)— new (View.mjs:253)createViewData; the column-selecting models call it on the View they register with (ColumnModel.mjs:78, 119, 146;CellColumnModel.mjs:46–97;CellColumnRowModel.mjs:46–77)grid.Body#selectionModel— REMOVED config; a throwing setter remains (Body.mjs:653)body.selectionModelreadsundefined; an assignment — a leftoverbody: {selectionModel: …}included — throwsgrid.Body carries no selection model: configure it on the View — grid.Container#viewConfig.selectionModel, or grid.view.selectionModel at runtimegrid.Body#selectedRecordField,#selectedRows,#selectedCells— REMOVEDRowreads the Viewgrep -noverBody.mjs/Row.mjsat the head finds only the setter and the View readsgrid.Container#applyViewSelectionModel,selection.grid.BaseModel#getActivePeers— REMOVEDupdateRowsundefined;grep -rnoversrcreturns nothinggrid.view.selectionModel = X— the runtime write (existing setter)examples/grid/bigData/treeBigDatacontrols)nullclears the gridsetProperties(gridId, {'view.selectionModel': …})Acceptance Criteria
viewConfig: {selectionModel: null}yields a grid with no selection model (view.selectionModel === null), noRowModelinstantiated anywhere, no selection wrapper cls on the View, and no rendered row carryingneo-selected/aria-selected— a record flagged in the data before construction included.viewConfig) instantiates exactly ONERowModelfor the whole grid — on the View — registered there (model.view === grid.view), with locked start and end bodies present; aRowModelconstruction spy counts one; a selection made through the model paints in every body.viewConfig: {selectionModel: {module: CellModel}}at construction,grid.view.selectionModel = …afterwards) is the one instance the View holds; a swap destroys the previous instance; noRowModelis created beside it; a column-selecting model swaps tonullwithout a throw and re-projects every body.grid.Bodydeclares noselectionModelconfig and exposes none (body.selectionModelreadsundefined); abody: {selectionModel: …}config throws at construction with the migration message;BaseModel.getActivePeersandgrid.Container#applyViewSelectionModelno longer exist.NEO_TEST_SKIP_CIguard — a guarded arm is green in CI by not running); AC-1, AC-2's instance count and AC-4 are red on the current code. The existing multi-body arms (ViewOwnedSelectionModel.specAC1/AC2 restated on the View,SelectionMultiBody,GridViewFocus,BigDataMultiBodyNL) stay green; cross-body row selection and keyboard selection through the View's default model are exercised by the e2e arms in CI.null;CellRowModelandCellColumnRowModelfollow the flag exactly asRowModeldoes — a pre-flagged record adopted at bind, a flag set after the render followed under the model's policy — while a cell or column model leaves it alone; lock / unlock churn recreates bodies and never a model;grid.destroy()destroys the model (gone from the instance manager) while removing a locked body does not.Out of Scope
viewConfig: {selectionModel: null}on the registers; that is its named retirement trigger.Neo.table.Body's ownselectionModel_(the table family),Neo.list.Baseselection.Avoided Traps
false= none). It keeps the body as a configuration owner and needs the hoist to translate it; the View already keepsnullasnull. Superseded PR #18656 is the measured dead end.selectionModelproxy config ongrid.Container. Two stored truths for one instance; the View is the owner,viewConfigonly passes construction config.keysconfig precedesselectionModel_, soRowModel.registerfindsview.keysat construction; the e2e keyboard arm is the tripwire.body: {selectionModel: X}would assign a dead property and the grid would silently run the default.Body.selectionModeland leftRow#createVdompushing flagged records into the liveselectedRowsarray (which marked a pre-flagged record even under an explicitnull) andBody#onStoreRecordChangecallingselectRowper body (three calls per change). @neo-gpt-emmy's ownership audit measured all of it on real instances (https://github.com/neomjs/neo/issues/18626#issuecomment-5653062788, the exact-head replay https://github.com/neomjs/neo/issues/18626#issuecomment-5653280033); the paint reads the View, the policy runs there once.singleSelect; the owner adopts throughselectRow, so the model decides.ntype === 'selection-grid-rowmodel'while the deleted render push had served every model whosentypecontainsrow:CellRowModelandCellColumnRowModellost annotation-driven selection, the gesture path included (CellRowModel#onCellClick→updateAnnotations→recordChange→ the View). Measured by @neo-gpt-emmy's expanded probe, baseline5dc6b80fefagainst8d607b944f. One predicate (View#rowSelectionModel) now feeds the paint and the adoption; a declared row-selection trait on the model classes belongs to the hierarchy refactor #9075.NEO_TEST_SKIP_CI"bucket B" guard, which the unit CI job sets — the arms were green in CI by not running while the PR called them CI-covered. Nothing in the spec needs a browser; the guard is gone (BodyWindowIsolation.specis the precedent).Decision Record impact
aligned-with#9486 (the multi-body architecture) and the #9868 R&D direction; completes #12758.Related
#9486 (epic) · #9492 (phase 6, the open selection sub) · #12758 / PR #12784 (the View-owned single instance) · #9075 (the model-hierarchy refactor, separate) · PR #18656 (superseded) · neomjs/neo-agent-institution#131 (the consumer that measured it) · neomjs/neo-agent-institution#44 · neomjs/neo-agent-institution#24.
Live latest-open sweep (2026-09-12T17:06Z, at creation): no equivalent among the latest 20 open issues; #9492 adapts selection for split rows, it does not remove the body seam. 2026-09-13 re-sweep: #9075 / #9492 / #9872 open and adjacent, none owns the body-level config removal. A2A in-flight claim sweep: no
[lane-claim]on grid selection other than mine. Own-assignment sweep: #18604 (a different surface).Body revised 2026-09-13 after the maintainer's design ruling on PR #18656 (bodies carry no selection model; the View is in charge): Problem, Reality, Fix, ACs and Avoided Traps rewritten; the first Fix (a
falsesentinel ongrid.Body) is recorded as the superseded shape. Revised again the same day after @neo-gpt-emmy's ownership audit: Fix 3 loses the readable accessor, the render-time and per-body selection policy moves to the View (Fix 4), AC-6 added. Revised a third time the same day after her expanded probe (the combined row models lost the flag at8d607b944f) and her evidence-precision catch (the inherited CI skip guard): Contract Ledger added, Fix 4 / AC-5 / AC-6 name the row-selecting set and the CI admission, two traps added.Origin Session ID: 46156fd3-ef37-410c-a3fa-f843df795597
Retrieval Hint: "grid.View owns selectionModel viewConfig null none RowModel default grid.Body no selection model getActivePeers"
📜 Clio · @neo-fable-clio · Claude Fable 5.1 · Claude Code · session 46156fd3-ef37-410c-a3fa-f843df795597