Commit graph CultLib/tests/GameCult.Caching.Tests/CultInspectorModelTests.cs
Author SHA1 Message Date
Meta Vens
b9cca38dbf Cover valid surrogate pairs and a second unpaired surrogate in node ids
Stryker on the escaping showed the pair-skip branch uncovered and the run
offset unpinned; the kinds now include a valid pair, a replacement character
before a lone low surrogate, a reversed pair, and two unpaired surrogates.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 15:49:51 +02:00
Meta Vens
7defd82c20 Pin node-id escaping, case-distinct nodes and the flat ungrouped list
Ids that differ only by %2F versus /, keys that differ only by case, and
unpaired surrogates each get a test; the ungrouped list stays flat.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 14:00:47 +02:00
Meta Vens
4f7d8821af Pin the create-in-group node check against a value that fits another grouping
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:29:09 +02:00
Meta Vens
439efd1550 Drop the inert inherit flag from the global check and its derived-type case
[CultGlobal] is not inherited, so the flag changed nothing and no test can tell.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:16:11 +02:00
Meta Vens
30e1f3fb95 Cover ungrouped types, null strings, derived globals and id separators
Stryker survivors on the grouping diff: the no-declaration path, the
null-string identity, the inherited [CultGlobal] check and the id separator.
GroupLevel takes text and tie as optional arguments so the scalar kinds
stop passing constant strings that could be swapped without any effect.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 13:15:03 +02:00
x
84c9a79972 Pin string case ordering and node id spelling in the grouping tests
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:52:05 +02:00
x
3d07ba0d20 Give the overriding variant its own name in the grouping test
A variant must override its name.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:21:28 +02:00
x
79e9318698 Add model-owned record grouping: declaration, tree, create-in-group, ref labels
[CultInspectorGroupBy(nameof(A), nameof(B))] on a document class names the
members its record list groups by, outermost first. CultInspectorModel owns
the rest: GroupingOf validates the declaration (a refused one gives a flat
list and a notice), GroupRecords partitions RecordCandidates by value
identity with occupied values only, CreateInGroup builds the record a node
holds, RecordRefLabel owns None and Missing <key>. A variant groups by its
resolved Document.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:09:25 +02:00
x
8ad0691bdd Declare the label fixture's key on its base member
The registry requires an override to repeat or omit the base declaration's [Key].

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 12:03:28 +02:00
Meta Vens
a6382475e4 Collapse the ref-candidate predicate and read inherited member attributes
RecordCandidates and the new IsRecordCandidate share one rule, so a
picker's candidates and a drop's acceptance cannot disagree. Member
attributes are read with Attribute.GetCustomAttributes(member, true):
PropertyInfo.GetCustomAttributes ignores inherit, so a base abstract
property's inspector attributes never reached the override.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-29 11:55:41 +02:00
Meta Vens
95a3c32250 Replace the asset path attribute with a stable GUID identity
CultInspectorAssetPathAttribute stored an engine asset path, which breaks
when the asset moves. CultInspectorAssetGuidAttribute stores the asset's
stable identity (in Unity, the .meta GUID) instead. The Studio drawer now
resolves the stored GUID to a path only to load the preview object, and
writes back AssetPathToGUID on a change. No path form survives for the
next field to pick up.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 18:43:03 +02:00
Meta Vens
29bec57021 Decode an edit's Record from the bytes its Document came from
BeginEdit cloned Document eagerly but Record lazily from record.Document,
so a cache change in place before Record's first read made the two copies
come from different states. BeginEdit now serializes the document once and
deserializes Document from those bytes; Record deserializes the same bytes
on first read. Record is the state the edit began from, and costs one
deserialize only when a drawer reads it, with no second serialize.

Each commit still spends the edit, so a slider drag begins an edit per
frame; that serialize was already paid for Document, and Record adds a
deserialize only in frames where a drawer reads it.

