# Studio Record Grouping And Reference Drag-Drop Cut First mapped 2026-09-17 (Imagination) against CultLib `122c217`. **Refreshed 2026-09-29 (Imagination) against CultLib `main` `feeced8` and Aetheria `codex/fire-control-12` `4b594e11`.** Every `file:line` below is against those revisions unless marked. Hands re-reads an anchor before editing it; `main` keeps moving. **Rulings.** Q1 **B** (a class attribute on the model; argument order is nesting order; 2026-09-17). Q2 **A** (occupied values only). Q3 **A** (list only, not the ref picker). Q4 **A** (no record search). Q5 **overtaken**: one release from `main`, together with document-variants C3 (`docs/document-variants-cut.md`), taking whatever native-plugin drift `main` carries. Q6 **A** (flat row by resolved value, C3's variant marker; 2026-09-30). Q7 **B** (legacy parity minus manufacturer, plus `FactionProductData` by `Manufacturer` then `Design`, plus `WeaponItemData` by `WeaponType`; 2026-09-30). ## Target ### Ends 1. **Declared record grouping.** `[CultInspectorGroupBy(nameof(A), nameof(B))]` on a document class makes Studio's record list for that type a foldout tree, nested in argument order. A declaration on a base type groups every derived type listed under it. 2. **Create in a group.** Every group node offers `Create`. The new record gets every grouped member on the path to that node set to that node's value, so it lands in that node. 3. **Reference drag and drop.** A record row dragged from Studio's list onto any `CultRecordRef` value the inspector draws (member, list element, dictionary key or value) is accepted exactly when the ref's picker would offer that record. Recovered from the legacy "Database Tools" window (Aetheria `d3db1730`, `Assets/Scripts/CultCache/Editor/DatabaseView.cs`, `AetheriaDatabaseView.cs`, `Inspectors/DatabaseLinkInspector.cs`). Selector lambdas are rejected (operator, 2026-09-17). **The attribute is not `[CultIndex]`.** `[CultIndex]` means unique (operator, 2026-09-30; `CultDocumentContracts.cs:34`). Grouping is `CultInspectorGroupByAttribute`, in `CultInspectorAttributes.cs`, read only by `CultInspectorModel`. It changes no schema id, catalog entry, index, or store byte. The index cut's deletion of grouping-only `[CultIndex]` declarations (CultMesh, Geometry) adds no `[CultInspectorGroupBy]` in their place: those are service stores, not Studio-authored catalogs. ### Invariants - **I1. The model owns grouping.** `CultInspectorModel` alone decides which members group a listed type and in what order, which declarations are invalid (and the notice), the tree's partition, node identity, order and labels, and the object create-in-group makes. Studio lowers the tree; it never reads the attribute, calls `SetValue` for a preset, or computes a group label. - **I2. The leaves partition the candidates.** Across all leaves the tree holds exactly `RecordCandidates(CultRecordRef, records)`, each record once, in candidate order inside a leaf. Grouping never hides a record, including one whose grouped value is unset, dangling, or an undefined enum value. - **I3. The listed type owns the tree's shape.** Grouping comes from the type selected in the type pane, never from each record's runtime type. - **I4. Node identity is value identity, not the label.** A ref groups by key, an enum by its underlying value, other kinds by the value's invariant string. Foldout state keys on node id. - **I5. Create-in-group round-trips.** Upsert what `CreateInGroup(listed, node)` returns, regroup: the new record lies in a node with the same id. - **I6. One candidate rule.** `IsRecordCandidate(refType, record)` is the only test for "this record may be this ref's value". `RecordCandidates` filters through it; drop acceptance calls it. - **I7. One ref label.** The model owns `None` and `Missing `; the picker and group labels both use it. - **I8. Invalid declarations never break the window.** They give a flat list plus the model's notice, never a throw in `OnGUI`. - **I9. A drop honours the picker's gates.** Refused in a disabled (read-only) scope, when the payload came from another model, and when the key equals the current key. A dictionary-key drop still passes `ReplaceKey` (`CultCacheStudioDrawers.cs:303-309`). - **I10 (new). Grouping reads what readers read.** A variant record groups, labels, and is a drop candidate by its resolved `Document` (`CultCache.cs:369-371`), never its delta. ### Non-goals No sibling-runtime port (TS `cult-cache-inspector.ts` is a byte dumper; Rust and Python have no inspector). No runtime CultUI panel. No search (Q4), no picker grouping (Q3), no drop-to-append on list headers, no legacy `New ` naming. ## Body findings Kept from 2026-09-17, re-anchored: - **F3.** Legacy grouping matched the exact type; grouping derived records under a base declaration is new. - **F4.** Legacy create-in-group was dead (`DatabaseView.cs:70-75` throws); legacy foldouts keyed on the label hash. - **F5.** `member.GetCustomAttributes(true)` ignores `inherit` for a `PropertyInfo`, so a base `[M] abstract P` is not seen on the override. Sites: `CultInspectorModel.cs:36` (now the only metadata site: `MetadataOf` caches it per member, `:341-351`) and `:209` (`CultInspectorDrawerClaims.Resolve`). Fix: `Attribute.GetCustomAttributes(member, true)`. - **F6.** Studio `Add` creates only the listed type (`CultCacheStudioWindow.cs:340-346`, `CreateElement(type, type)`), disabled when the model cannot create it (`:141`). So create-in-group never makes a derived type, and an abstract listed type offers no `Create`. - **F7.** Studio search filters the type pane only (`:112-120`). - **F8.** Foldouts are in-memory (`CultCacheStudioDrawers.cs:33`); `OnDisable` closes the store (`:48-51`), domain reload included. The only `EditorPrefs` key is `LastPathKey` (`:13`). - **F9.** No drawer claims `CultRecordRef<>`, so every ref reaches `DrawRecordRef` (`CultCacheStudioDrawers.cs:254-280`). - **F10.** The Studio editor sources compile outside Unity (netstandard2.1, C# 9, against Unity 6000.3.24f1 `Editor\Data\Managed\UnityEngine\*.dll` without the `UnityEditor.dll` facade, plus the built `GameCult.Caching*` and tracked `MessagePack*.dll`). The 09-17 scratch project is gone; Hands rebuilds it in its own scratch from this recipe. - **F11.** The record row reads its click in `GUILayout.Toggle` (`:184`); a drag start must be handled on a reserved rect before the toggle draws (IMGUI convention, not probed). New since the map: - **F12. Variants (C1, `6ffe17a`).** `CultInspectorModel(registry, CultCodec)` (`CultInspectorModel.cs:302-306`); Studio still calls `CultCacheMessagePack.CreateInspectorModel(registry)` (`CultCacheMessagePack.cs:33-34`, unchanged). `CultStoredDocument.Variant` is the delta (`CultCache.cs:374`); `Document` is the resolved view. A top-level override is a `Set` whose path is one step at the member's slot (`CultVariants.cs:72-73`), so "is this grouped member overridden on this variant" is computable from the member's `Slot`. - **F13. Every Studio commit to a variant key is refused until C3.** `CultInspectorEdit.Commit` is a plain `UpsertAsync` (`CultInspectorModel.cs:266-282`); Q1a refuses a plain write at a variant key (`CultCache.cs:2494-2496`). A drop into a variant's inspector therefore reports that refusal until C3 lands. The release waits for C3, so no shipped Studio shows it, but the operator click-through must run after C3. - **F14. Studio `Duplicate` of a variant makes a flattened plain copy** (`CultCacheStudioWindow.cs:349-353` clones the resolved `Document`). Not this campaign's defect; C3's `CreateVariant` sits beside it. - **F15. Element ids (C2a, `hands/variants-c2a`, WIP `df74f8c`).** No grouping interaction: grouped members are top-level scalars and refs; ids live on list elements. Drops write a ref into the edit document; ids are untouched and minted on write for new elements; C3's diff keys overrides on them. Two consequences: - After C2a, registration refuses a document type whose object-list element lacks a `[CultElementId]` (`CultCache.cs` descriptor build, WIP). Cut 1's new fixtures carry no object lists. - C2a does not hide or lock the id member, so Studio shows it as an editable string. Hand-editing one retargets overrides. That belongs to C3 (make `[CultElementId]` members read-only in the model), not here; flagged for Self. - **F16. The 1.0.60/1.4.0 release shipped** (`0155ba8`, tags on `45c2f40`) and `codex/asset-guid-attribute` is merged. It already carried the 302,080-byte native bridge, so the 09-17 Q5 drift is spent. Native source has moved since (`git diff --stat 45c2f40 main -- native/`: 5 files, +840/-118), so this release rebuilds the bridge again. The v1 exports the managed side imports (`src/GameCult.Mesh.Quic.Native/*.cs:263-276`) remain at `cultmesh_quic_native.cpp:1405-1465`. - **F17. Aetheria's schema moved.** `ItemData.Manufacturer` is gone (slot 3 retired, `ItemData.cs:281-282`). The manufacturer relation now lives on `FactionProductData` (`FactionProduct.cs:27`, `CultRecordRef Manufacturer`; `:24`, `CultRecordRef Design`). Aetheria pins `cultlib-unity-v1.0.60` / `caching-unity-v1.4.0` / `cultmath-unity-v0.2.4` (`Packages/manifest.json:54-56`), `CultLibRevision` `45c2f40` and a separate `CultMathRevision` `6d5e209` (`Directory.Build.props:6,10`). - **F18. After C2a, Aetheria cannot take the new CultLib without its element-id sweep** (`List`, `List`, `List`, `List`, `List`, `List`, among others). So the Aetheria re-pin belongs to variants C4, and this campaign's Cut 4 shrinks to annotations and tests on top of it. ## Identity and authority | Thing | Owner | Lifecycle | Persisted | |---|---|---|---| | `[CultInspectorGroupBy]` | consumer source | compile time; nearest declaration wins; no arguments opts out | source only | | `CultInspectorGrouping` (resolved) | model | cached per listed type like `_shapes` (`CultInspectorModel.cs:296`) | no | | `CultInspectorRecordGroup` tree, node ids, labels | model | rebuilt each `OnGUI`, as `RecordCandidates` is (`CultCacheStudioWindow.cs:167`) | no | | Group foldouts (node id → bool) | window | window memory; cleared by `CloseStore` (`:318-327`) | no | | Drag payload `{ Model, Key }` under `"GameCult.CultCacheStudio.Record"` | Studio list (source) | one drag | no | | Drop acceptance | `IsRecordCandidate` plus I9 gates | per `DragUpdated`/`DragPerform` | no | | Created-in-group object | model makes; `CultCache` admits via `UpsertAsync`, as `Add` | new plain record | store, on `Save` | ## Forks Q1-Q5 are ruled (header). Two remain. ### Q6. RULED A (2026-09-30). How does the grouped list show a variant? (blocks nothing in Cuts 1-2; shapes C3's row lowering) Mechanical defaults, not asked: a variant groups by its resolved values (I10), so a variant that inherits its base's `HullType` sits in the base's node and moves when the base changes; create-in-group always makes a plain record; C3's `CreateVariant` needs no preset because the variant inherits its base's group; a variant is a valid drag payload. - **A. Flat by resolved value.** A variant is an ordinary row in its resolved group. C3 adds a row marker ("variant of Longinus"). Which of its grouped values are overridden shows in the inspector through C3's per-member marker, not in the tree. - **B. Nested under the base.** Inside a leaf, a variant row sits indented under its base's row when both share the leaf; otherwise it stands alone with the marker. - **C. A's rows plus an inherited/overridden glyph on the row** for the grouped members. - **Recommendation: A.** I2 stays one rule, the tree has one shape, and the base link already lives in C3's inspector. B makes the tree's shape depend on two relations, and splits whenever a variant overrides a grouped value. C duplicates C3's inspector marker. ### Q7. RULED B (2026-09-30). Which Aetheria types group, and by what? Candidates on `codex/fire-control-12` (enums unless noted; `[Flags]` enums excluded): | Type | Member | Anchor | Note | |---|---|---|---| | `SimpleCommodityData` | `Category` (`SimpleCommodityCategory`) | `ItemData.cs:310` | legacy parity | | `CompoundCommodityData` | `Category` (`CompoundCommodityCategory`) | `ItemData.cs:338` | legacy parity | | `GearData` | `Hardpoint` (`HardpointType`) | `ItemData.cs:454` | legacy parity; `WeaponItemData` inherits when listed under `GearData` | | `WeaponItemData` | `WeaponType`; or `WeaponCaliber`, `WeaponRange` | `ItemData.cs:485`, `:482`, `:479` | re-declaration, applies when listing weapons | | `HullData` | `HullType` | `ItemData.cs:519` | legacy parity | | `FactionProductData` | `Manufacturer` (ref `Faction`), then `Design` (ref `CraftedItemData`) | `FactionProduct.cs:27`, `:24` | where "by manufacturer" now lives | | `Loadout` | `Hull` (ref `HullData`) | `Loadout.cs:17` | optional | `ConsumableItemData`, `CargoBayData`, `DockingBayData`, `Faction`, `PersonalityAttribute` have no groupable member worth a level. - **A. Legacy parity minus manufacturer:** commodities by `Category`, `GearData` by `Hardpoint`, `HullData` by `HullType`. - **B. A, plus `FactionProductData` by `Manufacturer` then `Design`, plus `WeaponItemData` by `WeaponType`.** - **C. B with `FactionProductData` by `Design` then `Manufacturer`** (the market-segment view: who makes this design). - **D. B plus `Loadout` by `Hull`.** - **Recommendation: B.** It recovers everything the legacy window grouped, puts the manufacturer view where the manufacturer now is, and gives the weapon list its own axis. C is the better view if the operator authors products per design rather than per faction. Product record counts are unprobed; Cut 4's Hands reports them. ## Defaults (recorded, not asked) - **Labels:** enum `Enum.GetName`, else the number; ref `RecordLabel` of the candidate with that key (for `Faction`, `Name`, not legacy `ShortName`); unset ref `None`; dangling `Missing `; string its value or `(empty)`; integer invariant string; bool `False`/`True`. - **Order within a level:** enums by underlying value; refs `None` first, then label (`OrdinalIgnoreCase`, then key), `Missing` last; strings `OrdinalIgnoreCase` then ordinal; integers numeric; `false` before `true`. - **Groupable kinds:** String, Integer, Bool, Enum (not `[Flags]`), RecordRef. Refused with a notice and a flat list: any other kind (Float included), a `[Flags]` enum, `Hidden`, `IsReadOnly`, a name that is not an inspector member of the listed type, a name given twice. - **Global types are never grouped.** - **Foldouts:** window memory, collapsed by default, cleared by `CloseStore`. Create-in-group, `Add` and `Duplicate` expand the path to the new selection. No `EditorPrefs`/`SessionState`. - **Create into a `Missing ` node** presets the dangling key (keeps I5). - **A drop of the current key is rejected** (no no-op commit). - **Variants:** see Q6's defaults and I10. ## Sequence with document variants | Cut | Files | Overlaps grouping? | |---|---|---| | Variants C2a (element ids, in flight) | `CultCache.cs`, new `CultElementIds.cs`, Mesh/Geometry documents, `ElementIdTests.cs` | no | | Index cut (`[CultIndex]` means unique; queued behind C2a) | `CultCache.cs`, Mesh/Geometry | no | | Variants C2b (nested paths; inferred from the brief, not yet written in the variants map) | `CultCache.cs`, `CultVariants.cs` | no | | **Grouping Cuts 1-2** | `CultInspectorModel.cs`, `CultInspectorAttributes.cs`, `CultInspectorModelTests.cs`, `CultCacheStudioWindow.cs`, `CultCacheStudioDrawers.cs`, Studio README | — | | Variants C3 (Studio edits the delta) | `CultInspectorModel.cs` (edit, commit, diff, `CreateVariant`, `Rebase`), window (variant controls beside `Duplicate`, row marker), drawers (per-member marker and revert), `CultInspectorModelTests.cs` | **yes: all five grouping files** | **Order:** grouping Cuts 1-2 now, in parallel with C2a, the index cut and C2b (no shared files). Grouping merges to `main` before C3's Hands starts; C3 needs C2b anyway. C3 then rebases onto grouping and lowers its row marker inside the grouped row drawing that Cut 2 writes. Then **one release (Cut 3)**, then one Aetheria pass (variants C4 plus Cut 4). Where they touch: `DrawRecords` rows (`CultCacheStudioWindow.cs:180-185`; grouping rewrites, C3 marks); the toolbar (`:142-157`; C3 adds variant buttons beside `Duplicate`, grouping leaves it alone); `DrawRecordRef` (grouping adds the drop target; C3 marks at the member level in `DrawValue`, not inside it); `CultInspectorModel.cs` (disjoint methods); `CultInspectorModelTests.cs` (disjoint fixtures; name grouping fixtures `Group*`). **One release commit:** Cut 3 below is the single release for both campaigns. The variants map's C3 line "Then tag `caching-unity-v1.5.0`" should point here instead of owning a second release step (proposal for Self). ## Cuts Hands for Cuts 1-2: branch `hands/studio-grouping` in a fresh worktree off `origin/main` (Self picks the path, e.g. `C:/wsNN-grouping`). `F:\Projects\CultLib-studio-grouping` holds the superseded docs-only `codex/studio-grouping`; do not reuse it. Do not touch the main checkout. Mutation testing is Stryker.NET on the cut's diff; there are no hand-applied mutations. ### Cut 1. CultLib model: declaration, tree, candidate and label authority (~150k tokens) - **Delete and collapse first:** - `CultInspectorModel.cs:519`: the inline `target.IsInstanceOfType(record.Document) && record.Key.Value.Length > 0` in `RecordCandidates` (`:514-522`) moves into `IsRecordCandidate`; `RecordCandidates` filters through it (I6). - `:36` and `:209`: `member.GetCustomAttributes(true)` → `Attribute.GetCustomAttributes(member, true)` (F5). - **Add in `CultInspectorAttributes.cs`**, before `CultInspectorDrawerAttribute` (`:81-86`): `CultInspectorGroupByAttribute(params string[] members)`, `AttributeUsage(Class, Inherited = true, AllowMultiple = false)`, property `Members`, with a comment in the file's style stating nesting, nearest-wins and opt-out. - **Add in `CultInspectorModel.cs`** (or a sibling `CultInspectorGrouping.cs`, same namespace; one file while it stays under ~800 lines; it is 645 now): - `CultInspectorGrouping { Type ListedType; IReadOnlyList Members; string? Notice }` (members empty when `Notice` is set). - `CultInspectorRecordGroup { string Id; string Label; int Depth; IReadOnlyList Values; IReadOnlyList Children; IReadOnlyList Records; int Count }`. - `GroupingOf(Type listed)`, cached like `ShapeOf` (`:324-333`): names resolve against `MembersOf(listed)` (`:335-339`); validation per Defaults. - `GroupRecords(Type listed, IEnumerable records)`: partitions `RecordCandidates(CultRecordRef)` by value identity, reading each record's `Document` (resolved, I10). - `CreateInGroup(Type listed, CultInspectorRecordGroup group, out string? notice)`: `CreateElement(listed, listed, out notice)` (`:387-398`), then `SetValue` each level; refs via `CreateRecordRef` (`:524-529`). `ArgumentException` for a node not of `listed`'s current grouping. - `IsRecordCandidate(Type refType, CultStoredDocument record)`. - `RecordRefLabel(Type refType, object? value, IEnumerable records)`: `None`, `RecordLabel` (`:315-319`), or `Missing `. - **Tests** in `tests/GameCult.Caching.Tests/CultInspectorModelTests.cs` (476 lines; fixtures `:299-476`). New fixtures `GroupBase` (abstract) / `GroupLeaf : GroupBase` `[CultDocument]`, a re-declaring type and an opting-out type; members: enum declared out of alphabetical order, `CultRecordRef`, string, float, `[Flags]` enum, hidden int, read-only int. No object lists (F15). Behaviours, one test each: 1. levels nest in declared order, not slot order; 2. a `GroupLeaf` listed under `GroupBase` groups by the base declaration (I3); 3. nearest declaration wins; an empty declaration opts out; 4. leaves partition the candidates over unset refs, dangling refs and undefined enum values (I2); 5. two refs sharing one `[CultName]` give two nodes; `None` first; `Missing k` last (I4, I7); 6. enum nodes order by value; 7. create at depth 2, upsert into a temp single-file `CultCache`, regroup: same node id (I5); 8. each invalid declaration (unknown, float, `[Flags]`, hidden, read-only, duplicate) gives a notice and a flat tree, no throw (I8); 9. `IsRecordCandidate` accepts a subtype, refuses a sibling and an empty key, and `RecordCandidates` equals the records it accepts (I6); 10. a base abstract property's `[CultInspectorLabel]` reaches the override's metadata (F5); 11. **a variant groups by its resolved value**: base with `Enum=A`, variant overriding it to `B` via `UpsertVariantAsync` and `Override`, regroup: the variant is in `B`; change the base to `C` with the variant not overriding it, regroup: it follows to `C` (I10). - **Verify (Yggdrasil):** `ygg-verify.sh /f/Projects/CultLib dotnet 'dotnet test tests/GameCult.Caching.Tests -c Release'`, then `ygg-verify.sh /f/Projects/CultLib dotnet 'dotnet tool install -g dotnet-stryker && export PATH="$PATH:$HOME/.dotnet/tools" && cd tests/GameCult.Caching.Tests && dotnet stryker --project GameCult.Caching.csproj --since: > /tmp/s.log 2>&1; rc=$?; tail -60 /tmp/s.log; exit $rc'`. Survivors triaged by name in the report. - **Ledger:** model ~+170, attributes ~+15, tests ~+230; one predicate collapsed. ### Cut 2. CultLib Studio: lower the tree, create in group, drag source, drop target (~150k tokens) - **Delete first:** - `CultCacheStudioDrawers.cs:266` inline `"Missing " + key : "None"` → `Model.RecordRefLabel(type, value, Records)` for `names[0]` when `index == 0`. - `CultCacheStudioWindow.cs:180-185` flat `foreach` → tree drawing. - **Window** (`CultCacheStudioWindow.cs`): - fields (`:13-30`): `Dictionary _groupFoldouts` (ordinal), `const string RecordDragKey`; `CloseStore` (`:318-327`) clears them. - `DrawRecords` (`:135-189`): `GroupingOf(_selectedType)`; a `Notice` shows a warning `HelpBox`. Draw `GroupRecords(_selectedType, _records)` recursively: foldout `Label (Count)` indented by `Depth`; an expanded node draws children, leaf rows, then a `Create` mini button under `Add`'s gate (`:141-146`). The root is not a foldout; `Add` stays root create. - row: reserve the rect with `GUILayoutUtility.GetRect`, handle `MouseDrag` there first (`PrepareStartDrag`, `SetGenericData(RecordDragKey, new CultCacheStudioRecordDrag(_model, key))`, empty `objectReferences`, `StartDrag(label)`, `Use()`), then `GUI.Toggle(rect, ...)` (F11). Keep the existing `` suffix logic (`:182-183`) in one place for C3 to extend. - `AddInGroup(node)` mirrors `Add` (`:340-346`) through `Run` (`:369`); selects the key and expands the node's id prefixes. `Add`/`Duplicate` reveal the new record's path (a `_revealKey` resolved next frame is acceptable). - **Drawers** (`CultCacheStudioDrawers.cs`): `internal sealed class CultCacheStudioRecordDrag { Model; Key }`. In `DrawRecordRef` (`:254-280`), capture the `HorizontalScope` rect; on `DragUpdated`/`DragPerform` inside it with our payload: `Copy` when `GUI.enabled`, same `Model`, key differs, record in `Records`, and `IsRecordCandidate`; else `Rejected`. Leave foreign drags alone. On perform: `AcceptDrag`, `next = key`, `GUI.changed = true`, `Use()`; return through the existing `:279`. - **README** (`src/GameCult.Unity/Assets/Caching/README.md`): grouping, create in group and drag-to-reference after the paragraph at `:30-34`; the grouping API, `IsRecordCandidate`, `RecordRefLabel` under `## Inspection Model` (`:39`). - **Verify:** - Cut 1's Yggdrasil test command still passes. - Negative greps (each empty): `rg -n "IsInstanceOfType" src/GameCult.Unity/Assets/Caching/Editor | rg -v "assetType.IsInstanceOfType"` (I6; `:215` is the asset-GUID sub-asset check, not candidacy); `rg -n "\"Missing \"" src/GameCult.Unity/Assets/Caching/Editor` (I7; the union drawer's `"None"` at `:393` is not a ref label, so grep `Missing` only); `rg -n "SetValue|GetCustomAttribute|CultInspectorGroupBy" src/GameCult.Unity/Assets/Caching/Editor/CultCacheStudioWindow.cs` (I1); `rg -n "EditorPrefs" src/GameCult.Unity/Assets/Caching/Editor` shows only `LastPathKey`. - **Starfire, one job, editor closed:** the F10 compile gate, `dotnet build` of a scratch netstandard2.1 project over the worktree's `Assets/Caching/Editor/*.cs`. 0 errors. - **Ledger:** window ~+80, drawers ~+30, README ~+20; two inline decisions deleted. ### Cut 3. Joint release: cultlib Unity 1.0.61, Studio 1.5.0 (after grouping and variants C3 merge) - **Base:** `main` with grouping Cuts 1-2 and variants C1-C3 merged; a clean worktree. - **Files:** `unity/org.gamecult.cultlib/package.json:4` `1.0.60` → `1.0.61`; `src/GameCult.Unity/Assets/Caching/package.json:4` `1.4.0` → `1.5.0` and `:13` dependency `1.0.61`; `CHANGELOG.md` entries in both packages (grouping, create in group, drag to reference; variant editing from C3); plugins via `powershell -File scripts\build-unity-package.ps1 -UpdateTemplate` (Windows: MSVC and the pinned MsQuic digests; its version check is at `:168-170`). - **Tags:** `cultlib-unity-v1.0.61`, `caching-unity-v1.5.0`; no `cultmath-unity` tag. - **Verify (Starfire, one job at a time):** - byte check: rerun the script; `git status --porcelain unity/` is empty; - expected diff: `GameCult.Caching.dll/.pdb` changes, and `gamecult_mesh_quic_native.dll` changes (F16); `msquic.dll` should not (same pinned digest). Anything else stops the release; - API present: reflect over the tracked `GameCult.Caching.dll` for `CultInspectorGroupByAttribute`, `GroupRecords`, `CreateInGroup`, `IsRecordCandidate`, `RecordRefLabel`, and C3's `CreateVariant`; - native ABI: `dumpbin /exports` lists `cultmesh_quic_open`, `_state`, `_poll`, `_error`, `_close`; - `git ls-remote --tags origin` lists both tags. ### Cut 4. Aetheria: grouping annotations (inside variants C4's pass; blocked on Q7) - **Where:** the Aetheria branch and worktree Self opens for variants C4. Never `F:\Projects\Aetheria`'s working tree. C4 owns the pin bump (`Directory.Build.props:6`, and `Packages/manifest.json:54-55` to `#caching-unity-v1.5.0` / `#cultlib-unity-v1.0.61`; `:56` cultmath unchanged; `packages-lock.json` follows) and the element-id sweep (F18). This cut adds only: - Q7's `[CultInspectorGroupBy(...)]` class attributes (recommendation B: five declarations); - `tests/Aetheria.Shared.Tests/StudioGroupingTests.cs` (xUnit): `GroupingOf` over `CultCacheMessagePack.CreateInspectorModel(registry)` names exactly the declared members per annotated type with no notice; `GroupingOf(typeof(WeaponItemData))` follows Q7; every registered document type's `GroupingOf` has a null notice. - **Verify:** - headless (Starfire, one job; the revision check needs `CultLibRoot` clean at the release SHA and `CultMathRoot` clean at `6d5e209`): `dotnet test tests\Aetheria.Shared.Tests -p:CultLibRoot= -p:CultMathRoot=`; - Unity 6000.3.24f1 batchmode compile, editor closed: `-batchmode -nographics -quit -projectPath -logFile `; 0 errors, both new tags resolved. - **Operator only, in Aetheria's Studio against a copy of `GameData/Aetheria.cc`:** - each annotated type shows the declared levels; `GearData` includes `` rows; - create in a node: the record lands there and is selected; - drag a `Faction` onto a `FactionProductData.Manufacturer`: accepted; drag a gear record onto it: rejected; - drag a `PersonalityAttribute` onto a `DemandProfile` key that already exists: refused through the notice; - read-only open: every drop rejected; - a variant (once C4 authors one, e.g. `LonginusX`) sits in its resolved `HullType` node, and a drop into its inspector writes an override (C3), not a refusal; - close without edits: store bytes unchanged. ## Subtraction ledger Added: one attribute, two model types, five model methods, ~11 CultLib and ~3 Aetheria tests, ~110 Studio lines, five annotations. Collapsed: the candidate predicate (now one method, reused by drop), the ref label strings, the property-inheritance miss (F5). Not added: selectors, registries, `EditorPrefs` keys, runtime ports, packages, targets, daemons, a second release. Build footprint: `GameCult.Caching` and its tests (Yggdrasil); the F10 compile gate, the package script (all tracked plugins including the native bridge) and Aetheria's headless and batchmode checks (Starfire; Windows is the target for the editor plugin and win-x64 bridge). ## What the 2026-09-17 map had wrong as of `feeced8` - Cut 4 grouped `GearData` by `Manufacturer`; `ItemData.Manufacturer` no longer exists (F17). - Q5's native-plugin drift was spent by the 1.0.60 release (F16); the Cut 3 "rebase onto `codex/asset-guid-attribute`" note is obsolete (merged). - Cut 4 re-pinned Aetheria on its own; after C2a it cannot, without the id sweep (F18). - Aetheria branch `codex/cultcache-cutover`, props `:5`, manifest `:53-55`, cultmath `v0.2.3`: now `codex/fire-control-12`, `:6`, `:54-56`, `v0.2.4` with a separate CultMath pin. - Negative greps for `IsInstanceOfType` and `"None"` would fail on unrelated lines (`:215`, `:393`). - Hand-applied mutations per test are retired; Stryker.NET on the diff replaces them. - Moved anchors: `RecordCandidates` `:512-520`→`:514-522`; `_shapes` `:298`→`:296`; `RecordLabel` `:313-317`→`:315-319`; `CreateElement` `:385-396`→`:387-398`; drawer attribute `:80`→`:81-86`; `DrawRecordRef` `:195-221`→`:254-280`, label `:207`→`:266`; `ReplaceKey` routing `:243-251`→`:303-309`; test fixtures `:380-459`→`:299-476`; README `:36-39`→`:30-39`; build-script version check `:150-155`→`:168-170`. - Worktree `F:\Projects\CultLib-studio-grouping` is now the superseded docs branch; Hands uses a fresh one.