Skip to content

Record what normalizers make of a value beside it, on every extracted value - #56

Merged
jdkent merged 1 commit into
mainfrom
feat/normalized-sidecar
Nov 4, 2026
Merged

jdkent merged 1 commit into
mainfrom
feat/normalized-sidecar

Conversation

@jdkent

@jdkent jdkent commented Nov 3, 2026

Copy link
Copy Markdown
Member

What changes

A record is filled in three passes: values, evidence, then normalization. This gives the third pass one place to write, on every extracted value: ExtractedValue.normalized, a list of Normalization entries.

Slot Meaning
vocabulary (required) answer set: MONDO, ONVOC, AgeUnit, a canonical task set
status (required) mapped, unmapped (read, could not place), ambiguous, absent (value states an absence)
value, label the term
item, head which list item, and which part of a comorbidity string, the entry reads
qualifiers, rollup words lifted off before lookup; whether the term is broader than the source
method, also_read how the term was reached; other slots read (entity-level normalizations sit on name)
mapping_tool (required), confidence SSSOM's names: the normalizer and version, and its score
  • Migrated: Group.age_unit_normalized and CategoryDistribution.category_normalized leave extraction (deviations deleted, tree regenerated with pondie.schema.generate). Storage keeps both deterministic columns, now mapped from the sidecar.
  • Not normalizations: is_healthy and population_characteristics stay derived slots; cross-entity relations stay in derived tables.
  • Versions: extraction-evidence 0.3.0, extraction-to-storage map 0.8.0. extraction-readme.md documents the pass and narrows the immutability rule to "normalization writes only normalized".

Why

  • Each sibling cost a storage slot, a deviation and a map entry, and was written as an ExtractedValue with evidence: not_found.
  • The other normalizers (MONDO/ONVOC conditions, canonical tasks, medication status, modality) wrote only to tables outside the records.
  • A sibling cannot hold per-item or multi-vocabulary readings, which medical_condition needs.

Checks

  • pondie.schema.generate --check, and pondie's extraction_to_storage_map, linkml, field_provenance and storage_parameter_priorities checks, all pass against this branch (PONDIE_SCHEMA_DIR).

  • tools/generate_models.py --check and the full test suite pass. The new tests/test_normalized.py:

    • every wrapper carries normalized;
    • both the model and the JSON Schema accept a record with sidecars;
    • both refuse an unknown status, confidence > 1, a negative item, and a missing vocabulary or mapping_tool;
    • the siblings are gone from extraction and kept in storage.

    These tests fail on main.

  • Prototype on two real out_all2 records (14 sidecars across age unit, sex category, MONDO+ONVOC on medical_condition, canonical task on Task.name): 0 new LinkML errors; the previous schema rejects every sidecar.

Follow-up (pondie, not in this PR)

  • age_unit, sex_distribution and handedness_distribution must write sidecars instead of siblings. Records written by the current pondie fail this schema until then, because the extraction models refuse undeclared fields.
  • Field modules should emit their decisions as sidecars.
  • Migrate existing records.
  • Make the astra build read groups_medical and tasks from the records.

🤖 Generated with Claude Code

… value

A record is now filled in three passes: the value, the evidence for it, and what the value
means in a shared vocabulary. The third pass had nowhere general to write. Two fields had a
hand-declared sibling (`Group.age_unit_normalized`, `CategoryDistribution.category_normalized`),
each costing a storage slot, an extraction deviation and a map entry, and each written as a full
`ExtractedValue` with `value_source: generated` and `evidence: not_found` -- a normalized value
posing as an extracted one with missing evidence. Every other normalizer (medical condition to
MONDO and ONVOC, task to a canonical paradigm, medication status, modality, ...) wrote only to
tables beside the records, so a record could not say what its own fields meant.

`ExtractedValue.normalized` is a list of `Normalization` entries, inherited by every wrapper:
`vocabulary` and `status` (mapped / unmapped / ambiguous / absent), the term's `value` and
`label`, which list `item` and which `head` of the value it reads, lifted `qualifiers`,
`rollup`, `method`, `also_read`, and SSSOM's `mapping_tool` and `confidence`. One entry per
vocabulary, item and head, which a sibling slot could not hold: a group's conditions map item
by item, and in two vocabularies at once. Unset still means no normalizer ran, as the siblings
defined it; `unmapped` says one ran and could not place the value.

An entity-level normalization is recorded on the entity's `name`, with `also_read` naming the
other slots used. Derived summaries (`is_healthy`, `population_characteristics`) stay slots, and
relations across entities stay in derived tables.

The two siblings and their deviations are gone from extraction, regenerated by
`pondie.schema.generate`. Storage keeps its two `deterministic` columns, since storage has no
wrapper; the map now fills them from the sidecar (`source_representation: normalized`).
extraction-evidence 0.3.0, map 0.8.0.

Prototyped on two real records (14 placements: age unit, sex category, MONDO and ONVOC on
medical_condition, canonical task on Task.name): 0 new LinkML errors, all rejected by the
previous schema. The tests check both artefacts accept it and refuse an unknown status,
confidence above 1, a negative item, and a missing vocabulary or mapping_tool.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@jdkent jdkent left a comment

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant