forked from lance-format/lance-graph
-
Notifications
You must be signed in to change notification settings - Fork 0
MedcareBridge = UnifiedBridge<HealthcarePort> + FieldMask role projection + Fisher-z clamp fix #582
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
+281
−52
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
ddb6c84
refactor(bridges): MedcareBridge = UnifiedBridge<HealthcarePort>
claude e1012ae
fix(contract): loosen Fisher-z clamp ±0.999 → ±0.9999 so self-match r…
claude 8b35dc0
feat(rbac): role carries a FieldMask projection — distinct views, not…
claude 4417a6f
Merge remote-tracking branch 'origin/main' into claude/medcare-bridge…
claude File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
192 changes: 157 additions & 35 deletions
192
crates/lance-graph-ontology/src/bridges/medcare_bridge.rs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,44 +1,166 @@ | ||
| //! MedCare (healthcare) tenant bridge — locks to the `Healthcare` | ||
| //! namespace. The Healthcare namespace itself is reserved and will be | ||
| //! populated by a future session (the FMA / SNOMED / RadLex import is the | ||
| //! remit of `lance-graph-rdf` in `lance-graph-rdf-fma-snomed-v1`). | ||
|
|
||
| use crate::bridge::{BridgeFromRegistry, NamespaceBridge}; | ||
| use crate::error::{Error, Result}; | ||
| use crate::namespace::NamespaceId; | ||
| use crate::registry::OntologyRegistry; | ||
| use std::sync::Arc; | ||
|
|
||
| pub const NAMESPACE: &str = "Healthcare"; | ||
|
|
||
| pub struct MedcareBridge { | ||
| registry: Arc<OntologyRegistry>, | ||
| g_lock: NamespaceId, | ||
| } | ||
| //! MedCare (healthcare) tenant bridge — now a thin type alias over | ||
| //! [`crate::bridges::unified::UnifiedBridge`] parameterised by | ||
| //! [`ogar_vocab::ports::HealthcarePort`]. | ||
| //! | ||
| //! Before the Healthcare codebook promotion (Northstar T9) this file | ||
| //! carried a bespoke `MedcareBridge` struct + hand-written | ||
| //! `NamespaceBridge` impl — the same boilerplate every per-tenant bridge | ||
| //! cloned. lance-graph#570 collapsed `OpenProjectBridge` / `RedmineBridge` | ||
| //! onto the generic [`UnifiedBridge<P>`] harness but explicitly deferred | ||
| //! `MedcareBridge` "until Healthcare gets promoted into the codebook". | ||
| //! That promotion has now landed: OGAR mints the `0x09XX` Health concepts | ||
| //! and `ports::HealthcarePort` carries the namespace / bridge_id / public- | ||
| //! name → class_id alias table, so this bridge becomes one line. | ||
| //! | ||
| //! The differences between bridges (namespace, bridge_id, alias table) | ||
| //! all come from the OGAR class schema. Codebook public names (`Patient`, | ||
| //! `Diagnosis`, …) synthesize an `EntityRef` whose `entity_type_id()` is | ||
| //! the canonical Health class_id; names outside the alias table fall | ||
| //! through to the registry-resolution path (so a hydrated TTL entity that | ||
| //! is not yet a codebook concept still resolves). The audit / authorization | ||
| //! path uses `row()` (registry-backed, not overridden here), so it is | ||
| //! unaffected by codebook synthesis on `entity()`. | ||
| //! | ||
| //! See `crate::bridges::unified::tests` for the generic-level coverage and | ||
| //! `tests/bridge_scope_lock.rs` for the Healthcare scope-lock pins. | ||
|
|
||
| use crate::bridges::unified::UnifiedBridge; | ||
| // `HealthcarePort::NAMESPACE` / `::aliases()` are `PortSpec` associated | ||
| // items — the trait must be in scope for the resolution to work (codex | ||
| // P1 on PR #570). Same import in the test module below. | ||
| use ogar_vocab::ports::PortSpec; | ||
| pub use ogar_vocab::ports::HealthcarePort; | ||
|
|
||
| /// MedCare `NamespaceBridge` — alias over the generic harness, locked to | ||
| /// the `Healthcare` namespace via [`HealthcarePort`]. | ||
| pub type MedcareBridge = UnifiedBridge<HealthcarePort>; | ||
|
|
||
| /// Canonical namespace name for MedCare / Healthcare. Mirrors | ||
| /// `HealthcarePort::NAMESPACE` so existing consumers that imported the | ||
| /// constant from this module keep building. | ||
| pub const NAMESPACE: &str = HealthcarePort::NAMESPACE; | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| //! Co-located unit tests for the migrated alias — constructor | ||
| //! success/failure, contract methods, codebook resolution, fallback | ||
| //! to registry. Mirrors `openproject_bridge::tests`; only the | ||
| //! `MedcareBridge` ident (now `UnifiedBridge<HealthcarePort>`) and the | ||
| //! Healthcare fixtures differ. | ||
|
|
||
| impl MedcareBridge { | ||
| pub fn new(registry: Arc<OntologyRegistry>) -> Result<Self> { | ||
| let g_lock = registry | ||
| .namespace_id(NAMESPACE) | ||
| .ok_or_else(|| Error::UnknownNamespace(NAMESPACE.to_string()))?; | ||
| Ok(Self { registry, g_lock }) | ||
| use super::*; | ||
| use crate::bridge::{BridgeError, NamespaceBridge}; | ||
| use crate::error::Error; | ||
| use crate::namespace::NamespaceId; | ||
| use crate::namespace_registry::NamespaceRegistry; | ||
| use crate::registry::OntologyRegistry; | ||
| use ogar_vocab::class_ids; | ||
| // PortSpec needed in scope for `HealthcarePort::aliases()`. | ||
| use ogar_vocab::ports::PortSpec; | ||
| use std::fs; | ||
| use std::sync::Arc; | ||
|
|
||
| fn registry_with_healthcare() -> Arc<OntologyRegistry> { | ||
| let ttl = r#" | ||
| @prefix ogit: <http://www.purl.org/ogit/> . | ||
| @prefix ogit.Healthcare: <http://www.purl.org/ogit/Healthcare/> . | ||
| @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . | ||
|
|
||
| ogit.Healthcare:Patient | ||
| a rdfs:Class; | ||
| rdfs:subClassOf ogit:Entity; | ||
| rdfs:label "Patient"; | ||
| ogit:scope "NTO"; | ||
| ogit:parent ogit:Node; | ||
| ogit:mandatory-attributes ( ogit:id ); | ||
| ogit:optional-attributes ( ) ; | ||
| . | ||
| "#; | ||
| let tmp = tempfile::tempdir().unwrap(); | ||
| fs::create_dir_all(tmp.path().join("Healthcare")).unwrap(); | ||
| fs::write(tmp.path().join("Healthcare").join("ents.ttl"), ttl).unwrap(); | ||
| let registry = Arc::new(OntologyRegistry::new_in_memory()); | ||
| registry | ||
| .hydrate_once_sync(tmp.path(), &["Healthcare"]) | ||
| .unwrap(); | ||
| std::mem::forget(tmp); | ||
| registry | ||
| } | ||
|
AdaWorldAPI marked this conversation as resolved.
|
||
| } | ||
|
|
||
| impl NamespaceBridge for MedcareBridge { | ||
| fn bridge_id(&self) -> &'static str { | ||
| "medcare" | ||
| #[test] | ||
| fn new_succeeds_when_namespace_registered() { | ||
| let registry = registry_with_healthcare(); | ||
| let bridge = MedcareBridge::new(registry).unwrap(); | ||
| assert_ne!(bridge.g_lock(), NamespaceId::UNKNOWN); | ||
| } | ||
| fn registry(&self) -> &OntologyRegistry { | ||
| &self.registry | ||
|
|
||
| #[test] | ||
| fn new_returns_unknown_namespace_when_not_registered() { | ||
| let registry = Arc::new(OntologyRegistry::new_in_memory()); | ||
| // `unwrap_err()` would require `MedcareBridge: Debug`, which the | ||
| // underlying `UnifiedBridge<P>` intentionally doesn't impl. | ||
| match MedcareBridge::new(registry) { | ||
| Ok(_) => panic!("expected UnknownNamespace, got Ok(_)"), | ||
| Err(Error::UnknownNamespace(name)) => assert_eq!(name, "Healthcare"), | ||
| Err(other) => panic!("expected UnknownNamespace, got {other:?}"), | ||
| } | ||
| } | ||
| fn g_lock(&self) -> NamespaceId { | ||
| self.g_lock | ||
|
|
||
| #[test] | ||
| fn bridge_id_is_lowercase_medcare() { | ||
| let bridge = MedcareBridge::new(registry_with_healthcare()).unwrap(); | ||
| assert_eq!(bridge.bridge_id(), "medcare"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn g_lock_matches_registry_namespace_id() { | ||
| let registry = registry_with_healthcare(); | ||
| let expected = registry.namespace_id(NAMESPACE).unwrap(); | ||
| let bridge = MedcareBridge::new(registry).unwrap(); | ||
| assert_eq!(bridge.g_lock(), expected); | ||
| } | ||
|
|
||
| #[test] | ||
| fn entity_resolves_patient_to_canonical_class_id() { | ||
| let bridge = MedcareBridge::new(registry_with_healthcare()).unwrap(); | ||
| let entity = bridge.entity("Patient").unwrap(); | ||
| assert_eq!(entity.schema_ptr.entity_type_id(), class_ids::PATIENT); | ||
| assert_eq!(entity.schema_ptr.entity_type_id(), 0x0901); | ||
| } | ||
|
|
||
| #[test] | ||
| fn entity_synthesised_schema_ptr_stamps_seeded_context_id() { | ||
| let bridge = MedcareBridge::new(registry_with_healthcare()).unwrap(); | ||
| let entity = bridge.entity("Patient").unwrap(); | ||
| let expected = NamespaceRegistry::seed_context_id("Healthcare").unwrap(); | ||
| assert_eq!(entity.schema_ptr.ontology_context_id(), expected); | ||
| assert_eq!(entity.schema_ptr.ontology_context_id(), 2); | ||
| } | ||
|
|
||
| #[test] | ||
| fn entity_for_each_codebook_entry_returns_its_canonical_class_id() { | ||
| let bridge = MedcareBridge::new(registry_with_healthcare()).unwrap(); | ||
| for &(public_name, expected_id) in HealthcarePort::aliases() { | ||
| let entity = bridge.entity(public_name).unwrap_or_else(|e| { | ||
| panic!("codebook entry `{public_name}` failed to resolve: {e:?}") | ||
| }); | ||
| assert_eq!( | ||
| entity.schema_ptr.entity_type_id(), | ||
| expected_id, | ||
| "codebook entry `{public_name}` should resolve to 0x{expected_id:04X}", | ||
| ); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl BridgeFromRegistry for MedcareBridge { | ||
| fn from_registry(registry: Arc<OntologyRegistry>) -> Result<Self> { | ||
| Self::new(registry) | ||
| #[test] | ||
| fn entity_for_non_codebook_name_falls_back_to_registry_lookup() { | ||
| let bridge = MedcareBridge::new(registry_with_healthcare()).unwrap(); | ||
| let err = bridge.entity("NotAConcept").unwrap_err(); | ||
| match err { | ||
| BridgeError::NotInScope { public_name, .. } => { | ||
| assert_eq!(public_name, "NotAConcept") | ||
| } | ||
| other => panic!("expected NotInScope, got {other:?}"), | ||
| } | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
When this alias opts MedCare into
UnifiedBridge's codebookentity()path, Healthcare aliases such asDiagnosiscan resolve fromHealthcarePorteven when the registry only has the namespace. The existing authorization/audit path inlance-graph-callcentercallsNamespaceBridge::row(public_name), and the defaultrow()still requires an actual registry row withbridge_id == "medcare"; normal TTL hydration files rows under"ogit", and the new fixture only seedsPatient. In that setup the advertised codebook aliases resolve viaentity()but fail before policy evaluation or audit, so either synthesize/overriderow()for these aliases or seed medcare rows for every alias.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Valid, and confirmed concretely: TTL hydration files mapping rows under
bridge_id = "ogit"(registry.rs:139), while the defaultNamespaceBridge::row()filters by the bridge's own id ("medcare") — per-bridge rows come from a separateappend_mappingstep, not hydration. Soentity()(codebook synthesis) androw()(registry-backed, the authz/audit path) can diverge for a codebook alias.But this is a property of the shared
UnifiedBridge<P>harness (lance-graph#570), not introduced by this PR:MedcareBridgeis literallytype MedcareBridge = UnifiedBridge<HealthcarePort>, identical to the already-mergedOpenProjectBridge/RedmineBridge— none of the three overriderow(). The minimal fixture seeds onlyPatientvia TTL to exercise namespace registration +entity()resolution + the not-in-scope fallback, not the full authz path.The real fix —
row()synthesizing from the codebook when no per-bridge mapping row exists — belongs inUnifiedBridge<P>(it would cover all three bridges) and touches the authz/audit path, so it's a deliberate harness change rather than something to slip into this alias PR (atypealias can't overriderow()anyway). Leaving this thread open as the tracking item — happy to do the harness change as a follow-up if you'd like.Generated by Claude Code