Friction points from the Percipio migration that should be addressed directly in
Statum (client JS, GObjectMapping, action/handler base classes, DirectiveBuilder,
TopicSystem, crypto providers, key config, and docs). Items that are actually
upstream invercargill-json bugs (numeric/bool serialization crashes) are in
invercargill-upstream-issues.md; this file references them only where a Statum-side
mitigation is also worth doing. Item numbers in parentheses refer to the original
Percipio friction report.
stm-data-form — let a trigger reference a sibling <form>'s inputsstatum.js collectData only reads form inputs when the trigger
is a <form>, so a co-equal "Save draft" <button> sent an empty body.<form> whose named inputs to send with the action. (Already covered by
another report — keep it on the list.)stm-selected — set the selected value on a <select>stm-attribute.value on a <select> only calls
setAttribute('value', …), which does not change the selected option.stm-selected attribute (or make stm-attribute.value on a
<select> set the selected option). Worked around today with per-option
stm-attribute.selected="q.skill == '…'". (Already covered — keep it.)js/statum.js)stm-if → blank page — SEVERE (orig #1)eval made stm-if="… eval == null …" evaluate
wrong. Inside with(scope){ … }, an identifier not in scope resolves to the
global (eval is the built-in), so eval == null was always false.
Result: fully blank page, no console error.undefined instead of falling through to
window (this is the same evaluator change as S-2).evalExpr is
new Function('scope','with(scope){try{return (EXPR);}catch(e){return undefined;}}').
If EXPR mentions a slot that isn't held, the reference throws
ReferenceError, the inner catch returns undefined, so stm-if is falsy
and stm-text is empty — silently. unsetSlot == null does not evaluate
to true (as plain JS undefined == null would), because the entire
expression is swallowed by the catch.stm-if mentioned
evaluation (unset for a fresh attempt). Very hard to spot — no error.undefined
(not throw), so unsetSlot == null → undefined == null → true. Only
property access on a truly-undefined value should be caught. Pair with the
debug mode in S-16.[hidden] is defeated by ordinary author CSS — SEVERE, sneaky (orig #3)stm-if/stm-for branches with the hidden
attribute, but the UA [hidden]{display:none} is lower specificity than a
normal rule like .challenge-list li{display:flex}, so the hidden loop
holder leaked as a persistent empty row.[hidden]{display:none !important} from statum.js itself
(so it can't be overridden), or switch to a dedicated stm-hidden class with
!important. At minimum, document this prominently.stm-text on an editable <input>/<textarea> resets the user's typing (orig #4)stm-text is one-way state→DOM;
on any re-evaluation (e.g. after a co-equal action's notify) it wipes what
the user just typed, resetting it to the stale state value.update.stm-value) that sets
the value when first bound and then leaves it alone while the field is
editable (or focused), and/or document the editable-field pattern.collectData ignores the enclosing form for non-<form> triggers (orig #5)<button stm-action=…> inside a <form> collects only its own
stm-data-* attributes, not the form's named inputs.intent field + onclick hack.stm-data-form).setBusyState adds stm-busy-class classes to the action element
and disables children, but showing a spinner requires hand-rolled
<span class="spinner"> + CSS keyed off .btn-loading.stm-spinner (inject a spinner while busy).GObjectMapping cannot serialise array/list/collection fields — FOUNDATIONAL (orig #10)FIXED (2026-08-31, WP1 server-side effort).
GObjectMapping.to_propertiesnow serialisesstring[]properties as JSON arrays (read-back supported viafrom_properties) andObject-typed properties holding an Invercargill collection (Series<T>/Lot<T>/Vector<T>) as JSON arrays — registeredObjectelements map via the existing mapper registry, fundamental elements as primitives. Note:int[]/int64[]/double[]/bool[]/Object[]cannot exist as GObject properties at all on Vala 0.56 (the compiler drops them), sostring[]+ typed collections are the supported shapes. Collection read-back is write-side only (element types are erased from the property GType) — documented prominently in theto_properties/from_propertiesvaladoc.update_heldnow carries such array fields forward instead of dropping them (see S-14).
Object[] / Lot<T> / Gee.List properties are silently skipped
by to_properties (src/GObjectMapping.vala:80-83 skips unregistered
Object-typed properties; collections become non-primitives). Every
list-bearing slot must be hand-built as a PropertyDictionary with a
JsonElement array, and set_typed<T> / update_held<T> are unusable for any
model containing a list.StateJson.list<T>(lot) helper; update_held effectively dead.GObjectMapping (emit JSON arrays),
and/or ship a built-in State.json_array<T>(lot) / State.nested(model)
helper so the "list of row models" pattern is one call instead of ~10 lines of
PropertyDictionary plumbing. (Independent of the upstream numeric/bool bugs
— Invercargill can already build array JsonElements.)TypedStatumAction<TPrivate>.request_private is broken (typeof(TPrivate) → void) (orig #11)FIXED (2026-08-31, WP1 server-side effort). Root cause confirmed: valac carries generic type arguments as hidden construct properties, so actions instantiated reflectively (
scope.resolve_type— the normal path) getG_TYPE_NONEfortypeof(TPrivate); the erased(TPrivate)transfer also dropped the only reference to the materialised model. Fix:TypedStatumActionnow has aprotected virtual Type private_type(overridden in subclasses to name the private model — the example'sBumpCounterActionshows the pattern), an eroded type logs a warning naming the action type instead of crashing, and the model is held via a plainObjectfield so the reference survives the erased-generic return. Verified by a cross-namespace reproduction intests/TestMain.vala.
request_private calls
GObjectMapping.from_properties(typeof(TPrivate), …)
(src/StatumHandlers.vala:196), but at runtime typeof(TPrivate) resolves to
void (Vala generic erasure) → g_object_new_with_properties: assertion
G_TYPE_IS_OBJECT failed → private data maps to null. Had to bypass it and
read request.action_private manually.request_private silently returned null.typeof(T)-in-generics issue — pass the GType
explicitly from the subclass (e.g. an abstract Type private_type override)
or capture it another way — so request_private works as documented. (The
example's counter action apparently works — worth diffing why; possibly
same-compile-unit vs cross-namespace type registration.)Partially FIXED (2026-08-31, WP1 server-side effort): decryption failures now log a
warning()naming the seal namespace and action type, andrequest_privatelogs when the blob cannot be materialised. Therequires_valid_privatedefault for plain actions is unchanged (stillfalse).
decrypt_action_private (src/StatumHandlers.vala:151-164) returns
an empty PropertyDictionary on any failure instead of throwing/logging, and
requires_valid_private defaults to false
(src/StatumHandlers.vala:86), so a mis-sealed action just runs with an empty
context (attempt_id == 0).requires_valid_private to true for plain actions too (it
already is for TypedStatumAction, StatumHandlers.vala:185), and log
decryption failures with the namespace so mismatches are diagnosable.StateModifier.apply (src/TopicSystem.vala:111-143)
serialises the derived state via GObjectMapping.to_properties
(src/TopicSystem.vala:136), which can only carry scalar/registered-object
state — not a per-question results list. Fell back to a manual
state_service.update(key, …) with a stashed slot key on the background worker.PropertyDictionary (with
JsonElement arrays), or provide a blessed "background worker → held slot"
push keyed by a business id, so "job finishes → update the client's view"
doesn't require hand-stashing slot keys. (Cascades from S-7; fixing S-7's list
support removes most of the pain.)auth SESSION slot, read it back
from request.held in every handler, derive admin from the signed public
role, hand-roll guards.request.current_user, require_admin(), a server-side guard that issues a
navigate/error directive) and documented guidance on SESSION vs DEVICE scope
and static-key persistence (see S-12).FIXED (2026-08-31, WP1 server-side effort). The single-line whispers in
create_signing_provider/create_encryption_providerare now prominent multi-line startup warnings (still warnings, not errors) that spell out the consequence and point atstatum-genkeys.
web-config.json static keys, every signed SESSION/DEVICE
slot is invalidated on restart (ephemeral keys) → all users logged out. There
is only a quiet warning(...) at startup
(src/Statum.vala:86 and src/Statum.vala:104).statum-genkeys tool already exists in tools/).to_properties returns Properties, forcing a PropertyDictionary cast (orig #16, primary)FIXED (2026-08-31, WP1 server-side effort).
GObjectMapping.to_propertiesnow returnsPropertyDictionarydirectly; all Statum callers updated.
GObjectMapping.to_properties() returns the Properties interface
(src/GObjectMapping.vala:57); adding a list/nested value requires casting to
the concrete PropertyDictionary to use its indexer.to_properties return PropertyDictionary directly (it already
constructs one internally at GObjectMapping.vala:58). The complementary
upstream enhancement (give Properties a typed setter) is tracked as
invercargill-upstream-issues.md I-4.update_held<T> is effectively unusable for real models (orig #17)FIXED (2026-08-31, WP1 server-side effort). With S-7's array support,
string[]fields round-trip throughupdate_held, and JSON-array fields the typed model cannot read back (collection-typed properties) are carried forward unchanged instead of vanishing (GObjectMapping.preserving_arrays;StateService.update_global_typedbehaves the same). Rebuilding from the source of truth remains the canonical pattern for mutating collection contents.
update_held (src/DirectiveBuilder.vala:73) round-trips through
to_properties/from_properties, so it can't preserve list fields — they'd
be dropped. The real pattern is "reload from DB and update(key, State)".
update_held looks temptingly usable until your list vanishes.update_held
safe) or document loudly that update_held is scalar-only and the canonical
mutation pattern is rebuild-from-DB + update.{slug}/{id} aren't
in request.route_params. The documented workaround is to seal context into
the action's private data — verbose and easy to forget.StatumRequest, which is populated at
src/StatumHandlers.vala:125-132).stm-if evaluated falsy and why, and any with(scope) ReferenceErrors.X-Statum-Slot / X-Statum-Private headers are hard to construct for testing (orig #20)key=…; as_at=…) vs base64-frame
form, and that action private travels on X-Statum-Private, took
reverse-engineering the JS client.example/) is minimal (counter + a scalar
announcement). It doesn't exercise list-bearing slots, encrypted private data,
update_held, typed action private, editable form fields, selects, or
background pushes — i.e. exactly the things that bit.Bugs found and fixed while landing WP1–WP4 of the stack refactor; recorded here so they stay tracked. All are fixed.
from_properties crashed reading cached int64 ValueElementsFIXED (2026-08-31, WP1 server-side effort).
GObjectMapping.from_propertiesnow converts cachedValueElements through the native GValue transformation (Value.type_transformable/transform) instead of the genericElement.to_valuedispatch, whoseint64/uint64branches mis-handle the boxed representation and crashed on read-back. Verified byStatum/testsround-trips.
from_properties whose properties still
held server-cached ValueElements (rather than re-parsed JSON) crashed in
Element.to_value on int64 values — the read-side twin of the upstream
I-1 write-side crash.element_value in src/GObjectMapping.vala — the raw scalar never
passes through the generic element dispatch when a native transform exists.EncryptionProvider.read_properties use-after-freeFIXED (2026-08-31, WP1 server-side effort).
read_propertiespreviously returned aJsonObjectwrapping memory owned by a temporaryJsonElement; once that element was finalized the returned properties pointed at freed memory. It now re-parses the decrypted JSON into aPropertyDictionaryit owns, with values held by freshJsonElements. (Also noted under upstream I-2, where the crash was first chased.)
src/Cryptography/EncryptionProvider.vala)
handed back a view over a temporary parse tree; consumers reading it later
read freed memory.PropertyDictionary.Spry.Authentication.UserPermissionEntity was missing its Object baseFIXED (2026-08-31, WP2 Spry rewrite). Found while wiring the rewritten Spry (recorded here because the effort's issue log lives in these files): the entity class was declared without
: Object, so its GType was fundamental and it could not be instantiated — the user-management wiring crashed at startup. Fixed by deriving fromObjectlike every other entity.
PropertyDictionary plumbing everywhere.[hidden] vs author CSS — sneaky, app-wide risk, trivial library fix.request_private — a documented API that silently returns null.Note: the #1 overall pain item — the
int64/boolsegfault — is upstream (invercargill-upstream-issues.mdI-1/I-2); it crashes the whole process and cannot be fixed in Statum.