The record list draws GroupRecords as foldouts (fold state keyed by listed
type and node id), each with Create through CreateInGroup; an ungrouped type
lists RecordCandidates flat under the model's notice. Add, Create and
Duplicate reveal the new record's path. Rows start the record drag.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A record row dragged onto a CultRecordRef value sets it when the ref's picker
would offer that record (IsRecordCandidate), refusing in a disabled scope, a
foreign model, or the key already held. The ref popup's None/Missing label
now comes from RecordRefLabel.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
*.meta is gitignored repo-wide; every tracked Unity .meta file, these two
included, is force-added on purpose.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
CultLib 1.0.60: CultInspectorAssetPath removed for CultInspectorAssetGuid +
CultAssetGuidKey (breaking for inspector annotations), and the native QUIC
bridge's Schannel to OpenSSL MsQuic 2.5.9 static-link change shipping since
1.0.59. Caching Studio 1.4.0: the GUID picker, sub-asset keys, built-in
refusal, and the stale-value warning/Clear button. build-unity-package.ps1
only stages files it names explicitly, so it neither drops nor overwrites
these; no script change needed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
TrimEnd('0','f') missed Library/unity default resources built-ins whose GUID
has no all-zero or all-f suffix (e.g. 0000000000000000e000000000000000, the
LegacyRuntime font / Cube mesh in 6000.3.24f1). Ask AssetDatabase instead: a
real project asset is always a main or sub-asset under Assets/ or Packages/;
built-ins are neither. Also add a Clear button beside the stale-value warning
so a non-resolving stored key can be cleared without picking a substitute
asset first (drawing still never rewrites a stored value on its own).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Ruling S (2026-09-17): a stored [CultInspectorAssetGuid] value is the
Addressables key form itself, the bare GUID for a main asset or
"guid[subAssetName]" for a picked sub-asset (one sprite of a sheet, say) -
the same shape AssetReference keeps and Addressables.LoadAssetAsync<T>
accepts.
CultAssetGuidKey (engine-free, GameCult.Caching) pins the parse/format rule
with unit tests (bare guid, guid[name], names with brackets/spaces,
malformed input), so headless checks can share it without touching Unity.
The Studio drawer's AssetGuid branch now: resolves either key form on read;
refuses to store a built-in resource's GUID (it has no Addressables entry)
and logs why instead of silently writing nothing; shows a warning next to a
stored value that fails to parse or resolve instead of drawing it as an
ordinary empty field, and never rewrites that stale value on its own.
Rebuilt GameCult.Caching/GameCult.Caching.MessagePack plugin DLLs
(scripts/build-unity-package.ps1 -UpdateTemplate) since the new type ships
in GameCult.Caching. Compiled CultCacheStudioDrawers.cs and
CultCacheStudioWindow.cs against Unity 6000.3.24f1's editor reference set
plus these plugins in a scratch csproj (removed after): 0 errors.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Removing the public asset path attribute is breaking. Runs
scripts/build-unity-package.ps1 -UpdateTemplate to refresh the tracked
plugin DLLs/pdbs in unity/org.gamecult.cultlib/Runtime/Plugins, and bumps
unity/org.gamecult.cultlib/package.json to 1.0.60 and
src/GameCult.Unity/Assets/Caching/package.json to 1.4.0 (dependency bumped
to cultlib 1.0.60). Tags are the next, separate step.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
Corrects the 1.0.58/1.3.0 release. Rebuilds the tracked plugins with
scripts/build-unity-package.ps1 -UpdateTemplate so they carry "" as the
unset CultRecordRef wire form in every position (nil still reads as unset)
and the edit-start Record snapshot. The native QUIC connector changes once
to its /Brepro form; later rebuilds reproduce it byte for byte. The Studio
package moves to 1.3.1 and depends on cultlib 1.0.59.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Rebuilds the tracked plugins with scripts/build-unity-package.ps1
-UpdateTemplate so they carry the unset-ref wire fix and
CultInspectorEdit.Record. The Studio package moves to 1.3.0 for
CultInspector.Record and depends on cultlib 1.0.58.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
The window decided whether a record type could be added
(GetConstructor(Type.EmptyTypes)), made it with Activator.CreateInstance,
and matched records to a type with IsAssignableFrom twice, duplicating
RecordCandidates.
Add and Create now ask the model's CreateElement, and its notice is the
refusal. The record list and the per-type counts come from
RecordCandidates. The window only renders.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The copy was dropped only when a claimed drawer threw. A built-in drawing
exception (BuildDictionary, SetValue, a throwing constructor, Clone) or an
ExitGUIException escaped OnGUI with the copy alive, and the next change
committed the half-made edit.
The window now takes the copy out of _edit before drawing and puts it back
only when the document drew to the end with no drawer failing. Every
abnormal exit, ExitGUIException included, leaves the next frame to clone
afresh from the cache.
ExitGUIException is dropped too, not exempted: Unity's pickers exit on the
click that opens them and deliver the pick in a later event, which lands
on the fresh copy and commits normally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
The model paragraph now lists the edit decisions the model owns (key
refusal by bytes and by the dictionary comparer, fresh keys, element
choices, integer clamping) and names CreateInspectorModel. The drawer
section states the return-and-GUI.changed contract and the discarded
frame on a throw. The sample drawer draws a hex color string instead of
CultMath.float3, which the package no longer references.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A drawer that threw after mutating a reference value in place left that
half-made mutation in the live edit copy, where the next unrelated change
saved it. DrawDocument now reports a drawer failure, and the window drops
the edit copy uncommitted so the next frame re-clones from the cache; the
error row says nothing from that frame was saved.
The drawer contract no longer invites in-place mutation: a change returns
the new value and sets GUI.changed, and an in-place mutation without
GUI.changed is not saved reliably. Detecting such a mutation would mean
serializing the document every frame, so it is documented, not detected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>
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>
CultCache Studio 1.1.0 -> 1.2.0.
The IMGUI inspector is now a lowering of CultInspectorModel: it asks the
model for members, shapes, claims, record candidates and key rules and
hands every change back through it (BuildList, BuildDictionary, Compose,
CreateUnionValue, CreateRecordRef). Deleted from the Studio: its own member
discovery, list/dictionary/union classification, default construction,
key identity, the per-type CultMath drawers (composite shapes replace
them) and the nameof GetProperty key lookup. The window edits through
CultInspectorEdit and clones through the model.
Drawer extension point: ICultInspectorDrawer.Draw receives the inspector,
label, declared value type, value and member (also for list elements and
dictionary keys and values). Attribute claims win over type claims, which
win over built-in drawing; CultInspector.DrawDefault hands a value back to
built-in drawing and DrawValue draws a sub-value with claims applied.
The GameCult.Unity.Caching runtime assembly is deleted; its attributes
live in GameCult.Caching. The package no longer depends on
org.gamecult.cultmath.
Game data stays out of Assets: New refuses a path under the project's
Assets folder and says why, Open warns, and the file dialogs start in the
last store's folder or the project root, never Assets.
README names the model/lowering seam and the new drawer contract.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Opening a directory store, read-only or writable, created the .records
folder and its .commit.lock before reading a manifest that might not
exist. Only a write creates them now: PullAll takes the commit lease with
create: false, which opens an existing lock and returns no lease when
there is none (no writer has ever committed there, so there is nothing to
exclude). CommitBatch and PushAll create the folder and lock as before.
Test DirectoryMessagePackBackingStore_Open_CreatesNothing_UntilAWrite:
opening a missing store and a manifest-less .records folder, read-only
and writable, leaves the filesystem unchanged; the first flush creates the
manifest, the lock and one page. With the previous lease code restored
the test fails on the first read-only open.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Edits: drawers now mutate a MessagePack clone of the selected record.
The cached object changes only through an admitted UpsertAsync; after
any upsert attempt the copy is dropped and re-cloned from the cache, so
a refused upsert leaves memory untouched (covers member Set, list and
dictionary in-place rebuilds). Duplicate uses the same clone helper.
Foldouts restore GUI.changed, so expanding never upserts or dirties.
Extension point: Draw is wrapped; ExitGUIException rethrows, anything
else draws an error row and returns the value unchanged. A type claimed
by two or more drawers is claimed by none; every claimant is logged and
the member draws the red row naming them.
Multi-dimensional arrays (float[,], bool2[,]) draw an error row and are
never flattened into a rank-1 array.
Dictionary keys compare by serialized identity; CultRecordRef<T> keys
compare by key string with null and "" the same empty key. Null,
empty-ref and duplicate keys are refused with a visible warning. Add
picks the first unused record for ref keys and warns when none is left.
Union popup shows "None" for null and never labels a declared subtype
as undeclared.
New creates only a writable single-file store: disabled with Read Only
or Directory toggled, refuses an existing path (file or .records
folder) without closing the open store, and reports Created only after
the file exists on disk.
CultCache.FlushAsync(bool soft) kept: Mimir.CultMeshMedia calls
cache.FlushAsync(soft: true) on a CultCache through a project reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 6. CultCache Studio 1.0.0 -> 1.1.0.
Deleted: the CultCacheBridge string-reflection bridge (type lookups by
name, GetMethod/GetProperty invocation, PullOnOpen, FlushAsync(bool)),
its DescriptorInfo/CatalogMemberInfo/RecordInfo shadows, and the
disabled-text-field fallback in DrawValue. The window calls
CultCacheMessagePack.Create, UpsertAsync(Type, object, key), Remove,
FlushAsync, IsDirty, Registry.AllDescriptors, AllStoredDocuments and
BackingStores directly through GameCult.CultLib.
Window: single-file and directory stores, optional read-only open with
edits disabled and the reason shown, document types from the registry,
records by [CultName] with key fallback including subclasses, Add,
Duplicate (MessagePack round trip), Delete, immediate upsert on edit,
Save = flush, Dirty indicator. An absent [CultGlobal] shows as absent
with an explicit Create; nothing is invented. Opening and closing never
writes.
Drawers (new CultCacheStudioDrawers.cs): dictionaries with recursive
keys and values, abstract/interface members through their MessagePack
[Union] declarations only, CultRecordRef<T> record picker plus raw key,
CultMath float2/3/4, double2/3, int2, bool2, quaternion, rect, Color32,
enums including flags, lists and arrays with add/remove, nested keyed
types, a red error row for unclaimed types, and the
[CultInspectorDrawer(typeof(T))] extension point discovered through
TypeCache (ICultInspectorDrawer). Existing inspector attributes keep
working.
package.json depends on org.gamecult.cultlib 1.0.57 (Cut 7) and
org.gamecult.cultmath 0.1.2. No Unity.Mathematics reference.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cut 3 of the CultCache migration (Aetheria docs/cultcache-migration-cut.md, 2.1
and 2.2). One commit, because routing, the batch that replaces the ambient
transaction, and conditional commit share one admission path.
Deleted first:
- globals: InitializeGlobals, MaterializeMissingGlobals, the two-argument
constructor, the initializeGlobals plumbing in CultCacheMessagePack,
ContainsDurableRecord (cache and directory store)
- replication: the push-to-every-store loops and the attach-time push of
existing records
- typeof(T) as the schema owner in AddAsync/UpsertAsync and in
CultNetDatabase.PutAsync/PutPredictedAsync
- the ambient transaction: ExecuteTransactionAsync x2, CultCacheTransaction,
CommitTransaction, VisibleStoredDocuments, the AsyncLocal, the SemaphoreSlim
gate, and every ambient check
- CultCacheOpenOptions.PullOnOpen; OpenAsync is Task.FromResult(Create(...))
- DirectoryMessagePackBackingStore.ResolveLegacyUncataloguedRecordCatalog: the
catalog precommit (fe55bff) is an ancestor of the v4 page format (eea8248),
so no v4 manifest can reach it
- CultCache.DeleteAsync<T>; its one caller, CultNetDatabaseSubscriptionClient,
called it by reflection and now calls Remove(key)
Added:
- AddBackingStore(store, params Type[] homes): attach validates (clean, not
already attached, one untyped store, no claimed home type, no admitted
record changes home) and then pulls under the gate
- Home(type): most derived routed home, else the untyped store
- Admit: every add and remove, from a write, a batch or a load, validates
(home, read-only, loaded-from-home, global singleton), lands durably, then
applies and publishes; a pull hands its whole delta over in one call, so a
refused record leaves the store and the cache unchanged
- read-only stores (CacheBackingStore(readOnly), both store constructors,
CultCacheOpenOptions.ReadOnly); flush skips them and writes only dirty stores
- assignable GetGlobal<T>/GetByName<T>/GetByIndex<T>/Watch<T>; more than one
candidate throws naming them
- Commit(Action<CultCacheBatch>) returning bool, TryCommit returning
CultCommitOutcome, CultCacheBatch (Upsert, Remove, Expect, ExpectUnchanged,
sealed after commit), CultCommitRequest; CacheBackingStore.CommitBatch
evaluates conditions under the store's exclusive lock
- single-file store: <path>.lock sidecar for CommitBatch and PushAll; a clean
store commits onto the file as it is, so records it never loaded survive
- storedAt minted in one place and bumped a tick when not later than the record
it replaces
Consumer adjustments in this repo: Mesh streaming tests drop PullOnOpen; the
Studio bridge stops reflecting on it; BackingStoreTests' global test creates
its global explicitly and its transaction tests are rewritten against Commit
(the two that proved ambient hydration rules are deleted); the pre-Cut 2
refusal test expects the attach to throw.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deletes four confirmed-dead tracked files:
- CultLib.txt, src/GameCult.Caching/GameCult.Caching.txt,
src/GameCult.Unity/Assets/UI/UI.txt: concatenated snapshot copies of live
source, carrying stale E: drive provenance and obsolete dependency
versions. Live source under src/ remains the owner.
- tests/GameCult.Networking.Tests/lcov.info: tracked coverage output.
.vscode/tasks.json:14 still generates it fresh via
"/p:CoverletOutput=./lcov.info" on every coverage run, so tracking the
stale committed copy in git is the liability, not the generation step.
- .voidbot/status/void-memory-maintenance.json: a one-time "starting"
receipt (added whole in 4813570, never touched again) naming a dead
E:\Projects\CultLib path and an unimplemented repo_face_heartbeat
launcher. VoidBot's actual heartbeat state lives at
.voidbot/status/repo-face-heartbeats.{cc,json} (see
packages/config/src/index.ts REPO_FACE_HEARTBEAT_STATE_PATH) — a
different file entirely; nothing reads this one.
Kill attempts:
- git grep -n for each of "CultLib\.txt", "GameCult\.Caching\.txt",
"UI\.txt", "lcov\.info" across origin/main, excluding each file's own
path: only .voidbot birth/distiller snapshots (auto-generated onboarding
file-listing catalogs, not functional readers) and the one real lcov.info
producer in .vscode/tasks.json.
- rg across all of F:/Projects (excluding target/node_modules/.git):
gamecult-site's docs/research/gamecult-org-repo-ledger.json lists
"CultLib.txt" as a filename in a static historical repo-file inventory
(not a functional consumer); Aetheria/docs/cultcache-migration-cut.md
cites GameCult.Caching.txt:157 as an evidence line number in a
migration-history note (stale citation after this cut, not a functional
break — flagged for the record, not fixed here since that doc is out of
scope).
- voidbot MCP search for void-memory-maintenance.json / repo_face_heartbeat
found only the unrelated repo-face-heartbeats state path, confirming no
reader.
Collision check: git diff --stat origin/main against each of the other 4
CultLib worktrees (codex/recover-aetheria-authority,
codex/delvehold-isosurface, claude/media-stream-catalog, and the two
detached-HEAD worktrees, both confirmed ancestors of origin/main) over
these five paths: all empty, no in-flight work touches them.
REC-102
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Deleted all the legacy UI prefabs and related UI scripts. Updated LayoutComponent with a helper function for spawning spacers and added a spacer prefab, thus completing the port of all legacy functionality.
Introduced Newtonsoft.Json-backed persistence.
Improved backing-store reliability and serialization across the caching stack.
Improved networking security posture.
Key changes:
- Add new GameCult.Caching.NewtonsoftJson project with SingleFile/MultiFile backing stores and polymorphic known-type envelopes.
- Add MessagePack helper serialization (DatabaseEntrySerialization) and update MessagePack backing stores to use the shared options/resolver; add DatabaseLink formatter and generator/analyzer docs.
- Update CultLib.sln to include the new projects and new caching tests project.
- Improve CultCache persistence behavior: track pending store operations, wait for operations on PullAll, and centralize async push handling with error logging.
- Harden file I/O in multi-file/single-file stores: atomic temp-file writes, safe Replace/Move semantics, rename-safe deletes, and improved file-watcher subscription and debounce disposal.
- Add README files and documentation for several subprojects and packaging projects.
These changes add JSON persistence support, improve serialization consistency, and make on-disk operations more robust and observable.