KilnCMS 1.3.0
Added
- Content webhooks carry
data.affected: the published documents whose fired artifacts embed this one, so a consumer can invalidate those pages too instead of waiting out a TTL. A formula page that lists a herb re-fires when the herb changes, so its delivered artifact changes too, but theherb.updatedwebhook named only the herb, and Verscienta.com's cache left the formula page to a 10-minute TTL.KilnCMS.Firing.References.affected/1reads the same reference edges the re-fire wave walks, tenant-scoped as the firing system actor, keeps the referrers that are currently published (the list is for invalidating pages a consumer serves), and names each astype(the public type name, as event names use it),id,slugandlocale.NotifyWebhooksadds it to every full payload,unpublishedincluded; tombstones stay identity only by contract. An empty list means nothing embeds the document; a failed lookup logs and answers[]rather than cost the delivery.@kiln-cms/clientexports theAffectedDocumenttype and the Elixir client'sKilnClient.Webhookdocuments the key. (#1925)
mix kiln.plugins.doctornotes a plugin with nosummary/0(or the generator'sTODO:placeholder) without failing the run. (#1942)
- The content list filters on records never reviewed (
?reviewed=never) and on records missing a consent kind (?consent_missing=<kind>); the governance dashboard links both as work queues. The Content health panel answers "what is late", which cannot cover a first-pass review: a record without a cadence is always:fresh, and a cadenced record never reviewed counts frompublished_at, so it reads as an ordinary re-review.reviewed=nevermatches a nillast_reviewed_at.consent_missingmatches records with no consent of a configured kind; consents reference content by id only, so the filter is an unrelatedexistsoverKilnCMS.CMS.Consent, authorized as the viewer and scoped to the current site. Both are facets under More filters, combine with the rest (e.g.?type=herb&consent_missing=medical_review), and can be saved as views. The dashboard's Work queues line links Review due, Never reviewed and one missing-consent list per kind. TheHealthSummarymoduledoc pointed at/editor?health=overdue, which the list ignores; it now sayshealth=due. Sorting by a type's own field (a catalogue's toxicity level, say) is left to the overlay. (#1953)
Changed
- The path forward's wedge minors are 1.3 to 1.8: v1.2.0 shipped the
toolchain move without them.
docs/path-forward.mdwas written against v1.1.0 with the first wedge release as 1.2; v1.2.0 then shipped the same day with the Elixir 1.20 / OTP 29 toolchain move and no wedge work, so every minor in the 2.0 plan moves up one. The six tracker issues carry the new numbers in their titles. (#1945)
- A
<type>ChangedGraphQL subscription now tells anonymous subscribers when a published record is unpublished or archived, asdestroyed. A retraction is an:updateto Ash, and ash_graphql resolves an update per subscriber through the policy-scoped read: once the record left:publishedthe anonymous read answered not found, and the batcher suppresses not-found results rather than leak that a record exists, so the one event a public reader most needs never arrived. The GraphQL guide said as much and told readers to poll.KilnCMS.CMS.Changes.NotifySubscribersWithdrawnnow sits besideNotifyWebhooks event: "unpublished"on:unpublish,:unpublish_scheduled,:archive_scheduledand:archive(the last only when the record was published) and publishes the same notification a real:destroywould, so the push arrives asdestroyedwith the id, the one arm of the union that needs no read. The contract, now in the guide:destroyedmeans the record left the published feed, whether unpublished, archived or deleted. A bearer-authed editor also receives theupdatedpush for the same transition. Archiving a draft stays silent. (#1925)
Fixed
The reranker scores relevance instead of 1.0 for every pair, and a reranker model that cannot be loaded no longer stops Kiln from booting. A cross-encoder reranker (
BAAI/bge-reranker-base, the default) has a single output logit, and Bumblebee's text classification defaults to softmax, which over one logit is exactly 1.0. So a reranked section kept its fused order, but every hit's score became 1.0./api/askthen sorted sources across content types by those tied scores and fell back to registry order: the bug its flat sort was written to fix.KilnCMS.Search.RerankerServingnow builds the serving withscores_function: :sigmoidfor a single-logit head (scores_function/1), so a score is a 0–1 relevance with the logit's order. A head with more labels keeps softmax and logs a warning at load. Separately, the serving used to load the model with{:ok, _} =whileKilnCMS.Applicationwas building its child list. With reranking switched on and the model missing from an offline image's cache, the match failed and the application crash-looped at boot.RerankerServing.load/0now returns{:error, reason}instead of raising.RerankerServing.children/1turns that into no serving child and aKilnCMS.Config.Report.error/3, and the application starts without a reranker.RerankerServing.status/0answers:off,:runningor{:unavailable, reason}after boot.KilnCMS.Search.Reranker.Bumblebee.scores/2answers{:error, _}. It now also catches the exit from a serving that is not running, and it warns at most once a minute, logging the failure's kind and never the query.
Every surface keeps its fused order, as when reranking is off.
build/0keeps its contract and raises with the reason. One caveat remains: because reranking is per section, a section whose rerank call fails keeps fused RRF scores while the others carry 0–1 scores, so/api/ask's flat sort mixes the two scales (documented indocs/rag.md). (#1941)