Data access — one read path, one write path, every query names its partition
Audit of 2026-09-23. Every Source/*.cs in every module was re-read against the platform's
Data Binding rules and the /gui + /mesh-data skills. This page records
the rules the modules now follow, what the audit changed, and what it deliberately left alone.
The rules
| You need to… | Use | Never |
|---|---|---|
| read the area's own node | host.Workspace.GetMeshNodeStream() (parameterless — the local branch) |
the path overload on your own address |
| read other nodes | hub.GetQuery("<module>-…:{what}", …) — a live synced query, every leg anchored |
GetMeshNodeStream(otherPath) (the permission probe can wedge), a one-shot mesh.Query(...).Take(1) |
| write a node's content | GetMeshNodeStream(path).Update(node => …) — runs on the owning hub against the live node; only the diff travels |
IMeshService.UpdateNode(copy) — a whole node read moments earlier, written over whatever landed in between |
| create / delete a node | IMeshService.CreateNode / CreateOrUpdateNode / DeleteNode (lifecycle) |
— |
| edit a node's fields | bind the control to the node: DataContext = LayoutAreaReference.GetMeshNodeDataContext(path) (RecordSupport.NodeText) |
seed a /data copy, then a Save button that reads it back |
| pick a node | [MeshNode("namespace:<P> scope:descendants nodeType:<T>", …)] — one leg per partition |
[MeshNode("nodeType:<T>")] |
| show the result of a write | nothing — every read is live, the write's echo repaints | a "refresh" tick in /data that re-subscribes the reads |
Every query names its partition — pickers included
The register/desk reads were anchored in #146 (Query reach). The
pickers were not: 190 [MeshNode("nodeType:X")] attributes across 75 files named no partition.
The store refuses such a query in CI (UnanchoredQueryException — the picker renders empty) and in
production serves it as the UNION over every partition schema, logged at Error — the fan-out
that seized both portals on 2026-08-31. Each now carries one anchored leg per partition the type's
instances ship in: the type's home partition, plus ReinsuranceDemo where the worked book ships
instances (Broker, Acceptance, Claims/Claim, Underwriting/Submission, the SST and IFRS 17
book types) or installs them at run time (Reinsurance/Cedent, via the Setup importer).
Known limit. A picker cannot name the viewer's own partition: the attribute's template offers
{node.namespace}/{node.path} (the node's own path), not its partition. A demo copy installed into
alice/ReinsuranceDemo therefore picks from the shipped partitions, not from alice. Closing that
needs a {node.partition} template in core's MeshNodeAttribute.ResolveQueries.
Editors are bound to the node; commands are not editors
- The ILS order draft (recipient, subject, body) is a plain editor — it is now bound straight to
the order node and the Save draft button is gone. Approve reads the order inside the
Updatelambda, as the owning hub holds it. Known trade-off: a node-bound field has no server-side status guard of its own — the form is rendered only while the order is a Draft and disappears as soon as the live node moves on, so a stale editor can at worst patch the message of an order approved in the same instant. Approve itself re-checksStatus == Draftinside the owning hub's update. - The decision, allocation and recommendation forms are commands, not editors: what the viewer
types is parsed (
20m,750 bps,yyyy-mm-dd), carries a journaled reason, and is applied as one audited step that records a revision per changed field. Their input is transient view state in the area's/data(like a status line), and the node is written only by the command throughIlsWorkflow.UpdateOwn. Binding them to the node would write unparsed text and skip the audit.
What the audit changed
- 190 picker queries anchored (above), plus the two doc pages that show the attribute.
ClaimsReview: taking over / reverting a claim position isGetMeshNodeStream(claim).Update(…), no longer aTake(1)read of another hub's node followed byUpdateNode; the refresh tick is gone (the claims and enquiries are live synced queries).ReinsuranceDemo/InstallerSetup: the "currently in …" counts are ONE liveGetQuery(three anchored legs, built under the viewer's context) instead of three one-shot queries re-run by a refresh tick; the headless import records its outcome through the own node'sUpdate.- ILS order form: node-bound (above).
scripts/validate-repos.py→check_data_access: a picker query withoutnamespace:/path:, apartitions:all, or anUpdateNode(call in any module's C# fails the PR.
Deliberately left alone
- Status and compose lines (
host.UpdateData(StatusId, …)) — transient view state, never node content. - The ILS intake's Refresh button — it re-lists the content collection (uploaded files), which is not a mesh query and has no live stream to bind to.
- Existence probes before a create in the Setup importer (
path:{p}one-shots inside the import action) — lifecycle, not rendering. Controls.HtmlinEconomicsArea(the waterfall SVG) andClaimsReviewArea(summary chips) — presentation, not data access; tracked separately.