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

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:

  1. Author. The draft's framework-stamped last author (lastModifiedBy, else createdBy — 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).
  2. Install. The author's install records are re-read (synced query, System scope, by path) and must list the package.
  3. 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 — through Controls.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

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:

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.

Reconnecting…
The connection to the server was interrupted. Trying to restore it…
Trying again…
The connection could not be restored. Reloading the page…
The server was updated. Reloading the page to pick up the latest version.