Ratings & reviews — derived from real reviews, written only by those who installed
Every Store/Plugin package — course, app or plugin — carries a rating and a list of reviews
without any per-package work: the surfaces are registered on the package ROOT's type, the storage
is a satellite of the package's own partition, and the rules live in one NodeType,
Store/Review (Store/Review/Source).
Where a review lives — two nodes, two structurally unique paths
| node | path | written by | holds |
|---|---|---|---|
| draft | {viewer}/_ReviewDrafts/{package} |
the author, through the node-bound form | rating, text |
| published review (public) | {package}/_Reviews/{viewer} |
only the Store (ReviewControlPlane, as System) |
rating, text, authorName, reviewedAt — a public PROJECTION, never a moderation field |
| moderation flag (private) | {package}/_ReviewModeration/{viewer} |
only the Store, after the admin gate | hidden, hiddenBy, hide time — never the text |
- One review per (user, package) is structural. The viewer is a path segment of both nodes, so there is no second node to write, no index to consult and no check-then-write race.
- The author edits their own node. The rating and comment controls are bound to the draft with
LayoutAreaReference.GetMeshNodeDataContext(draftPath)— the GUI writes each pick straight onto the author's node, in their own circuit, under their own identity. No/datareplica, no Save button (Doc/GUI/DataBinding). The controls bind only once the draft EXISTS (a control bound to an absent path NotFound-storms — the Quiz lesson), and the draft is created only when the viewer clicks Rate this, never on render: looking at a cover writes nothing. - The package's partition is written only by the Store. A plugin space is system-synced and
nobody else is Admin on it (AGENTS.md), so no viewer receives a grant there. The draft's own hub
runs
ReviewControlPlane, which publishes AS SYSTEM — theStore/InstallRequestshape: a node in the requester's own space, watched by its own hub, doing the privileged write on their behalf. - Why
_Reviewsis safe in a synced, gated partition. It is an underscore satellite:StaticRepoImporter.ComputePrunableNodesnever prunes it, so a GitSync of the package keeps every review, andPluginGate.GatedChildRootsnever denies it, so it inherits the cover's rootPublic/AnonymousViewer grants — everyone who can read the cover can read the reviews. That is the intended exposure, and the only one: the published node carries a display name (first name and last initial, never an e-mail address —ReviewRules.DisplayName), the rating and the comment. Contrast_Entitlements, which is capped private (EntitlementPrivacy.md) because a purchase is not something its holder chose to publish; a review is. - Reads follow
NodeLookupReadsTheOwner.md. The publisher reads the existing published review and the author's profile withMeshQueries.FindNode(listing for existence, the owner for content) and writes withCreateOrUpdateNode, whose emission is the owner's verdict. The review LISTS (cover, catalog) are live synced collections — the transport that doc names for "a live collection", not a one-node lookup.
Who may review — the install record, enforced where the write happens
Eligible = signed in AND the viewer's install record lists the package (ReviewRules.Eligibility).
The fact is TRI-STATE plus a fault (InstallCheck): until the record has answered the form says
Checking your install…, and a read that faulted says so and asks for a reload — neither is ever
rendered as "Install to leave a review", which would tell an installed learner something false.
The install record — {viewer}/_Install/{repo-slug} items with an installedAt, written LAST by the
one install engine (Installing.md) — is the Store's authoritative "this viewer installed this
package": the cover's Get/Repair step and the catalog cards read the same record
(InstallRecords.InstalledPackages, read by shape across every manifest, so a record stranded
under a former repo slug still counts). An entitlement alone is deliberately NOT enough — a buyer
who never installed has not used the package — and a pre-installed package needs no entitlement yet
has install records.
The server-side rules, all in ReviewControlPlane.Publish and all re-checked on every publication:
- Author. The draft's framework-stamped last author (
lastModifiedBy, elsecreatedBy— never a content field) must be the partition owner. System is NOT an author: the Store creates the empty draft as System on Rate this, and only the viewer's own edit can turn it into a review. So nobody — an administrator included — publishes in someone else's name (ReviewRules.AuthorizationFailure). - Install. The author's install records are re-read (synced query, System scope, by path) and must list the package.
- Validity. Rating 1–5, comment at most 1000 characters (
ReviewRules.Problem). The comment is plain text: control characters are dropped, markup is kept literally, and every surface renders it — and the author's name — throughControls.Label, never markdown or HTML.
The click handlers are server-side too: the viewer is captured synchronously from the authenticated context at the click (never a value the client names); Rate this re-reads the install before it creates anything; Delete my review only ever addresses the clicking viewer's two paths (draft first, so its watcher cannot re-publish, then the published review); the form's own eligibility gate is the courtesy, these are the security.
🚨 No read-then-replace, anywhere. An existing public review is changed only through its owning
stream's Update mapper, which computes the projection from the node as its OWNER holds it at that
moment; a missing one is created with the create verb, which can never overwrite (a lost create race
falls back to the mapper). Moderation never rewrites a review at all (below). The watcher's key is
everything the decision reads off the draft — rating, cleaned text AND the framework-stamped author
and the author's explicit submittedAt (ReviewControlPlane.DecisionKey).
🚨 A re-save of identical text is not an event — so a retry is the Publish button. Measured on the
gate (Plugins #2341, run 35966679071): after a refused edit by someone other than the owner, the
owner re-saving the byte-identical rating and text was never published. The mesh's write path posts
nothing when the new node equals the one it holds (IsRecordNoOp / an empty merge diff), so the
draft's lastModifiedBy stays the other editor's and its stream never emits: keying the watcher on
the author cannot see an event that never happened. The form therefore shows Publish whenever
the draft is valid but not what is published; it stamps submittedAt on the author's own draft, AS
the author (the authenticated context captured at the click and re-entered around the write, the
Edu AsCaller shape) — a real change, stamped with their identity, which is a new decision key. It
is also the retry after a publication that failed. submittedAt is draft-only and never published.
🚨 The publisher never writes the draft. A reconcile that writes the node it watches feeds
itself, and here it would also restamp lastModifiedBy to System, which the author rule reads. The
form's status line — pick a rating, publishing…, published — is DERIVED by the page from the
draft and the published review (ReviewAreas.FormStatus). Nothing the Store writes elsewhere can
re-trigger it.
A consequence worth knowing: a draft edited while the author is NOT eligible is not published, and is not retried by itself when they later install — their next edit publishes it. An uninstall does not remove a review already published.
The cold-start rule — never fake data
- Nothing is seeded, defaulted or fabricated. A package nobody reviewed has no review nodes; a new draft has no rating; there is no stored counter or average anywhere.
- Aggregates are DERIVED on every read (
ReviewSummary.Of) from the published review nodes: every review that is not hidden and carries a valid 1–5 rating counts once. - Below three real ratings the package is "New" (
ReviewSummary.Threshold = 3) — no stars, no average. From three on: the average to one decimal (half away from zero) and the count. - The catalog asks ONE synced query per category scope for all its cards
(
StoreCatalogLayoutAreas.ObserveRatings, keyed on the same scope as the viewer facts, so a re-emitting plugin feed never re-asks —CatalogRendering.md), and a card shows no badge at all until that answer lands, rather than a "New" it cannot know yet.
Install count — deliberately not shown
The honest source of "installed by N" would be the install records, and they live in each viewer's
PRIVATE home ({viewer}/_Install/*). Counting them means a System-identity fan-out across every
user partition, driven from an anonymous public surface, on every catalog render. The partition the
package owns holds only _Entitlements — which counts acquisitions, not installs, is empty for
pre-installed packages, and is capped private precisely because its existence leaked
(EntitlementPrivacy.md). Neither is an honest, cheap, privacy-safe count, so none is shown. The
accepted way to add one is a Store-owned, System-maintained per-package tally that the install
engine writes alongside the install record — a decision for the Store's owner, not a side effect of
this feature.
Moderation — minimal, and private
_Reviews is publicly readable by design (see above), so a hidden review must not live there in
any form — a flag on the public node would only be a UI filter, and anyone querying the container
would still read the hidden text and the moderator's id. So hiding MOVES the review out of public view
instead of marking it:
- Hide (
ReviewAreas.Hide): make{package}/_ReviewModeration/_Policydurable first —PartitionAccessPolicy { read: false, publicRead: false }, the_Entitlementsshape one container over (EntitlementPrivacy.md), judged on the owner's answer to the write and FAIL CLOSED — then write the private flag{package}/_ReviewModeration/{author}(who, when; no rating, no text), then DELETE the public node. While hidden, the review's text exists only in the author's own private draft; the public container holds nothing about it, and the count drops with it. - Publishing respects the flag, before AND after its write. The publisher checks the flag first (flagged → publish nothing, remove any public node) and again after its own write (a hide that landed mid-flight wins — the node is removed again). So an author's edit can never un-hide, and a hide never clobbers an edit: moderation never reads a review and writes it back.
- Show again (
ReviewAreas.ShowAgain): delete the flag, then re-publish from the author's draft through the ONE publisher, with every rule it applies. A draft that is gone stays unpublished. - The flag is per (package, author) and outlives the review. "Delete my review" removes the draft and the public node but not the flag, so deleting and re-posting does not escape a hide.
- Who. A global administrator (
hub.IsGlobalAdmin()— there is no per-package owner on a system-synced space). Each click runs the shared admin gate (SystemAuthorization.Authorize(hub, invoker, "moderate a review")— probed by id, bounded, fail closed) before anything is written as System. The moderator's view lists the hidden authors from the private flags, read as System only after that same admin check (metadata only: the author is the path), with Show again; every public review carries Hide. - What the cap relies on. Like
_Entitlements, theread: falsechild cap is effective once the package root carries the role-open policy the gating pass reconciles to; and because the flag holds no text, even a cap that failed would disclose only that an author's review was hidden, by whom.
Where it shows
| surface | what |
|---|---|
Catalog card (/Store?category=…) |
★ 4.3 (12), or 🆕 New below three ratings |
Cover (/{package}) |
the Reviews area embedded at the foot: summary, the viewer's form or the one-line reason ("Install to leave a review." / "Sign in to leave a review."), every visible review newest first — stars, display name, date in the VIEWER's time zone and chrome language (ChromeLocale.LongDate) |
/{package}/Reviews |
the same section on its own — the reachable URL for an entitled viewer, whom the cover redirects straight into the package (CoverContract.CoverRedirect) |
/{package}/RateThis |
the compact prompt: summary + form, no list |
An authored cover body that already embeds @@("area/Reviews") does not get a second one appended.
Every string is Store chrome in StoreTexts (English and German — the de table serves de-CH,
du-form).
Asking for a review — embed RateThis
A package asks where it wants to ask, by embedding its own root's RateThis area. A course ends its
last lesson (or its completion page) with:
@@("/AgenticPrimer/area/RateThis")
The embed is ABSOLUTE on purpose: the area lives on the package root and binds to the VIEWER's draft, so the same prompt works on the central lesson and inside a learner's installed copy (the relative-embed rule for exercise answer boxes is about binding to the learner's copy, which this does not). No Edu change is needed; an Edu completion hook can later render the same embed.
Where it lives
Store/Review.json (the NodeType), Store/Review/Source/PackageReviewContent.cs (the content —
named so because Collaboration/Review already claims ReviewContent), ReviewRules.cs (paths,
eligibility, validation, publication, moderation, the derived summary — all pure),
ReviewControlPlane.cs (the publisher), ReviewAreas.cs (Reviews, RateThis, the card badge),
registered on Store/Plugin (AddReviewAreas, which also registers the content type) and read by
Store/Catalog. Pinned by Store/Review/Test/ReviewTests.cs through the type's Tests area.
Pinned live (Store/Review/Test/ReviewLiveTests.cs, rendered by the type's Tests area on the
gate's mesh): the owner's edit publishes a System-authored public node; a viewer with no install
record publishes nothing — until the record exists and their next edit publishes; an edit by someone
other than the owner publishes nothing — and the owner's Publish of the SAME text afterwards
publishes. Each refusal case carries its own positive control, so a dead
watcher cannot pass as a refusal. Not yet pinned live: Hide / Show again.