@@ -47,9 +47,9 @@ use lance_graph_rbac::policy::{Operation, Policy};
4747
4848use crate :: super_domain:: SuperDomain ;
4949use crate :: unified_audit:: {
50- AuditChain , AuditMerkleRoot , AuthDecision , AuthOp , NoopUnifiedAuditSink , UnifiedAuditEvent ,
51- UnifiedAuditSink ,
50+ AuditChain , AuditMerkleRoot , AuthDecision , AuthOp , UnifiedAuditEvent ,
5251} ;
52+ use crate :: audit_sink:: { AuditSink , NoopAuditSink } ;
5353
5454/// Extract the canonical ontology entity type name from a resolved
5555/// [`MappingRow`], for use as the [`Policy::evaluate`] key.
@@ -249,9 +249,9 @@ pub struct UnifiedBridge<B: NamespaceBridge> {
249249 tenant : TenantId ,
250250 /// Audit sink — every `authorize_*` call that reaches the policy
251251 /// evaluation step emits one `UnifiedAuditEvent`. Default is
252- /// `NoopUnifiedAuditSink ` (zero overhead, no persistence). Swap via
252+ /// `NoopAuditSink ` (zero overhead, no persistence). Swap via
253253 /// [`Self::with_audit_chain`].
254- audit_sink : Arc < dyn UnifiedAuditSink > ,
254+ audit_sink : Arc < dyn AuditSink > ,
255255 /// Merkle-chained audit advancer. Holds the prior event's root +
256256 /// per-super-domain salt so each new event chains off it. Mutex
257257 /// guards the `last_root` advance under concurrent `authorize_*`
@@ -262,7 +262,7 @@ pub struct UnifiedBridge<B: NamespaceBridge> {
262262impl < B : NamespaceBridge > UnifiedBridge < B > {
263263 /// Construct a new unified bridge.
264264 ///
265- /// Defaults audit to `NoopUnifiedAuditSink ` + a chain anchored at
265+ /// Defaults audit to `NoopAuditSink ` + a chain anchored at
266266 /// `SuperDomain::Unknown` with salt 0. Call
267267 /// [`Self::with_audit_chain`] to swap in a real sink + the
268268 /// super-domain-specific salt before authorization traffic starts.
@@ -278,20 +278,20 @@ impl<B: NamespaceBridge> UnifiedBridge<B> {
278278 actor_role,
279279 actor_role_hash : fnv1a_str ( actor_role) ,
280280 tenant,
281- audit_sink : Arc :: new ( NoopUnifiedAuditSink ) ,
281+ audit_sink : Arc :: new ( NoopAuditSink ) ,
282282 audit_chain : Mutex :: new ( AuditChain :: new ( SuperDomain :: Unknown , 0 ) ) ,
283283 }
284284 }
285285
286- /// Builder: swap in a real `UnifiedAuditSink ` + the super-domain's
286+ /// Builder: swap in a real `AuditSink ` + the super-domain's
287287 /// `merkle_salt` (§13.4 — cross-domain audit logs unlinkable). Resets
288288 /// the chain to GENESIS; pass a resume root via
289289 /// [`Self::with_audit_chain_resume`] if continuing a persisted chain.
290290 pub fn with_audit_chain (
291291 mut self ,
292292 super_domain : SuperDomain ,
293293 salt : u64 ,
294- sink : Arc < dyn UnifiedAuditSink > ,
294+ sink : Arc < dyn AuditSink > ,
295295 ) -> Self {
296296 self . audit_sink = sink;
297297 self . audit_chain = Mutex :: new ( AuditChain :: new ( super_domain, salt) ) ;
@@ -306,13 +306,30 @@ impl<B: NamespaceBridge> UnifiedBridge<B> {
306306 super_domain : SuperDomain ,
307307 salt : u64 ,
308308 last_root : AuditMerkleRoot ,
309- sink : Arc < dyn UnifiedAuditSink > ,
309+ sink : Arc < dyn AuditSink > ,
310310 ) -> Self {
311311 self . audit_sink = sink;
312312 self . audit_chain = Mutex :: new ( AuditChain :: resume ( super_domain, salt, last_root) ) ;
313313 self
314314 }
315315
316+ /// Ergonomic constructor: wire a `JsonlAuditSink` at `base_path` as
317+ /// the primary audit destination. Per OQ-7-3 (locked 2026-05-13):
318+ /// `new()` defaults to `NoopAuditSink`; this constructor is the
319+ /// explicit opt-in for the production "JSONL primary + optional Lance
320+ /// projection" pattern (MedCare-rs sprint-2 item 5). Only available
321+ /// when the `jsonl` feature is enabled.
322+ #[ cfg( feature = "jsonl" ) ]
323+ pub fn with_jsonl_audit (
324+ self ,
325+ super_domain : SuperDomain ,
326+ salt : u64 ,
327+ base_path : impl Into < std:: path:: PathBuf > ,
328+ ) -> std:: io:: Result < Self > {
329+ let sink = Arc :: new ( crate :: audit_sink:: JsonlAuditSink :: new ( base_path. into ( ) ) ?) ;
330+ Ok ( self . with_audit_chain ( super_domain, salt, sink) )
331+ }
332+
316333 /// Returns the underlying namespace bridge.
317334 pub fn bridge ( & self ) -> & B {
318335 & self . bridge
@@ -350,7 +367,7 @@ impl<B: NamespaceBridge> UnifiedBridge<B> {
350367 /// from policy authorship.
351368 ///
352369 /// On policy evaluation reaching, one `UnifiedAuditEvent` is emitted
353- /// through the configured `UnifiedAuditSink ` carrying tenant +
370+ /// through the configured `AuditSink ` carrying tenant +
354371 /// super-domain + owl + decision. **`BridgeError` short-circuits
355372 /// before audit** — bad input names aren't auth decisions, they're
356373 /// invalid requests (D-SDR-5 minimum; revisit if probing detection
@@ -428,7 +445,10 @@ impl<B: NamespaceBridge> UnifiedBridge<B> {
428445 } ;
429446 let stamped = chain. advance ( event) ;
430447 drop ( chain) ;
431- self . audit_sink . emit ( & stamped) ;
448+ // Best-effort: audit emission failures must not block the authorize
449+ // hot path. Sinks are responsible for their own buffering/backpressure
450+ // (see audit_sink::{JsonlAuditSink, LanceAuditSink} BestEffort mode).
451+ let _ = self . audit_sink . emit ( stamped) ;
432452 }
433453}
434454
@@ -821,9 +841,16 @@ mod tests {
821841 }
822842 }
823843
824- impl UnifiedAuditSink for RecordingSink {
825- fn emit ( & self , event : & UnifiedAuditEvent ) {
826- self . events . lock ( ) . unwrap ( ) . push ( * event) ;
844+ impl AuditSink for RecordingSink {
845+ fn emit ( & self , event : UnifiedAuditEvent ) -> Result < ( ) , crate :: audit_sink:: AuditError > {
846+ self . events . lock ( ) . unwrap ( ) . push ( event) ;
847+ Ok ( ( ) )
848+ }
849+ fn flush ( & self ) -> Result < crate :: audit_sink:: MerkleRoot , crate :: audit_sink:: AuditError > {
850+ Ok ( 0 )
851+ }
852+ fn checkpoint ( & self ) -> Result < ( ) , crate :: audit_sink:: AuditError > {
853+ Ok ( ( ) )
827854 }
828855 }
829856
0 commit comments