Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,7 @@ claw_swarm_memory.db*
ros3-agentdb.db
# Convention for any future private data — put it under private/
/private/
# Real (PHI) dossier drop-in for the local UI — never commit; only the
# synthetic demo-dossier.json is tracked. hybrid.html auto-loads this if present.
/ui/private/
ui/private/
180 changes: 180 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ members = [
"crates/helix-refranges",
"crates/helix-demo",
"crates/helix-wasm",
"crates/helix-ingest",
]

[workspace.package]
Expand Down
41 changes: 39 additions & 2 deletions crates/helix-connect/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,10 @@ pub fn parse_observation(
if obs["resourceType"].as_str() != Some("Observation") {
return Err(ConnectError::Parse("not an Observation".into()));
}
let id = obs["id"].as_str().unwrap_or("obs").to_string();
// Explicit id if present; otherwise synthesized below from code + measured_at
// (the `id` binding near the ProvRecord). Deferring lets us fall back to a
// UNIQUE key instead of the literal "obs", which used to collide.
let explicit_id = obs["id"].as_str();

// LOINC coding.
let coding = obs["code"]["coding"]
Expand Down Expand Up @@ -137,8 +140,15 @@ pub fn parse_observation(
let reference_range =
rr.map(|r| ReferenceRange::new(r["low"]["value"].as_f64(), r["high"]["value"].as_f64()));

// With an explicit id, keep the stable `fhir-{source}-{id}`. Without one,
// synthesize a UNIQUE id from code + measured_at (mirrors the Apple scheme)
// so distinct id-less observations on the same date never collide.
let id = match explicit_id {
Some(existing) => format!("fhir-{source}-{existing}"),
None => format!("fhir-{source}-{code}-{measured_at}"),
};
Ok(ProvRecord {
id: RecordId::from(format!("fhir-{source}-{id}")),
id: RecordId::from(id),
source: source.to_string(),
measured_at,
method: MeasurementMethod::LabFeed,
Expand Down Expand Up @@ -338,6 +348,33 @@ mod tests {
assert!(parse_observation(&no_loinc, "s").is_err());
}

#[test]
fn idless_same_date_distinct_codes_get_distinct_ids() {
// Three id-less Observations, SAME date, DISTINCT LOINC codes. Before the
// fix they all collapsed to id "obs" and overwrote each other; now each
// gets a unique code+date id.
let mk = |code: &str| {
serde_json::json!({
"resourceType": "Observation",
"code": { "coding": [{ "system": "http://loinc.org", "code": code, "display": "x" }] },
"valueQuantity": { "value": 1.0, "unit": "u" },
"effectiveDateTime": "2026-06-19"
})
};
let a = parse_observation(&mk("2276-4"), "s").unwrap();
let b = parse_observation(&mk("2085-9"), "s").unwrap();
let c = parse_observation(&mk("3016-3"), "s").unwrap();
let ids: std::collections::BTreeSet<RecordId> = [a.id, b.id, c.id].into_iter().collect();
assert_eq!(ids.len(), 3, "id-less same-date distinct-code obs must yield 3 distinct ids");
}

#[test]
fn explicit_id_path_unchanged() {
let obs = observation("2276-4", "Ferritin", 28.0, "ng/mL", "2026-06-19");
let r = parse_observation(&obs, "MyChart").unwrap();
assert_eq!(r.id, RecordId::from("fhir-MyChart-o1"), "with-id path is stable");
}

#[test]
fn connector_imports_and_queues_unparseable() {
let good = observation("2276-4", "Ferritin", 28.0, "ng/mL", "2026-06-19");
Expand Down
Loading
Loading