The new test mutates the cached document between BeginEdit and the first
Record read; it fails on the lazy clone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:34:23 +02:00
Meta Vens
90e7dbee27 Give inspector drawers the record they sit in, read-only
A drawer saw only its value; Aetheria's schematic-shape drawer needs sibling
members of the owning document (texture path, aspect, hull tints).

The model owns the seam: CultInspectorEdit.Record is a second copy of the
stored document as the edit began, cloned on first read and kept for the
edit. The Studio lowers it as CultInspector.Record, so a runtime CultUI
panel gets the same seam from the same edit. The Draw signature is
unchanged, so existing drawers keep compiling.

Why not hand drawers the edit copy the Studio already draws: that copy is
what Commit upserts, so a write through it would be saved without passing
through the returned value, GUI.changed, or read-only metadata. The record
copy is read by nothing, so writing to it saves nothing and changes neither
the edit nor the cache. It does not show edits other drawers made earlier in
the same frame.

The Studio's separate document-type field collapses into the held edit.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 21:01:22 +02:00
Meta Vens
ccf9bd0c0c Show an unkeyed struct's fields or its properties, never both
Unity's Quaternion showed eulerAngles beside x/y/z/w, Rect about eleven rows
over four values, and Bounds center/extents/size/min/max: settable properties
alias the fields they wrap. A struct with public fields now shows only those; a
struct without shows its settable properties, so record structs and CultMath
matrices (properties over private rows) still get clean rows and vectors show
fields only.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:55:03 +02:00
Meta Vens
baa8de0baf Disable Studio Add for record types the model cannot create
The window lost its disabled state when record creation moved into the model:
Add and Create were enabled for abstract types and threw on click, and
CreateElement(type, type) on an abstract [Union] type threw "X is not a choice
for X". CultInspectorModel.CanCreate is now the one rule CreateDefault and the
window both read, a slot's own type is always a choice, and an abstract one
returns the "abstract or no parameterless constructor" notice.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:53:32 +02:00
Meta Vens
8e0cd3e32d Compare dictionary keys under their owning document's serializer
Key identity serialized a key with the options of the key type's own
assembly, but the store writes it inside the document with the document
assembly's options, [CultCacheFormatterResolver] resolvers included. A key
that failed to serialize fell back to comparison by ToString.

The inspector codec now takes the owning document type, ReplaceKey and
FreshKey take it, and a key serializes as its declared key type under that
document's options, the store's own path. A key the document cannot
serialize is refused with a notice; the text fallback is deleted.

The comment and README said "the dictionary's own comparer"; it is the
rebuilt dictionary type's default comparer, and now says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:38:24 +02:00
Meta Vens
7d59021b57 Show unkeyed structs by their writable surface, not a lock rule
The unkeyed struct rule found auto-properties by the compiler's
<Name>k__BackingField name and marked the whole struct Unsupported when
one was get-only or a field was readonly. Record structs fell out with a
generic reason, and settable or init properties beside a field were
silently hidden. Editing is in place, so the lock protected nothing.

