Why the Third and Not the Second
Two implementations of something give you a sample of two, and a sample of two is not enough to tell which parts are essential and which are incidental. Abstract from it and you will lift one app's incidental choices into the shared layer as though they were the shape of the problem, and then spend the next year adding parameters to undo that.
The third copy is where the pattern separates from its instances. What all three do is the mechanism; what only one does is that app's business. That is the working heuristic in this family, and most promotions follow it.
Not all, and the exceptions are recorded rather than improvised. Several modules were promoted at their second use, each with a measurement attached — the two copies were byte-identical once the app names were substituted, which is evidence about the shape rather than a hope about it. A second copy that is provably identical has already told you what the third would.
The Exception, and What Makes It One
One surface was promoted before a second consumer existed: a wrapper that frames externally-authored text as untrusted evidence before it reaches a model. It does not sanitize or summarize anything; it only marks a boundary, saying explicitly that the quoted material is evidence and not instructions.
The argument for moving early was written down at the time. Every sibling that hands a model text somebody else wrote has this surface. Establishing "untrusted text passes through the wrapper" as a shared rule once is cheaper than each repository rediscovering it — and the discovery is not a duplicated file, it is an incident. The owning application had already found out the expensive way: a model-emitted identifier trusted without checking it against the batch the model had actually been shown, on a path that publishes to a public site.
Promotion Is Not Only Extraction
Worth noting because it is easy to under-scope: promoting a pattern means more than moving a file. It means naming the seams the other consumers will need (which usually only becomes visible once a second one adopts), writing down where it is expected to be adopted and where it is deliberately not, and — for a surface adopted per-use-site rather than per-app — accepting that the ledger will show intentions alongside adoptions for a while.
That last part is honest rather than sloppy. The safety wrapper is marked as adopted in exactly one place with call sites, and pending everywhere else, because a repository receives it when it actually feeds a model somebody else's text — not in anticipation. Vendoring it everywhere immediately would have produced a table full of checkmarks and no additional safety.