A design spike two weeks ago named the gap plainly: neither of Smeldr's two existing "who did this" mechanisms can actually answer that question everywhere it matters. AuditRecord only fires on 4 of 8 lifecycle events, and it's keyed by content type and slug, a RelationEdge has neither. RelationEdge.CreatedByJob records *a* job identity, but only for job-originated edges, and never a human one. Neither has a Reason field. ProvenanceRecord closes both gaps at once.
What it is
app.Provenance(smeldr.NewProvenanceStore(db))
One call, mirroring App.Audit's own shape. From then on, every completed lifecycle transition on every Node-subject, create, update, publish, unpublish, schedule, archive, delete, and every asserted RelationEdge, writes one ProvenanceRecord: who (actor kind + ID), what (verb, from-state, to-state), and, when supplied, why.
A current limitation, worth stating plainly
ActorKind declares "human", "job", and "agent" as possible values, but actorKindFor only ever returns "human" or an empty string today. The system cannot yet tell a human actor from an AI agent behind the same action, even though it can already say, precisely, which authenticated identity performed it.
The thing we didn't do
The original design doc's Decision 1 said ProvenanceRecord *replaces* AuditRecord. Re-checked against CHANGELOG.md before writing a line of code: Smeldr's own v1.0.0 API stability promise covers AuditRecord/AuditStore/App.Audit/GET /_audit, they've been exported since A97. Replacing them would be exactly the kind of breaking change the promise exists to prevent. So ProvenanceRecord ships purely additive: a new table, a new store, zero changes to the audit path. The four events both mechanisms cover now get logged twice, once to each table, deliberate redundancy, not two competing truths, since both writes trace back to the same signal dispatch with the same data. They can't disagree.
Recovering the actor without touching a signature
RelationStore.Assert (and MCPAssertRelation, MCPProposeRelation) take a plain context.Context, no actor-identity accessor. The design doc's own open question offered two paths: add actorID/actorKind parameters (breaking, needs a coordinated smeldr.dev/mcp bump), or ship parallel AssertWithActor-style methods on a deprecation timeline. Neither shipped.
smeldr.Context embeds context.Context directly. Any caller that hands a smeldr.Context value into a context.Context-typed parameter is still, underneath, that same concrete value, recoverable with a type assertion:
if sc, ok := ctx.(Context); ok {
actorID = sc.User().ID
}
relations.go already had exactly this pattern for one other purpose (detecting whether the injected DB supports transactions); dynamic.go's SetStatus does the same thing through a small local interface. Reusing an established pattern meant zero signature changes anywhere, and zero coordination with the separate smeldr.dev/mcp repo.
RequiredReason
smeldr.Transition{From: "ratified", To: "superseded", RequiredReason: true}
Fail-closed, same enforcement layer as RequiredRole, same function (validateTransition), a transition with the flag set rejects with ErrBadRequest when the caller supplies no reason. The one concrete path that can satisfy it today is the new DynamicTypeRepo.SetStatusWithReason; SetStatus itself is untouched. Nothing served over MCP can supply a reason yet, transition_item still calls plain SetStatus, so don't reach for RequiredReason: true on a flow you're driving through MCP until that catches up.