@@ -211,21 +211,26 @@ function emptyFacts(): ObjectFacts {
211211 * where a first pass reported ~40 correct keys as orphans:
212212 *
213213 * 1. A view record is a CONTAINER, not a view. The default list sits at
214- * `list`; the named tabs at `listViews.<key>` and `formViews.<key>`, each
215- * of which may also carry its own `name`. Both the map key and the inner
216- * `name` are accepted — authors write either, and the key is what the
217- * console renders the tab from.
214+ * `list`; the named tabs at `listViews.<key>` and `formViews.<key>`.
218215 * 2. The object binding lives INSIDE the container (`list.data.object`), not
219216 * at the record root. A record-level lookup alone resolves to nothing on
220217 * the canonical shape, which silently drops the whole record — a rule that
221218 * then reports every view key the app ships.
222219 *
223- * The default `list` is the ONE place where "read the author's `name`" was
224- * wrong, and #5164 is why: the runtime does not key that view by its `name`,
225- * it keys it by the identity the composer assigns. Its key therefore comes
226- * from {@link defaultListViewKey} — asked of the composer, never re-derived
227- * here. See that function for the three facts that live in the composer and
228- * nowhere else.
220+ * NOWHERE here is the author's inner `name` a `_views` key. This rule used to
221+ * read it on the named branches too ("authors write either", from the HotCRM
222+ * corpus) — but the composer constructs every named entry's runtime identity
223+ * from the MAP KEY alone and ignores `name` entirely, so an inner `name` that
224+ * diverges from its map key is a key the runtime never resolves, and #5164
225+ * (ruled 2026-08-06: canonical = the runtime identity's bare key) applies to
226+ * the named branches exactly as it applies to the default `list` (#6422).
227+ * Every `_views` key therefore comes from the composer: the default list's
228+ * via {@link defaultListViewKey}, the named entries' via {@link namedViewKeys}
229+ * — asked, never re-derived. See those functions for the composer facts that
230+ * live there and nowhere else (collision renames included: `formViews.default`
231+ * beside a default `list` is registered as `default_2`, because the rename IS
232+ * the registry key — the map key it was written under resolves to the OTHER
233+ * view).
229234 *
230235 * A third thing was learned later, from the showcase (#5415): the container's
231236 * DEFAULT form (`form`) is a section anchor too. It is not one of the
@@ -268,14 +273,20 @@ function collectViewRecord(view: AnyRec, factsFor: (objectName: string) => Objec
268273 if ( isRec ( view . list ) ) addView ( listBinding , defaultListViewKey ( listBinding , view ) ) ;
269274 addView ( recordObject ?? listBinding , strName ( view . name ) ) ;
270275
271- for ( const key of [ 'listViews' , 'formViews' ] as const ) {
272- const container = view [ key ] ;
276+ const named = namedViewKeys ( view ) ;
277+ for ( const family of [ 'listViews' , 'formViews' ] as const ) {
278+ const container = view [ family ] ;
273279 if ( ! isRec ( container ) ) continue ;
274- for ( const [ subKey , sub ] of Object . entries ( container ) ) {
280+ const registryKeys = family === 'listViews' ? named . list : named . form ;
281+ let at = 0 ;
282+ for ( const sub of Object . values ( container ) ) {
283+ // Advance in lockstep with the composer: it makes an item for every
284+ // object-typed value (arrays included), so the index moves for each.
285+ if ( ! sub || typeof sub !== 'object' ) continue ;
286+ const registryKey = registryKeys [ at ++ ] ;
275287 if ( ! isRec ( sub ) ) continue ;
276288 const binding = bindingOf ( sub ) ?? listBinding ;
277- addView ( binding , subKey ) ;
278- addView ( binding , strName ( sub . name ) ) ;
289+ addView ( binding , registryKey ) ;
279290 addSections ( sub , binding ) ;
280291 }
281292 }
@@ -343,6 +354,55 @@ function defaultListViewKey(object: string | undefined, container: AnyRec): stri
343354 return item . name . startsWith ( prefix ) ? item . name . slice ( prefix . length ) : item . name ;
344355}
345356
357+ /**
358+ * The bare `_views` keys the RUNTIME assigns to a container's NAMED entries —
359+ * `listViews.<key>` / `formViews.<key>` — in authoring order per family.
360+ *
361+ * {@link defaultListViewKey}'s sibling, and a thin reader of the same composer
362+ * (#6422, the named-branch limb of the #5164 ruling): the composer constructs
363+ * each named entry's identity from the MAP KEY alone — the inner `name` is
364+ * ignored — and renames on collision (`<object>.default` already claimed by
365+ * the default `list` ⇒ `formViews.default` is registered as
366+ * `<object>.default_2`). Both facts live in the composer and nowhere else, so
367+ * both are asked of it, never re-derived. The rename matters even though the
368+ * view-ref lint makes collisions a hard error: this rule must agree with the
369+ * registry, not with another rule staying strict — under a collision the map
370+ * key spelled by the author resolves to the OTHER view, and the renamed key is
371+ * the one the bundle must spell.
372+ *
373+ * Alignment with the composer is positional and rests on two documented facts
374+ * of `expandViewContainerWithDiagnostics`: each family expands its named map's
375+ * entries FIRST (defaults are appended after), and it makes an item for every
376+ * object-typed value in the map, in `Object.entries` order. So the first N
377+ * items of a family are the named entries, index-aligned with the map.
378+ *
379+ * The object passed to the composer is a fixed probe: every identity it
380+ * assigns is `${object}.${key}` with the SAME object, so the bare key —
381+ * renames included — does not depend on it, and a container whose entries bind
382+ * different objects still gets each key filed under its own entry's binding by
383+ * the caller.
384+ */
385+ function namedViewKeys ( container : AnyRec ) : {
386+ list : Array < string | undefined > ;
387+ form : Array < string | undefined > ;
388+ } {
389+ const object = 'probe' ;
390+ const prefix = `${ object } .` ;
391+ const bare = ( name : string ) => ( name . startsWith ( prefix ) ? name . slice ( prefix . length ) : name ) ;
392+ const countEntries = ( v : unknown ) =>
393+ isRec ( v ) ? Object . values ( v ) . filter ( ( e ) => ! ! e && typeof e === 'object' ) . length : 0 ;
394+ const listCount = countEntries ( container . listViews ) ;
395+ const formCount = countEntries ( container . formViews ) ;
396+ if ( ! listCount && ! formCount ) return { list : [ ] , form : [ ] } ;
397+ const items = expandViewContainer ( object , container ) ;
398+ const keysOf = ( kind : 'list' | 'form' , count : number ) =>
399+ items
400+ . filter ( ( i ) => i . viewKind === kind )
401+ . slice ( 0 , count )
402+ . map ( ( i ) => bare ( i . name ) ) ;
403+ return { list : keysOf ( 'list' , listCount ) , form : keysOf ( 'form' , formCount ) } ;
404+ }
405+
346406/** The object a view (or one of its containers) binds to, across the shapes it is authored in. */
347407function viewObjectName ( view : AnyRec ) : string | undefined {
348408 return (
0 commit comments