|
| 1 | +# Meta-3 Review — MedCareMembraneGate (Round 3, Stage 3) |
| 2 | + |
| 3 | +**Reviewer:** Meta agent 3 of 3 (final review pass) |
| 4 | +**Scope:** medcare-rs/crates/medcare-realtime/src/gate.rs + tests/{integration,regulatory}.rs |
| 5 | +**Method:** read W9-W12 commits + log entries; cross-check against |
| 6 | +PR #29 reference impl + Meta-1/Meta-2 carry-forwards + v1 boundary |
| 7 | +honesty. |
| 8 | + |
| 9 | +> **Tone:** brutally honest. This is the last review pass before sprint |
| 10 | +> closure. Findings either block ship or document v1 limits explicitly |
| 11 | +> for future sessions. No filler. |
| 12 | +
|
| 13 | +--- |
| 14 | + |
| 15 | +## Verdict |
| 16 | + |
| 17 | +**Ship Round 3 + close sprint.** Zero CRITICAL findings. Two HIGH |
| 18 | +findings are honest documentation gaps that the W12 author has |
| 19 | +already partially captured; meta surfaces them more sharply. Two |
| 20 | +MEDIUM findings deferred to follow-up sprints. The v1 surface is |
| 21 | +the right shape; it's just smaller than ambition. |
| 22 | + |
| 23 | +| # | Severity | Finding | Action | |
| 24 | +|---|---|---|---| |
| 25 | +| 1 | HIGH | Action operations (Operation::Act) unreachable via gate — gate routes only Read/Write | Doc note in gate.rs module head + tests/regulatory.rs; orchestration layer is the right home for action gating | |
| 26 | +| 2 | HIGH | BtM Escalate "v1 limitation documented" tests will silently pass even if the gate IS later updated to return Escalate — the assertion is `is_allowed()` | Tighten future-spec assertions: explicit `assert_eq!(decision, AccessDecision::Allow)` so future Escalate flip is a real test failure | |
| 27 | +| 3 | MEDIUM | Three name paths for `Policy` (rbac, gate, lib) | Choose one canonical; document the others as legacy aliases | |
| 28 | +| 4 | MEDIUM | No bench harness validates 20-200 ns gate decision claim | Add to backlog; gate-bench follow-up sprint | |
| 29 | +| 5 | LOW | Module-head TD-MEMBRANE-FIRST-VS-ANY caveat carries forward but no test exercises the divergence case (predicate-specific RLS) | Backlog: write a unit test once a real divergence case is identified | |
| 30 | + |
| 31 | +--- |
| 32 | + |
| 33 | +## HIGH #1 — Action operations unreachable via gate |
| 34 | + |
| 35 | +**Finding.** `MedCareMembraneGate::should_emit` and `evaluate` route |
| 36 | +`gate_commit: bool` to `Operation::Read { depth }` (false) or |
| 37 | +`Operation::Write { predicate }` (true). Neither path reaches |
| 38 | +`Operation::Act { action }`. |
| 39 | + |
| 40 | +This means actions like: |
| 41 | +- `Diagnosis.classify` / `finalize` / `retract` |
| 42 | +- `Prescription.issue` / `renew` / `revoke` |
| 43 | +- `Anamnese.append` (the ONLY mutation path for Anamnese per BMV-Ä §57) |
| 44 | +- `Ueberweisung.send` / `accept` / `decline` |
| 45 | +- `Patient.merge` / `anonymize` / `delete` |
| 46 | + |
| 47 | +...cannot be gated through `MedCareMembraneGate`. The orchestration |
| 48 | +layer must call `medcare_rbac::Policy::evaluate(role, entity, |
| 49 | +Operation::Act { action })` directly. |
| 50 | + |
| 51 | +**This is intentional from PR #29's design** — the upstream |
| 52 | +`MembraneGate` trait shape is `(commit: bool)` only. But it's a |
| 53 | +substantial v1 limit that medcare's append-only Anamnese semantic |
| 54 | +relies on. |
| 55 | + |
| 56 | +**Super-helpful solution.** Add an explicit doc note to gate.rs |
| 57 | +module head: |
| 58 | + |
| 59 | +```rust |
| 60 | +//! # Action operations not gated here |
| 61 | +//! |
| 62 | +//! `MedCareMembraneGate` routes only Read/Write per upstream's |
| 63 | +//! `(gate_commit: bool)` shape. Action operations (`Diagnosis.classify`, |
| 64 | +//! `Prescription.issue`, `Anamnese.append`, etc.) must go through |
| 65 | +//! `medcare_rbac::Policy::evaluate(role, entity, Operation::Act |
| 66 | +//! { action })` at the orchestration layer. |
| 67 | +//! |
| 68 | +//! This is by upstream design — `MembraneGate::should_emit` is the |
| 69 | +//! wire-shape of "is this projection allowed to leave the membrane", |
| 70 | +//! which is a Read/Write question. Action authorization (issue this |
| 71 | +//! prescription, finalize this diagnosis) is an orchestration-layer |
| 72 | +//! concern that fires before the eventual Read/Write projection. |
| 73 | +``` |
| 74 | + |
| 75 | +**Action.** Update gate.rs doc — small follow-up commit. Or carry |
| 76 | +this into the sprint summary as documented v1 limit. |
| 77 | + |
| 78 | +--- |
| 79 | + |
| 80 | +## HIGH #2 — BtM Escalate "limitation documented" tests are too weak |
| 81 | + |
| 82 | +**Finding.** W12's regulatory tests for the BtM/finalize/anonymize |
| 83 | +limitation: |
| 84 | + |
| 85 | +```rust |
| 86 | +#[test] |
| 87 | +fn btm_escalate_path_documented_as_v1_limitation() { |
| 88 | + let gate = MedCareMembraneGate::from_medcare_policy("doctor", "Prescription"); |
| 89 | + let decision = gate.evaluate(true); |
| 90 | + assert!(decision.is_allowed()); // ← too loose |
| 91 | +} |
| 92 | +``` |
| 93 | + |
| 94 | +`is_allowed()` returns true only for `AccessDecision::Allow`. If a |
| 95 | +future commit lands the Escalate wrapping (the documented future |
| 96 | +behavior), `decision` becomes `Escalate { reason: "..." }`, which is |
| 97 | +NOT `is_allowed()`, so the test FAILS. That's the desired flip — the |
| 98 | +test fails until the spec is updated. |
| 99 | + |
| 100 | +But the failure message will be cryptic: "expected true, got false". |
| 101 | +The reader has to figure out whether is_allowed false means Deny, |
| 102 | +Escalate, or some other variant. |
| 103 | + |
| 104 | +**Super-helpful solution.** Tighten the assertion: |
| 105 | + |
| 106 | +```rust |
| 107 | +#[test] |
| 108 | +fn btm_escalate_path_documented_as_v1_limitation() { |
| 109 | + let gate = MedCareMembraneGate::from_medcare_policy("doctor", "Prescription"); |
| 110 | + let decision = gate.evaluate(true); |
| 111 | + // v1: Allow uniformly (gate doesn't see btm_flag). |
| 112 | + // FUTURE: when row-context lands, this should become |
| 113 | + // AccessDecision::Escalate { reason: "BtM second signature required" } |
| 114 | + // for btm_flag=true rows. This explicit assert_eq! makes the future |
| 115 | + // flip a clear test failure: "expected Allow, got Escalate { ... }" |
| 116 | + // is much more readable than "expected true, got false". |
| 117 | + assert_eq!(decision, AccessDecision::Allow); |
| 118 | +} |
| 119 | +``` |
| 120 | + |
| 121 | +This applies to all three v1-limitation tests (BtM, finalize/retract, |
| 122 | +anonymize). Same pattern. |
| 123 | + |
| 124 | +**Action.** Optional W12-revision-2 to tighten three assertions. Not |
| 125 | +blocking — the loose assertions still flip when future changes land. |
| 126 | +But the failure message clarity matters for someone diagnosing a CI |
| 127 | +break six months from now. |
| 128 | + |
| 129 | +--- |
| 130 | + |
| 131 | +## MEDIUM #3 — Three name paths for `Policy` |
| 132 | + |
| 133 | +**Finding.** Same `Policy` type reachable via: |
| 134 | +- `medcare_rbac::policy::Policy` (canonical home) |
| 135 | +- `medcare_realtime::gate::Policy` (re-exported via `pub use`) |
| 136 | +- `medcare_realtime::Policy` (lib.rs crate-root re-export) |
| 137 | + |
| 138 | +Compilation-equivalent. Cognition-confusing. Future "import Policy" |
| 139 | +may grab any of three depending on which path the IDE auto-suggests. |
| 140 | + |
| 141 | +**Super-helpful solution.** Pick one canonical path; document others |
| 142 | +as legacy aliases. Recommended canonical: `medcare_realtime::Policy` |
| 143 | +(crate-root) — same as smb-realtime's pattern. Other two paths stay |
| 144 | +for backward-compat but aren't documented as primary. |
| 145 | + |
| 146 | +**Action.** Backlog. Doc-only update; no behavior change. |
| 147 | + |
| 148 | +--- |
| 149 | + |
| 150 | +## MEDIUM #4 — No bench harness for 20-200 ns claim |
| 151 | + |
| 152 | +**Finding.** Gate doc claims "decisions run at L1 inner speed |
| 153 | +(~20-200 ns)". v1 has zero benchmarks validating this. |
| 154 | + |
| 155 | +**Super-helpful solution.** Backlog item: `gate-bench-v1` follow-up |
| 156 | +adding `criterion`-based microbenchmarks for: |
| 157 | +- `should_emit(_, _, _, true)` — allow path |
| 158 | +- `should_emit(_, _, _, true)` — deny path (unknown role) |
| 159 | +- `should_emit(_, _, _, false)` — read path |
| 160 | +- `evaluate(true)` (Escalate path when future row-context lands) |
| 161 | + |
| 162 | +Targets: <500 ns p99 for current sync impl. If we can't hit that, the |
| 163 | +"20-200 ns" claim in the topology doc needs revision. |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## LOW #5 — TD-MEMBRANE-FIRST-VS-ANY untested |
| 168 | + |
| 169 | +**Finding.** PR #29 caveat #3 says `writable_predicates.first()` may |
| 170 | +deny when "any" should allow IF predicate-specific RLS conditions |
| 171 | +exist. medcare-realtime carries the caveat forward in module-head |
| 172 | +docs but writes no test. |
| 173 | + |
| 174 | +**Super-helpful solution.** Backlog: when a real divergence case is |
| 175 | +identified (e.g. a Patient predicate where `Operation::Write { predicate }` |
| 176 | +has different RLS conditions per predicate), write a regression test. |
| 177 | +v1 doesn't have such a case, so the test would be vacuous. |
| 178 | + |
| 179 | +--- |
| 180 | + |
| 181 | +## Sprint-wide closure assessment |
| 182 | + |
| 183 | +**Round 1 (medcare-rbac):** Solid. 26 tests, 2 CRITICAL fixes applied |
| 184 | +in revision-2. Surface mirrors lance-graph-rbac with medcare entities. |
| 185 | + |
| 186 | +**Round 2 (medcare-realtime skeleton):** Solid. 5 tests, 1 CRITICAL |
| 187 | +casing fix + HIPAA-grade values applied in W7-revision-2. Workspace |
| 188 | +registration clean. |
| 189 | + |
| 190 | +**Round 3 (MedCareMembraneGate):** Solid. 33 tests across gate.rs + |
| 191 | +integration.rs + regulatory.rs. Two HIGH gaps (action ops unreachable, |
| 192 | +v1-limit assertions loose) are honest documentation issues, not |
| 193 | +correctness blockers. |
| 194 | + |
| 195 | +**Total tests:** 64 across all three crates (medcare-rbac 26 + |
| 196 | +medcare-realtime 38). |
| 197 | + |
| 198 | +**Compilation expectation:** medcare-rs root `cargo build` should |
| 199 | +work assuming: |
| 200 | +1. lance-graph submodule symlink is functional (vendor/lance-graph) |
| 201 | +2. lance-graph workspace builds (lance-graph-contract + |
| 202 | + lance-graph-callcenter + medcare-rbac symbol resolution) |
| 203 | +3. `StepDomain::Medcare` profile values match upstream (verified |
| 204 | + in W7-rev2) |
| 205 | + |
| 206 | +If any of (1)/(2) fails on a fresh checkout, that's a vendor/submodule |
| 207 | +hygiene issue — not a sprint deliverable issue. |
| 208 | + |
| 209 | +**Recommended follow-up sprint scope (smaller than this one):** |
| 210 | +1. Apply HIGH #1 doc note (5 min) |
| 211 | +2. Apply HIGH #2 assertion tighten (10 min) |
| 212 | +3. Bench harness for gate decisions (HIGH-MEDIUM, ~2 hours) |
| 213 | +4. Action-operation orchestration layer wrapper (HIGH-HIGH, half day) |
| 214 | + |
| 215 | +--- |
| 216 | + |
| 217 | +## Verdict reaffirmed |
| 218 | + |
| 219 | +**Ship.** v1 surface is correct and honest about its limits. Two HIGH |
| 220 | +findings are documentation/test-clarity issues, not correctness |
| 221 | +issues. Three MEDIUM/LOW findings go to backlog. |
| 222 | + |
| 223 | +POLICY-1 / MEMBRANE-GATE-1 medcare-side seam is **CLOSED** for v1. |
| 224 | +Topology I-1 / I-2 / I-3 / I-4 invariants are upheld. PR #29's three |
| 225 | +TD caveats are honestly carried forward to the medcare side. |
0 commit comments