An unkeyed struct now shows its public fields (readonly ones read-only)
and its public properties that have a setter (init-only ones read-only).
Get-only properties are computed or kept by the in-place edit, so they are
not rows; vector swizzles stay out of the inspector.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:34:56 +02:00
Meta Vens
04c7c6bcd8 Refuse a list Add that creates nothing
CreateElement returned null without a notice when the element type itself
had no parameterless constructor, and the Studio added that null. The
notice now follows the result: nothing made is refused, except the null
a Nullable<T> slot legitimately holds.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:31:45 +02:00
Meta Vens
d22f662f7d Decide element choices and integer narrowing in the inspection model
ElementChoices says what a new list element, dictionary value or union
pick may be (a union's declared subtypes, or the slot type itself), and
CreateElement makes one or returns a notice when a subtype has no
parameterless constructor. It replaces CreateUnionValue, whose throw the
IMGUI caught and sent to the console. A dictionary with a union value
type now offers the subtypes too instead of adding a null value.

NarrowInteger clamps an integer edit to its type's range; the IMGUI no
longer converts and swallows overflow itself. IMGUI renders the model's
answers and shows its notices.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:07:19 +02:00
Meta Vens
d03e376411 Delete the composite constructor heuristic from the inspector
Operator ruling: the heuristic that rebuilt unkeyed structs through a
guessed constructor is removed, not guarded. Gone: the Composite value
kind, CultInspectorShape.Composer, CultInspectorModel.Compose and
Composite, the composite-component flag on CultInspectorMember, the IMGUI
composite rebuild path, and the README text about it.

An unkeyed struct is now edited in place through its public fields. A
struct holding state it cannot write in place (a public readonly field or
a get-only auto-property) is Unsupported with a reason naming the member,
until a drawer claims it. Computed get-only properties are not state and
do not lock a struct.

The composite-rejection test goes with the heuristic; the replacement
test pins both lock rules and fails with either removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:05:30 +02:00
Meta Vens
2a46409017 Decide dictionary key edits in the inspection model
A refused key and an unavailable fresh key are now the model's answer:
ReplaceKey returns the kept key with the notice text, FreshKey returns
null with its notice, and IMGUI only shows them.

A key is taken when another serializes the same or when the dictionary's
own comparer calls them equal, so 0.0 against -0.0 is refused instead of
throwing in BuildDictionary.

Keys never actually compared by bytes: the Studio handed the model the
document codec, whose registry guard throws for any non-document value,
so every key fell back to ToString. CultCacheMessagePack now builds the
model with a value serializer, and the Studio and tests both use it.

Tests pin the comparer rule, the unchanged-key rule (an old empty
reference key survives an edit of its value) and serialized identity
(keys whose ToString collides); each fails with its rule removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 14:03:00 +02:00
Meta Vens
5de49ebc60 Resolve hidden inspector members to the most-derived declaration
A keyed type that hides a base member with `new` offered two candidates
for one name. The inspector now keeps the most-derived declaration, after
dropping [IgnoreMember] ones, as the registry's own-member scan does.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:59:15 +02:00
Meta Vens
ba1119f18c Add the engine-free CultCache inspection model
The CultInspector* attributes move from the Unity-only
GameCult.Unity.Caching runtime assembly into GameCult.Caching, so headless
shared models carry them. CultInspectorAssetPath keeps a System.Type the
consumer names. CultInspectorDrawer claims either a value type (open
generic definitions claim every closed form) or an Attribute subclass.

CultInspectorModel owns what an inspector shows and which edits it may
make, with no engine reference:
- members: registry catalog slots for documents, [Key] slots for other
  MessagePack objects, public fields for unkeyed structs; Label, Hidden,
  ReadOnly, Order, Range, TextArea and AssetPath metadata; assignability by
  the rule the registry persists by.
- shapes: string, integer, float, bool, enum, composite (an unkeyed value
  type rebuilt through the constructor whose parameters match its members,
  which covers the CultMath vectors, rect and Color32 without naming
  them), CultRecordRef<T> with candidate records, rank-1 list or array,
  dictionary, union limited to declared [Union] subtypes, nested, and
  unsupported with a reason (multi-dimensional arrays included).
- CultInspectorDrawerClaims: attribute claims, then type claims, then
  built-in; invalid drawers and contested claims are errors and draw
  nothing.
- dictionary keys by serialized identity (a record reference by its key
  string, null and "" alike), refusal of null, empty-reference and
  duplicate keys, fresh keys, and collections rebuilt as new instances.
- CultInspectorEdit: a codec clone of the stored document; Commit upserts
  the copy, so a refused upsert leaves the cached object unchanged.

ICultRecordRef exposes CultRecordRef<T>.Key without T, replacing the
Studio's by-name GetProperty lookup.

Tests (CultInspectorModelTests): member list and metadata, claim
resolution order, declared-only union choices, record-reference
candidates, dictionary key refusal, refused commit leaving the cached
document unchanged, and shape classification.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-14 13:43:45 +02:00