The assumed learning signal for a content system is engagement. Publish, measure, do more of what worked.
At one person's volume that loop never closes. Detecting a twenty per cent lift with any confidence needs on the order of six hundred and twenty posts, roughly three years of continuous publishing. A system claiming to learn per-user preferences from engagement inside a month is fitting noise.
There is a better signal sitting unused in the approval queue.
1. Why edits beat engagement
An edit says this exact word was wrong and here is what it should have been. There is no attribution problem, because there is only one cause.
2. The signal table
One row per real approve, edit or reject decision the user makes anywhere in the product.
user_behavior_signals
workspaceId sourceId action
intentType category workflowType
contentPurpose contentArchetype
lengthBefore lengthAfter
hashtagsBefore hashtagsAfter
rejectionReasonTwo things about this table are deliberate.
The source identifier is not a foreign key to any one entity. It is the id of whatever produced the signal. Today that is a task. A future approval loop outside the task model feeds the same table with its own recorder call, not a new table.
And writing a row costs nothing. No model call. Character counts, hashtag counts and a word diff are all arithmetic. The qualitative learning about why something changed runs separately and asynchronously into living memory. This table is the countable counterpart, and its cheapness is why it can capture every decision rather than a sample.
3. Sorting an edit
Deterministic. Paying for a model call to answer whether hashtags were removed would be waste.
4. From edits to proposals
A window of observations produces proposals. Below the repetition threshold it produces nothing.
Three repetitions is the threshold. Enough to rule out a one-off, few enough to learn inside a week or two of ordinary use.
Confidence rises with support and is capped.
below three observations confidence is zero
otherwise 0.5 + 0.4 x (support - 3) / 3, capped at 0.9At three observations a proposal starts at 0.5, meaning suggestive. At six it reaches 0.9 and stops. It never reaches one, because an inference about a person is never certain.
5. A proposal is a question
Every proposal carries a sentence written to be read rather than parsed.
You've removed "leverage" from 4 drafts and never added it. Should I stop using it?
You've cut hashtags in 3 drafts, usually down to 1. Should that be the default?
You've rewritten the opening on 7 drafts. Tell me what you'd rather I open with and I'll stop guessing.
The last one does not propose a value. It admits the system keeps getting something wrong and asks for input, which is often the most useful thing a personalisation system can do.
6. The ordering
The design in one line:
an explicit preference beats a repeated-edit proposal beats measurementThe safety property that matters: one accidental edit must never become a permanent rule. A proposal needs repetition to appear, and it needs the user to accept it before anything is written. Accepting is only ever called by a user action.
One detail in accepting is worth showing, because getting it wrong is silent.
if (proposal.path === 'lexicon.banned') {
const { card } = await this.style.getOrEmpty(workspaceId);
const next = [...new Set([...card.lexicon.banned, String(proposal.value)])];
await this.style.applyUserEdit(workspaceId, 'lexicon.banned', next);
return;
}The banned list is a list, so accepting has to append. A plain write would mean accepting a second banned word quietly dropped the first, which the user would experience as the system forgetting.
7. Rejections carry a reason
Rejection is coarser than an edit and still useful, provided the reason is structured. Six options, one of which is other.
| Reason | What it indicates |
|---|---|
| too generic | A specificity failure, usually a missing input |
| wrong tone | The voice model is off |
| factual errors | A provenance failure, not a style one |
| wrong format | The wrong shape for the platform |
| not what I asked for | The intent was misread |
| other |
These aggregate into the behaviour profile under the same floor as everything else. Below three signals no ranking is displayed, because a bar chart built on two data points is not a finding.
8. What we deliberately did not build
No fine tuning. Edits are strong enough to justify a training loop and there is nowhere near enough volume per person to run one. The proposals write into a specification instead.
No automatic application. Every proposal waits for consent. The cost is slower learning. The benefit is that no user is ever surprised by a preference they did not set.
No second preference store. Everything lands in the style card. A separate table of learned preferences would drift from it and become a second source of truth about the same person.
9. Where this is weak
Word diffs are lexical. Someone rewriting a sentence entirely produces a large removal set with no repeated word, and generates no proposal at all, despite having made the strongest correction available.
The observation window is not weighted by time. A preference held three months ago and since changed still counts toward support.
Proposals are per person, not per context. Someone who wants no emoji in one place and occasional emoji in another sees their edits combine into one contradictory signal.
And the conversion from characters to words divides by 5.5, which is an approximation and is language dependent.