Skip to main content

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 the herb.updated webhook named only the herb, and Verscienta.com's cache left the formula page to a 10-minute TTL. KilnCMS.Firing.References.affected/1 reads 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 as type (the public type name, as event names use it), id, slug and locale. NotifyWebhooks adds it to every full payload, unpublished included; 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/client exports the AffectedDocument type and the Elixir client's KilnClient.Webhook documents the key. (#1925)
  • mix kiln.plugins.doctor notes a plugin with no summary/0 (or the generator's TODO: 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 from published_at, so it reads as an ordinary re-review. reviewed=never matches a nil last_reviewed_at. consent_missing matches records with no consent of a configured kind; consents reference content by id only, so the filter is an unrelated exists over KilnCMS.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. The HealthSummary moduledoc pointed at /editor?health=overdue, which the list ignores; it now says health=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.md was 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>Changed GraphQL subscription now tells anonymous subscribers when a published record is unpublished or archived, as destroyed. A retraction is an :update to Ash, and ash_graphql resolves an update per subscriber through the policy-scoped read: once the record left :published the 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.NotifySubscribersWithdrawn now sits beside NotifyWebhooks event: "unpublished" on :unpublish, :unpublish_scheduled, :archive_scheduled and :archive (the last only when the record was published) and publishes the same notification a real :destroy would, so the push arrives as destroyed with the id, the one arm of the union that needs no read. The contract, now in the guide: destroyed means the record left the published feed, whether unpublished, archived or deleted. A bearer-authed editor also receives the updated push 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/ask then 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.RerankerServing now builds the serving with scores_function: :sigmoid for 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, _} = while KilnCMS.Application was 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/0 now returns {:error, reason} instead of raising.
    • RerankerServing.children/1 turns that into no serving child and a KilnCMS.Config.Report.error/3, and the application starts without a reranker.
    • RerankerServing.status/0 answers :off, :running or {:unavailable, reason} after boot.
    • KilnCMS.Search.Reranker.Bumblebee.scores/2 answers {: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/0 keeps 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 in docs/rag.md). (#1941)