Abstract
Mainstream social recommenders optimize for engagement. Because arousal correlates with engagement, and outrage is the cheapest available arousal, these systems tend toward amplifying anger, fear, and tribalism — a result widely documented in both internal platform research and external audits.¹,²,³ We describe the design and production deployment of a different recommender on Feason, a non-denominational Christian scripture-study platform, that points the same machine-learning machinery at a different objective function: edification (orientation toward hope, growth, dignity, repentance, lament-with-vulnerability, and peacemaking) minus a magnified reactivity penalty (orientation toward contempt, fear, tribalism, shame, despair, and division). The system uses dense embeddings (Voyage voyage-3, 1024-d) and a constrained categorical taxonomy derived from the Beatitudes (Matthew 5:3–10) to compute per-user state vectors that personalize a feed without forming a comfort bubble. Denominational identity is not a ranking feature; disagreement is evaluated by its manner, not by which Christian tradition expresses it. The pipeline is implemented end-to-end on Next.js + Supabase Postgres with pgvector, runs across four scheduled cron jobs, and ships in production. We describe the formal scoring function, the privacy architecture (encrypted-journal exclusion), the explicit anti-bubble mechanisms, and the limits of what we can presently evaluate.
Keywords: recommender systems · alignment · pro-social ranking · dense retrieval · filter bubbles · pgvector · Beatitudes.
Reading map
The paper moves from moral premise to deployed mechanism:
- Premise — §§1–3. Why engagement is the wrong target, the constraints that follow, and the taxonomy used to make those constraints computable.
- Ranker — §§4–7. The scoring function, the user state it reads, the behavioral signals it accepts, and the scripture it brings into the feed.
- Guardrails — §§8–10. Privacy by construction, production implementation, and what the system can and cannot presently prove.
- Context — §§11–12. Related work and the narrower claim this paper is prepared to make.
- Reproduction — Appendices A–B. The load-bearing tagging rubric and database schema.
1. Motivation
Most large social platforms maximize a proxy for value — typically a weighted combination of dwell time, click-through, replies, and reposts. Over time, this proxy diverges from value: human attention is captured by emotionally arousing stimuli, and the cheapest arousal to produce at scale is anger.¹ Internal platform research has repeatedly identified this dynamic;⁴ external work has shown the same with public data.²
Two architectural responses are common in critique but rare in deployment:
- Replace the optimization target. Instead of engagement, optimize for some other property — wellbeing, learning, friendship strength.⁵ Goodhart's Law⁶ applies regardless of which target is chosen, but the character of the resulting failure modes differs sharply with the target.
- Add reflexive guardrails. Bubble-prevention via deliberate diversity injection, ranking transparency, user-side reweighting.⁷
This paper describes a production system that does both. We do not claim that the resulting feed is "objective" or "unbiased" — every ranking is a value choice. We do claim that the value choice we made is more defensible than the one most platforms have inherited by default.
2. Design principles
The system was built under six constraints:
P1. The objective is the system. Every implementation decision derives from the choice of objective function. We treat "what should the feed maximize?" as the first-order design question and defer all others (architecture, model selection, infrastructure) to its consequences.
P2. Lament is edifying. A naive sentiment classifier would code grief and doubt as negative content and downrank them. This would be precisely the wrong move on a faith platform: lament is constitutive of Christian spirituality (cf. Psalms of lament, the Book of Job, Christ's cry of dereliction). The tagging prompt explicitly instructs the model that lament, grief, doubt, and hard self-examination offered with vulnerability are highly edifying. This is encoded in §3.
P3. Asymmetric scoring. Rage-bait should not merely fail to surface — it should be penalized more than edifying content is rewarded. We use a reactivity multiplier of λ = 1.5 (§4.1). Mathematically equivalent to a loss-averse reward function, this also reflects the moral asymmetry: causing harm and providing benefit are not symmetric ethical acts.
P4. Encrypted content does not enter the algorithm. Feason's Discernment editor stores per-user reflections under client-side encryption (encrypted_title, encrypted_body, iv, salt). The server cannot read these entries. They are deliberately excluded as a state-vector source. This is not a regrettable cost but an architectural feature: the most private content stays out of the algorithm by construction.
P5. Personalization without sealing. Pure personalization leads to filter bubbles: a user shown only what matches their current state grows narrower, not wider. We reserve fixed slots in the surfaced feed for high-quality content outside the user's state vector (the "challenge slot," §4.3). The aim is not maximum satisfaction in any given session but long-term spiritual formation.
P6. Christian without denominational capture. Feason is intentionally non-denominational. The ranker does not classify or reward denominational identity, and it must not encode one tradition's disputed doctrine, vocabulary, or practice as universal Christianity. Disagreement may still receive a reactivity score, but that score concerns its manner — charity, contempt, fear, peacemaking — rather than which Christian tradition speaks. This is a product and governance constraint, not a claim that the underlying language model is free of denominational bias; that limitation requires continuing audit (§10.2).
3. Content taxonomy
Each piece of user-generated content (UGC) is scored along two orthogonal continuous axes and tagged with two categorical sets.
3.1 Continuous axes
- Edification ∈ [0, 1]: how strongly the content orients readers toward hope, growth, dignity, connection, repentance, gratitude, peacemaking, or honest lament before God.
- Reactivity ∈ [0, 1]: how strongly it orients readers toward contempt, fear-mongering, tribalism, shame-throwing, despair, in-group/out-group division, or doctrinal score-settling.
These axes are explicitly not opposites. A vulnerable lament about anger toward an unjust situation can be high on both — high edification (vulnerability, honesty before God) and high reactivity (anger, division). The ranker scores them independently and combines via the asymmetric formula in §4.1.
Tagging is performed by a small instruction-tuned model (Anthropic claude-haiku-4-5⁸) with a strict-JSON output constraint and a sanitizing parser that drops malformed outputs rather than coercing them. The job is asynchronous: posting does not wait for either the tagger or the embedding model.
3.2 Free-form themes
The model also returns 1–5 lowercase tokens describing the content's subject matter, drawn from a recommended vocabulary (gratitude, lament, doubt, grief, prayer, repentance, hope, fear, forgiveness, …) with permission to add tokens outside the vocabulary when no listed term fits. These tokens are used in the admin dashboard for distribution monitoring; they do not currently enter the ranking score directly.
3.3 Beatitude taxonomy
Each piece of content is also tagged with 0–3 of the eight Beatitude states from Matthew 5:3–10:
poor_in_spirit, mourning, meek, hungering_for_righteousness, merciful, pure_in_heart, peacemakers, persecuted
The taxonomy is closed — the parser whitelists exactly these eight keys and discards anything else, preventing model drift into invented categories. The semantic content of "speaks to those mourning" or "speaks to peacemakers" is explained to the tagger in the system prompt. A separate set of current_beatitudes is computed per user (§5.2) from the decayed tally of their own writing's Beatitude tags.
We use the Beatitudes for three operational reasons:
- Rooted in shared scripture. Unlike a freshly invented taxonomy, the categories begin with a text received across Christian traditions.
- Legible. The source and intent of the categories can be shown to users, which aids explainability and review.
- Eight is small enough to be tractable. Larger taxonomies suffer from sparsity at small user populations; smaller ones lose resolution.
These eight keys and their short glosses are Feason's operational taxonomy, not a claim to settle the interpretation or enumeration of the Beatitudes. Christian traditions differ in how they number, translate, and expound this passage. We claim only that this is a coherent and inspectable vocabulary for the ranker.
4. The scoring function
4.1 Score formula
For a content item c and viewer u:
with λ = 1.5, γ = 0.3, β = 0.15, K = 2 (so the Beatitude bonus is capped at 0.30). Here:
e(c), r(c) ∈ [0, 1]are the edification and reactivity scores from §3.v_c ∈ ℝ¹⁰²⁴is the content embedding produced by Voyagevoyage-3over the content body.v_u ∈ ℝ¹⁰²⁴is the user state vector (§5).B_c ⊆ {8 Beatitude keys}andB_uare the content's served Beatitudes and the user's current Beatitudes.
Untagged content is assigned a neutral phase-1 score of 0.3 to avoid penalizing freshly-posted material before the cron catches up. Missing embeddings or missing state both reduce the corresponding term to zero, degrading gracefully to phase-1-only scoring.
Notation. The symbols used throughout this paper:
| Symbol | Meaning |
|---|---|
e(c), r(c) | Edification, reactivity scores of content c; both in [0, 1] |
λ = 1.5 | Reactivity penalty multiplier (§P3) |
γ = 0.3 | Personalization weight on cosine similarity |
β = 0.15, K = 2 | Beatitude-overlap bonus per match, capped at K matches |
v_c, v_u ∈ ℝ¹⁰²⁴ | Content embedding; user state vector |
B_c, B_u | Beatitude tag sets for content and user (Matt 5:3–10) |
S_u | Items contributing to u's state (90-day window) |
w_i = exp(-(ln 2) · age_i / h) | Time-decay weight, half-life h = 14 days |
p_i ∈ (0, 1] | Interaction-depth prior (§6); 1.0 for authored items |
L = 20, pool = ⌈1.5·L⌉ = 30 | Surfaced window size; recall pool size |
4.2 Pool-and-prune
The home feed is heterogeneous: Gleams, articles, community activity, and scripture-related items share the final response. Gleams and articles are each pulled from a 30-day chronological window into a pool 1.5× wider than that source's working limit. Each pool is scored independently, and the best candidates from that source are retained before all feed types are merged. The default response limit is 20, but request limits can vary; “top 20 from 30” describes the default per-source reranking pass, not a global learned ranker over every item type.
4.3 Challenge slot
Three positions in the reranked Gleam list (zero-based indexes 2, 9, 16 when the list is long enough) are designated challenge slots. After scoring, we identify the subset of Gleam-pool candidates that satisfy both:
e(c) - λ · r(c) ≥ 0.4(high phase-1 quality, no personalization signal applied), andcos(v_c, v_u) ≤ 0.6(genuinely different from current state)
We sort these by similarity ascending (most different first) and place them into the challenge slots, overwriting the picks that would otherwise occupy those positions. If fewer than three candidates qualify, we fill what we can and leave the rest unchanged.
This implements §P5: a deliberate, non-stochastic, similarity-aware diversity injection. Unlike pure ε-greedy exploration,⁹ challenge picks must first clear the phase-one quality threshold. Because source lists are subsequently merged and sorted, these indexes are not promises about the picks' final positions in the heterogeneous feed.
4.4 Algorithm
The full per-request ranking flow, combining the formula of §4.1, per-source pool-and-prune of §4.2, the Gleam-only challenge mechanism of §4.3, and the scripture splice of §7:
Algorithm 1 — Home-feed assembly for user u
Input: user u (optional), request limit L, optional source filter
Output: ordered heterogeneous list R of at most L items
1. G ← recent Gleam pool; A ← recent article pool
2. for each c in G and A:
q1(c) ← e(c) − λ · r(c) # phase-1 quality
q2(c) ← q1(c) + γ · cos(v_c, v_u)
+ β · min(|B_c ∩ B_u|, K) # full score
3. G' ← top source-limit Gleams by q2
4. A' ← top source-limit articles by q2
5. C ← { c ∈ G \ G' : q1(c) ≥ 0.4 ∧ cos(v_c, v_u) ≤ 0.6 }
6. replace available indexes {2, 9, 16} in G' with up to 3 members
of C, ordered by ascending similarity
7. R ← G' ∪ A' ∪ eligible community and scripture-comment items
8. sort R by source timestamp, descending
9. if first page AND signed in AND user state exists:
V ← feed_scripture_match(v_u, 12)
V ← deterministic 2-of-12 by hash(user_id, today)
splice V into R at zero-based indexes {4, 11}
10. return first L items of R
The deployed implementation lives in app/api/home-feed/route.ts. The pseudocode intentionally omits cursor pagination, blocked-author filtering, engagement-count hydration, and source-specific eligibility checks, but preserves the ordering of the ranking and merge stages.
5. The user state vector
5.1 Sources
Three signal sources feed each user's state vector:
| Source | Description | Privacy |
|---|---|---|
| Own Gleams | Posts the user has written publicly | Public |
| Scripture notes | Plaintext server-side; user's private notes on Bible passages | Server-readable; embedded only as derived vectors |
| Feed interactions | Per-Gleam engagement (like, dwell, scripture-open, reply, save) joined to the Gleam's embedding | Behavioral signal |
The third source is critical: many users on any platform are predominantly readers, not writers. Without it, personalization can only function for users who post — a substantial population is left in the cold start.¹⁰
Discernment journal entries are excluded as a fourth source by design (§P4). Their inclusion would require breaking client-side encryption.
5.2 Aggregation
The state vector at time t for user u is:
where S_u is the set of content items (Gleams, notes, or interaction targets) the user produced or engaged with in the last 90 days; x_i is the content's Voyage embedding; w_i = exp(-(ln 2) · age_days_i / h) is the exponential time-decay weight with half-life h = 14 days; and p_i ∈ (0, 1] is the prior weight derived from the interaction kind (1.0 for an authored item; the interaction depth weight from §6 for an engagement).
The 14-day half-life means that each additional 14 days halves an item's contribution. This is a guess we plan to revisit once we have behavioral data on which to fit it; the current value was chosen to let the state follow a person's recent concerns without swinging with every session.
current_beatitudes is computed from the user's own Gleams only; scripture notes and interactions contribute to the dense vector but not to this categorical tally. Engaging with content that serves a state is not the same as being in that state. We take the top 5 Beatitude keys by decayed-weight tally. During onboarding, a user may also select up to three Beatitude descriptions; the system seeds an initial state from the corresponding Matthew 5 verse embeddings when those embeddings are available.
The state builder is scheduled once daily and processes a bounded batch of stale-or-new users per run (default 20, hard cap 100). Total computation is bounded by the number of selected users and the content items in each user's 90-day source window.
6. Behavioral signal
Each user interaction is logged with a depth-weighted score in feed_interactions:
| Kind | Weight | Source |
|---|---|---|
like | 0.1 | Server-side from like toggle (only on fresh likes, not unlikes) |
dwell | 0.3 | Client-side IntersectionObserver after 3 s of ≥ 50% viewport coverage |
scripture_open | 0.5 | Client-side click handler on the in-Gleam scripture anchor |
reply | 0.7 | Server-side from reply creation |
save | 1.0 | Server-side from save toggle (only on fresh saves) |
Weights are server-set on insert — client-provided weights would be the obvious attack surface, so the route looks them up by kind and ignores any client value.
We deliberately do not log negative interactions (skip, scroll-past, etc.). The asymmetry is principled: a positive interaction is a deliberate act by the user; a non-interaction has too many possible causes (didn't see it, was interrupted, was unimpressed) to be reliably interpreted.¹¹
7. Scripture in feed
Razon, Feason's scripture-retrieval subsystem, currently embeds roughly 31,000 verses from the 66-book World English Bible corpus verse-by-verse using the same Voyage voyage-3 model. Christian traditions recognize different biblical canons; this corpus description is a technical boundary, not a claim that one canon exhausts Christian scripture. The indexed verses and user state vectors live in the same 1024-d space and can be compared by cosine similarity end-to-end. We exploit this with a database function:
feed_scripture_match(query_embedding vector(1024), match_count int)
-> TABLE (id, book_num, chapter, verse, text, similarity)
On the first feed page of an unfiltered, signed-in request, we retrieve the top 12 verses by cosine similarity to the user's state vector, then deterministically pick two with a (user_id, date)-seeded pseudorandom shuffle. The pair stays consistent across the user's session but rotates daily, preventing the same verses from becoming wallpaper while preserving the state-match priority.
The two scripture tiles are spliced into the sorted feed at positions 5 and 12 (1-indexed). They are labeled "Found for you" but the algorithm is fully visible in the source: there is no claim of supernatural revelation; what's surfaced is what's semantically nearest to where the user has been.
This is the system's most characteristically Feason feature. The cross-pollination between scripture and user state requires both a verse-level embedded corpus and user-state vectors in the same embedding space. The design is reproducible; we make no claim that Feason is the only platform capable of satisfying those conditions.
8. Privacy architecture
Three layers of protection:
-
Encrypted-journal exclusion (§P4). The Discernment journal is end-to-end encrypted client-side. Server-side keys do not exist. Therefore the algorithm cannot read journal entries even if it wanted to.
-
Vector-only derivation of private content. Scripture notes are server-readable plaintext but are never exposed cross-user. The algorithm computes their embedding once and stores only the vector in
note_signals.embedding; the vector is then used as input to a user's own state computation. No other user sees any artifact derived from one user's notes. -
Internal-only signal tables.
post_signals,article_signals,note_signals,user_state, andfeed_interactionsall have row-level security enabled with no public policies. The Supabase service-role client (used only server-side) bypasses RLS; the anonymous and authenticated client roles see nothing.
The above does not eliminate all privacy concerns — server-side logs can still leak via operational mistakes, and our cron secrets are bearer tokens that could be replayed if exposed. But the structural privacy guarantee — that the algorithm cannot read your encrypted journal — is enforced by cryptography, not by policy.
9. Implementation
The full stack:
- Next.js 15.5 (App Router) on Vercel for routing and server-side rendering.
- Supabase Postgres with pgvector extension for content storage and vector similarity (HNSW indexes with
vector_cosine_ops). - Anthropic Claude Haiku 4.5 for content tagging.
- Voyage AI
voyage-3for embeddings (1024-d, asymmetricdocumentvsqueryinput types). - GitHub Actions scheduled workflows for the four recommender cron jobs.
- Upstash Redis (via the existing
lib/rate-limitdistributed limiter) for rate-limiting high-frequency interaction endpoints.
The pipeline is built around four scheduled jobs, each implemented as a Next.js route hit by a GitHub Actions cron:
| Job | Schedule | Purpose |
|---|---|---|
tag-gleams | daily, 02:00 UTC | LLM-tags newly posted Gleams (edification, reactivity, themes, Beatitudes) |
embed-notes | daily, 03:00 UTC | Embeds new or edited scripture notes via Voyage voyage-3 |
tag-articles | daily, 04:00 UTC | Same rubric, applied to long-form articles |
build-user-state | daily, 05:00 UTC | Recomputes a bounded batch of stale users' state vectors and current_beatitudes |
The schedules are independent so a stall in one job does not back the others up. Each route is gated by a shared bearer token (CRON_SECRET) and is idempotent: re-running it after a partial failure resumes from the rows still flagged stale, without double-tagging.
The system uses services already present in Feason's stack. That makes the design operationally modest, but not conceptually automatic: choosing, documenting, and auditing the objective remains harder than assembling the infrastructure around it.
10. Evaluation and limitations
10.1 Evaluation is genuinely difficult
The standard recommender-system evaluation pattern — offline A/B against held-out engagement metrics — is misaligned with our design. Maximizing engagement is precisely what we are not trying to do. Optimizing for clicks at A/B time would defeat the system at deployment time.
The metrics we do care about — long-term spiritual formation, depth of reflection, charitable engagement with disagreement — are not measurable in a session. They are not even reliably measurable in a year. This is a fundamental epistemic limitation of any system that optimizes for non-instrumental goods.
What we can measure at deployment:
- Tagging coverage and calibration (manual review of dashboard samples)
- Distribution of edification/reactivity scores over time
- Behavioral-signal-to-content correlations once we have volume
- Whether scripture-in-feed matches feel "found" or "random" to users
These are necessary but not sufficient checks. We accept that we will be operating partly on intuition for the foreseeable future.
10.2 Known limitations
-
Cold start. Onboarding now lets a user select up to three Beatitude descriptions and, when the corresponding verse embeddings are available, seeds an initial vector from Matthew 5:3–10. Users who skip the prompt or cannot be seeded receive Phase-1 (unpersonalized) ranking until authored content, notes, or interactions produce a state.
-
Tagger calibration drift. Claude Haiku's interpretation of "edifying" is not formally specified beyond the prompt; if the model is updated or the prompt is changed, scores from before and after are not directly comparable. We version this via
model_versioninpost_signalsbut do not currently re-tag historical content. -
Single-axis personalization. The state vector is one dense vector per user. A user with multiple distinct concerns (e.g., grief about a family loss + ongoing gratitude about work) gets a vector that averages both, which may match neither well. A mixture-of-vectors approach is plausible future work.
-
Adversarial robustness. Performative lament — content stylistically resembling vulnerability but lacking substance — would currently score high. At present community scale this is not a realistic threat, but at scale it would need attention.
-
No formal A/B framework. We have no system in place to run randomized experiments on ranking weights. This is an honest gap; the cost of building it has so far exceeded our confidence that we know what to test.
-
Denominational and linguistic calibration. The Beatitudes are shared Christian scripture, but the English-language rubric and the model applying it may still carry assumptions associated with particular traditions or cultural settings. We have not yet completed a formal cross-tradition calibration study. The non-denominational constraint in §P6 therefore functions as an explicit review standard, not as evidence that bias has already been eliminated.
11. Related work
The literature most directly relevant to this work falls into four areas:
-
Engagement-maximizing harms. Documented effects of platform algorithms on outrage propagation,² teen mental health,⁴ and political polarization.³ These are the failure modes our objective function is chosen to avoid, not merely critique.
-
Pro-social recommender design. Stray (2020)⁵ outlines the design space of recommenders optimizing for "wellbeing" rather than engagement; this paper sits within that frame and contributes a specific theological taxonomy and an asymmetric scoring formula.
-
Filter-bubble mitigation. Pariser's original formulation⁷ motivated subsequent work on diversity-aware ranking;¹² our challenge-slot mechanism is in that tradition but uses similarity thresholds rather than diversity objectives in the loss.
-
Embedding-based retrieval. Standard dense retrieval techniques¹³ are used here in a slightly unusual configuration: the comparison vector is not user-typed text but a materialized user state aggregated from past content.
12. Conclusion
The argument of this paper is small and concrete: it is possible to build a personalized social recommender in production whose objective function is not engagement, and it is possible to do this with off-the-shelf infrastructure in a workable time budget. The conceptual choice of what to maximize is harder than the engineering. Once that choice is made, the rest follows.
Whether the resulting feed actually leads to spiritual formation is a question we cannot yet answer. What we can say is that this design space is underexplored, and that asymmetric scoring, a Beatitude-derived operational taxonomy, challenge picks, encrypted-content exclusion, and scripture retrieval in a shared embedding space form a coherent system worth testing in public.
We publish this paper in the hope that others working on platforms with non-engagement objectives find some of the choices reusable. For Feason, reuse also has a boundary: broadly Christian formation can guide the platform without quietly turning one denomination's disputed convictions into the default for everyone.
Reproducibility. The deployed Feason source remains closed; the design presented here is published in full. Appendix A reproduces the current tagger system prompt; Appendix B documents the operational signal tables without pretending to be a migration file. A workable reimplementation requires an LLM with reliably parseable structured output, a shared embedding space for content and user state, and a vector-capable store. We use Claude Haiku 4.5, Voyage voyage-3, and Postgres with pgvector, but the design does not depend on those vendors.
References
[1] Brady, W. J., et al. (2017). Emotion shapes the diffusion of moralized content in social networks. Proceedings of the National Academy of Sciences, 114(28).
[2] Milli, S., et al. (2023). Twitter's algorithm: amplifying anger, animosity, and affective polarization. arXiv:2305.16941.
[3] Bakshy, E., Messing, S., & Adamic, L. A. (2015). Exposure to ideologically diverse news and opinion on Facebook. Science, 348(6239).
[4] Wells, G., Horwitz, J., & Seetharaman, D. (2021). Facebook knows Instagram is toxic for teen girls. The Wall Street Journal, September 14, 2021.
[5] Stray, J. (2020). Aligning AI optimization to community well-being. International Journal of Community Well-Being, 3(4), 443–463.
[6] Goodhart, C. A. E. (1975). Problems of monetary management: the UK experience. Reserve Bank of Australia.
[7] Pariser, E. (2011). The Filter Bubble: What the Internet Is Hiding from You. Penguin Press.
[8] Anthropic. (2025). Claude 4.5 models technical overview. https://www.anthropic.com/
[9] Sutton, R. S., & Barto, A. G. (2018). Reinforcement Learning: An Introduction. MIT Press, 2nd ed.
[10] Schein, A. I., Popescul, A., Ungar, L. H., & Pennock, D. M. (2002). Methods and metrics for cold-start recommendations. SIGIR '02.
[11] Joachims, T., et al. (2017). Accurately interpreting clickthrough data as implicit feedback. SIGIR Forum, 51(1).
[12] Castells, P., Hurley, N. J., & Vargas, S. (2015). Novelty and diversity in recommender systems. Recommender Systems Handbook, 2nd ed., Springer.
[13] Karpukhin, V., et al. (2020). Dense passage retrieval for open-domain question answering. EMNLP 2020.
A. Tagging prompt
The Claude Haiku tagger is steered by a single static system prompt. The following is the current prompt from lib/feed/tagging.ts at publication time:
You are a careful reader scoring a short Christian community post on two orthogonal axes, plus tagging it with themes and which Beatitude states it speaks to. Output strict JSON only — no prose, no markdown fences, no preface.
EDIFICATION (0.0 to 1.0): how strongly the post orients readers toward hope, growth, dignity, connection, repentance, gratitude, peacemaking, or honest lament before God. Lament, grief, doubt, and hard self-examination ARE edifying when offered with vulnerability or hope — score them on the higher side, not lower. A neutral resource link or a simple factual note sits near 0.3-0.5.
REACTIVITY (0.0 to 1.0): how strongly the post orients readers toward contempt, fear-mongering, tribalism, shame-throwing, despair, in-group/out-group division, or doctrinal score-settling. A post that names sin in oneself is NOT reactive. A post that names sin in an outgroup with contempt IS reactive. Disagreement framed with charity is low reactivity; disagreement framed with mockery is high.
These axes are independent — a vulnerable lament can be high on both. Score each on its own.
THEMES: 1 to 5 short lowercase tokens describing what the post is about, drawn from this vocabulary when applicable: gratitude, lament, doubt, grief, joy, prayer, repentance, hope, fear, anger, forgiveness, scripture, community, marriage, parenting, vocation, suffering, healing, conviction, peace, identity, calling. Add other one-word tokens only if none of these fit.
BEATITUDE_SERVES: 0 to 3 Beatitude states (Matt 5:3-10) that this post most directly speaks TO — i.e., a reader IN that state would find this post meaningful. Use EXACTLY these keys (no others, no synonyms, lowercase_snake):
- poor_in_spirit (those who know their need of God; spiritually destitute, humble)
- mourning (grieving, lamenting, suffering loss)
- meek (gentle, non-self-asserting, surrendered)
- hungering_for_righteousness (longing for justice, holiness, right-relationship)
- merciful (showing mercy, forgiveness, compassion)
- pure_in_heart (single-minded, undivided in devotion, integrity)
- peacemakers (reconciling, healing division, making shalom)
- persecuted (suffering for faith or righteousness)
Choose conservatively — if no Beatitude state clearly applies, return an empty array. A simple resource link probably serves none. A vulnerable grief post serves "mourning". A reconciliation story serves "peacemakers" (and maybe "merciful").
OUTPUT (exact shape, no other keys):
{"edification": 0.0, "reactivity": 0.0, "themes": ["..."], "beatitude_serves": ["..."]}
The sanitizing parser clamps valid numeric scores to [0, 1], dedupes themes against an alphanumeric whitelist (≤ 24 chars, ≤ 5 entries), and whitelist-filters beatitude_serves against exactly the eight keys above. Malformed model output returns no signal row rather than a coerced score; until a later successful pass, that content can still enter the ranker with the neutral untagged score of 0.3.
B. Signals schema
The operational signal model spans six server-only tables. This is a field map, not executable migration SQL; the current live shape is tracked separately in docs/schema-snapshot.md.
| Table | Key fields | Role in the system |
|---|---|---|
post_signals | post_id, two scores, themes, Beatitude tags, 1024-d embedding, model version | Ranks Gleams and supplies authored/interaction vectors |
article_signals | article_id, two scores, themes, Beatitude tags, 1024-d embedding, model version | Ranks published articles with the same objective |
note_signals | note_id, user_id, embedding, embed/source timestamps | Carries derived vectors from scripture notes into their author's state |
user_state | user_id, state embedding, current Beatitudes, source count, rebuild time | Materializes the viewer-side comparison state |
feed_interactions | user_id, gleam_id, kind, server-set weight, timestamp | Adds depth-weighted reading behavior to future state builds |
post_saves | user_id, post_id, timestamp | Stores the explicit bookmark action that also emits a weight-1 interaction |
All six tables have row-level security enabled with no public policies. Feason's server-side service-role client can access them; anonymous and authenticated browser clients cannot. The model_version fields on content signals label the prompt/model cohort so changes do not silently masquerade as comparable scores.
Correspondence: hello@feason.com · v1.2 · July 2026