groundworkers 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. {groundworkers-0.3.2 → groundworkers-0.3.3}/PKG-INFO +12 -1
  2. {groundworkers-0.3.2 → groundworkers-0.3.3}/README.md +11 -0
  3. {groundworkers-0.3.2 → groundworkers-0.3.3}/pyproject.toml +7 -1
  4. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/app.py +3 -1
  5. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/criteria-gate-yesno/guidance.md +57 -0
  6. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/criteria-gate-yesno/manifest.yaml +22 -0
  7. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/diagnosis-role-modifier/guidance.md +90 -0
  8. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/diagnosis-role-modifier/manifest.yaml +28 -0
  9. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/domain-routing/guidance.md +45 -0
  10. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/domain-routing/manifest.yaml +20 -0
  11. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/domain-routing/rules.yaml +17 -0
  12. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/drug-concept-granularity/guidance.md +52 -0
  13. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/drug-concept-granularity/manifest.yaml +22 -0
  14. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/drug-concept-granularity/rules.yaml +22 -0
  15. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/history-of-event/guidance.md +58 -0
  16. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/history-of-event/manifest.yaml +18 -0
  17. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/standard-concept-preference/guidance.md +55 -0
  18. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/standard-concept-preference/manifest.yaml +16 -0
  19. groundworkers-0.3.3/src/groundworkers/knowledge/packs/core/standard-concept-preference/rules.yaml +50 -0
  20. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/graph.py +171 -1
  21. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/knowledge/catalogue.py +24 -20
  22. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/concept_tools.py +95 -0
  23. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/knowledge_tools.py +23 -15
  24. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/PKG-INFO +12 -1
  25. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/SOURCES.txt +15 -0
  26. {groundworkers-0.3.2 → groundworkers-0.3.3}/setup.cfg +0 -0
  27. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/__init__.py +0 -0
  28. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/adapters/__init__.py +0 -0
  29. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/adapters/cdm.py +0 -0
  30. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/adapters/llm.py +0 -0
  31. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/adapters/omop_emb.py +0 -0
  32. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/adapters/omop_graph.py +0 -0
  33. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/base/__init__.py +0 -0
  34. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/base/errors.py +0 -0
  35. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/base/results.py +0 -0
  36. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/base/server.py +0 -0
  37. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/bootstrap.py +0 -0
  38. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/config.py +0 -0
  39. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/server.py +0 -0
  40. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/__init__.py +0 -0
  41. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/domain.py +0 -0
  42. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/grounding.py +0 -0
  43. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/knowledge/__init__.py +0 -0
  44. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/knowledge/models.py +0 -0
  45. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/mapping.py +0 -0
  46. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/__init__.py +0 -0
  47. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/assisted.py +0 -0
  48. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/canonical_headers.py +0 -0
  49. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/classifier.py +0 -0
  50. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposer.py +0 -0
  51. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/__init__.py +0 -0
  52. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/csv_.py +0 -0
  53. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/ddl_.py +0 -0
  54. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/docx_.py +0 -0
  55. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/json_.py +0 -0
  56. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/pdf_.py +0 -0
  57. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/xlsx_.py +0 -0
  58. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/decomposers/xml_.py +0 -0
  59. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/detector.py +0 -0
  60. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/models.py +0 -0
  61. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/normalisation.py +0 -0
  62. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/provenance.py +0 -0
  63. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/router.py +0 -0
  64. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/serialisation.py +0 -0
  65. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/service.py +0 -0
  66. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/source_profiles/__init__.py +0 -0
  67. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/source_profiles/base.py +0 -0
  68. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/source_profiles/redcap.py +0 -0
  69. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/source_profiles/registry.py +0 -0
  70. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/source_planning/warnings.py +0 -0
  71. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/text/__init__.py +0 -0
  72. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/text/models.py +0 -0
  73. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/text/prompts.py +0 -0
  74. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/text/service.py +0 -0
  75. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/services/vocab.py +0 -0
  76. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/__init__.py +0 -0
  77. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/domain_tools.py +0 -0
  78. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/embedding_tools.py +0 -0
  79. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/mapping_tools.py +0 -0
  80. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/resolver_tools.py +0 -0
  81. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/search_tools.py +0 -0
  82. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/source_planning_tools.py +0 -0
  83. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/system_tools.py +0 -0
  84. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/tools/text_tools.py +0 -0
  85. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/transports/__init__.py +0 -0
  86. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/transports/rest/__init__.py +0 -0
  87. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/transports/rest/api.py +0 -0
  88. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers/transports/rest/models.py +0 -0
  89. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/dependency_links.txt +0 -0
  90. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/entry_points.txt +0 -0
  91. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/requires.txt +0 -0
  92. {groundworkers-0.3.2 → groundworkers-0.3.3}/src/groundworkers.egg-info/top_level.txt +0 -0
  93. {groundworkers-0.3.2 → groundworkers-0.3.3}/tests/test_bootstrap_config.py +0 -0
  94. {groundworkers-0.3.2 → groundworkers-0.3.3}/tests/test_rest_api.py +0 -0
  95. {groundworkers-0.3.2 → groundworkers-0.3.3}/tests/test_server_registry.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: groundworkers
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: Groundworkers MCP server — read-only agentive access to OMOP vocabularies, concept graphs, and embeddings.
5
5
  Requires-Python: >=3.12
6
6
  Description-Content-Type: text/markdown
@@ -63,8 +63,19 @@ No patient-level writes. No session state. No transport-specific business logic.
63
63
  - exact, normalized, full-text, and embedding-backed retrieval
64
64
  - mapping-oriented candidate bundles and context assembly
65
65
  - stateless source-planning workflows
66
+ - bundled baseline knowledge packs for reusable mapping and planning context
66
67
  - LLM-backed text normalization and domain classification
67
68
 
69
+ ## Knowledge Packs
70
+
71
+ `groundworkers` ships with bundled baseline knowledge packs inside the package.
72
+ These packs are available by default and provide reusable guidance and rules
73
+ that apply broadly across deployments.
74
+
75
+ Site-specific or localisation packs are added through the shared stack config
76
+ via `tools.groundworkers.knowledge.packs_root`. When a configured pack has the
77
+ same `layer` and `name` as a bundled baseline pack, the configured copy wins.
78
+
68
79
  ## Runtime model
69
80
 
70
81
  ```mermaid
@@ -17,8 +17,19 @@ No patient-level writes. No session state. No transport-specific business logic.
17
17
  - exact, normalized, full-text, and embedding-backed retrieval
18
18
  - mapping-oriented candidate bundles and context assembly
19
19
  - stateless source-planning workflows
20
+ - bundled baseline knowledge packs for reusable mapping and planning context
20
21
  - LLM-backed text normalization and domain classification
21
22
 
23
+ ## Knowledge Packs
24
+
25
+ `groundworkers` ships with bundled baseline knowledge packs inside the package.
26
+ These packs are available by default and provide reusable guidance and rules
27
+ that apply broadly across deployments.
28
+
29
+ Site-specific or localisation packs are added through the shared stack config
30
+ via `tools.groundworkers.knowledge.packs_root`. When a configured pack has the
31
+ same `layer` and `name` as a bundled baseline pack, the configured copy wins.
32
+
22
33
  ## Runtime model
23
34
 
24
35
  ```mermaid
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "groundworkers"
3
- version = "0.3.2"
3
+ version = "0.3.3"
4
4
  description = "Groundworkers MCP server — read-only agentive access to OMOP vocabularies, concept graphs, and embeddings."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.12"
@@ -72,6 +72,12 @@ package-dir = {"" = "src"}
72
72
  [tool.setuptools.packages.find]
73
73
  where = ["src"]
74
74
 
75
+ [tool.setuptools.package-data]
76
+ groundworkers = [
77
+ "knowledge/packs/*/*/*.md",
78
+ "knowledge/packs/*/*/*.yaml",
79
+ ]
80
+
75
81
  [tool.pytest.ini_options]
76
82
  testpaths = ["tests"]
77
83
  markers = [
@@ -182,7 +182,9 @@ def build_services(config: AppConfig, adapters: Adapters) -> Services:
182
182
  assisted_classifier = AssistedColumnRoleClassifier(adapters.llm)
183
183
  services.source_planning = SourcePlanningService(assisted_classifier=assisted_classifier)
184
184
  if adapters.omop_graph is not None:
185
- services.graph = GraphService(adapters.omop_graph)
185
+ # cdm is optional; only the classified-edge traversals
186
+ # (concept_associations / concept_extended_inheritance) require it.
187
+ services.graph = GraphService(adapters.omop_graph, adapters.cdm)
186
188
  services.grounding = ConceptGroundingService(
187
189
  services.graph,
188
190
  min_fulltext_overlap=config.grounding.min_fulltext_overlap,
@@ -0,0 +1,57 @@
1
+ # "Meets Criteria For..." Yes/No Gates
2
+
3
+ ## The pattern
4
+
5
+ Source instruments frequently ask a dichotomous question that determines
6
+ whether a diagnosis, syndrome, or clinical state applies: "Does the
7
+ participant meet the criteria for dementia?", "Does the participant meet
8
+ criteria for MCI?", "Does the participant meet criteria for MBI?". These are
9
+ not ordinary categorical Yes/No observations — they are a gate on whether a
10
+ specific standard concept should be asserted for this participant at all.
11
+
12
+ ## The model
13
+
14
+ - **Yes (1)**: emit one record for the underlying concept — CONDITION_OCCURRENCE
15
+ when the concept's domain is Condition, OBSERVATION otherwise (per
16
+ `domain-routing`).
17
+ - **No (0)**: emit nothing. Do not write a parallel OBSERVATION record coding
18
+ the negative answer with a generic "No" value_as_concept_id.
19
+
20
+ This differs from an ordinary Yes/No categorical item (e.g. "does the
21
+ participant currently smoke?"), where both directions are independently
22
+ informative and both get recorded. The distinguishing feature of a criteria
23
+ gate is that a "No" answer only means "the named concept does not apply" —
24
+ it carries no positive clinical content of its own.
25
+
26
+ ## The exception
27
+
28
+ Before defaulting to "No → nothing", check whether a standard concept exists
29
+ for the *negative* finding specifically (e.g. an explicit "criteria not met"
30
+ or "absence of X" concept distinct from a generic negation). This is
31
+ uncommon — most vocabularies do not pre-coordinate the negative case — but
32
+ when one exists, use it rather than suppressing the record.
33
+
34
+ ## Detection
35
+
36
+ Look for phrasing such as "meet(s) criteria for", "meet(s) the criteria for",
37
+ "meets any criteria for" in the field label, where the field is a radio/Yes-No
38
+ item gating a specific diagnostic concept. Ordinary presence/absence
39
+ checkboxes and self-report Yes/No items that are not framed as a diagnostic
40
+ criteria determination are out of scope for this pattern — apply
41
+ `value-representation`'s normal Yes/No handling to those instead.
42
+
43
+ ## What not to do
44
+
45
+ - Do not write a `value_as_concept_id`-coded "No" record for a criteria gate —
46
+ it adds a row with no clinical content and implies a false symmetry between
47
+ meeting and not meeting diagnostic criteria.
48
+ - Do not use this pattern for items where "No" is itself independently
49
+ meaningful (e.g. a clinician confidence judgment, a symptom checklist item)
50
+ — only apply it to fields that are literally asking whether the participant
51
+ meets criteria for a named concept.
52
+
53
+ ## See also
54
+
55
+ `diagnosis-role-modifier` often applies to the same syndromes downstream of a
56
+ criteria gate — once a diagnosis is asserted via a "Yes", a separate role/status
57
+ field may further qualify it.
@@ -0,0 +1,22 @@
1
+ name: criteria-gate-yesno
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ Yes/No fields phrased as "meets criteria for X" are a gate on asserting a
7
+ concept, not an ordinary categorical observation: Yes emits one record for
8
+ the concept (CONDITION_OCCURRENCE or OBSERVATION per its domain), No emits
9
+ nothing. Check for an explicit negative-finding concept before defaulting
10
+ to suppression. Does not apply to ordinary Yes/No items where both
11
+ directions are independently informative.
12
+
13
+ mechanisms:
14
+ - context-inject
15
+
16
+ applicability:
17
+ always: true
18
+
19
+ see_also:
20
+ - diagnosis-role-modifier
21
+ - domain-routing
22
+ - value-representation
@@ -0,0 +1,90 @@
1
+ # Diagnosis + Role/Status Modifier
2
+
3
+ ## The pattern
4
+
5
+ Some source instruments split a single clinical fact across two fields:
6
+
7
+ 1. A **presence field** — does the participant have this diagnosis/finding at all?
8
+ 2. A **role/status field** — what is its status or role relative to the participant's
9
+ overall clinical picture (primary, contributing, non-contributing; primary vs.
10
+ secondary; admitting vs. discharge; provisional vs. confirmed)?
11
+
12
+ This is a two-source, one-target mapping: both fields describe the *same*
13
+ CONDITION_OCCURRENCE record, not two independent facts. Do not emit a second,
14
+ freestanding OBSERVATION record to carry the role — populate
15
+ `CONDITION_OCCURRENCE.condition_status_concept_id` on the same record instead.
16
+
17
+ ## Detection
18
+
19
+ Look for a presence/diagnosis field paired with a sibling field whose label reads
20
+ as a status, role, or type qualifier of that same diagnosis (often suffixed or
21
+ named similarly — e.g. `xdx` + `xdxif`, `xdx` + `xdx_role`). The pairing is
22
+ usually confirmed by the branching logic or field note referencing the diagnosis
23
+ field, and by choice lists such as "Primary / Contributing / Non-contributing" or
24
+ "Primary / Secondary".
25
+
26
+ ## The model
27
+
28
+ | Field | Value |
29
+ |---|---|
30
+ | Table | CONDITION_OCCURRENCE |
31
+ | condition_concept_id | standard concept for the diagnosis (from the presence field) |
32
+ | condition_status_concept_id | standard concept from the **Condition Status** vocabulary, selected using the role field's value |
33
+
34
+ `Condition Status` is its own OMOP vocabulary/domain. Commonly available concepts include:
35
+
36
+ | concept_id | concept_name |
37
+ |---|---|
38
+ | 32902 | Primary diagnosis |
39
+ | 32908 | Secondary diagnosis |
40
+ | 32890 | Admission diagnosis |
41
+ | 32896 | Discharge diagnosis |
42
+ | 32899 | Preliminary diagnosis |
43
+ | 32893 | Confirmed diagnosis |
44
+
45
+ There is no dedicated "Contributing diagnosis" concept — map a "contributing"
46
+ role to **Secondary diagnosis (32908)**, the closest available status, and note
47
+ the approximation in the mapping's notes/comments.
48
+
49
+ ## Non-contributing (and other null-role values)
50
+
51
+ A role value that means "not actually relevant/contributing" (e.g.
52
+ "Non-contributing") is not a status to record — it means the diagnosis should
53
+ not be asserted at all for this record. **Suppress the CONDITION_OCCURRENCE
54
+ record entirely** when the role field carries this value, rather than writing
55
+ a record with an empty or misleading condition_status_concept_id.
56
+
57
+ ## What not to do
58
+
59
+ - Do not create a second OBSERVATION row (concept_id=0 or otherwise) just to
60
+ carry the role/status value as free text or a generic categorical code —
61
+ that treats a modifier of the diagnosis as an independent clinical fact.
62
+ - Do not populate condition_status_concept_id with a source-local code; always
63
+ resolve it to a standard Condition Status concept.
64
+ - Do not assert the CONDITION_OCCURRENCE record when the paired role field
65
+ indicates the diagnosis is non-contributing/not applicable.
66
+
67
+ ## Output structure: representing the merge in a row-per-source-field ETL format
68
+
69
+ When the mapping output is authored as one row per source field, this pattern
70
+ is an n:1 merge (two source fields, one CDM record) and needs an explicit way
71
+ to say "this record's condition_status_concept_id comes from a second field."
72
+ The convention used in this pipeline is three additional columns on the
73
+ presence field's row:
74
+
75
+ - `secondary_source_field` — the source field supplying the role/status value
76
+ - `secondary_target_column` — the CDM column it populates (`condition_status_concept_id`)
77
+ - `secondary_code_map` — `source_code:concept_id` pairs, pipe-separated, with the
78
+ literal token `SUPPRESS` for any code that should drop the record
79
+ (e.g. `1:32902|2:32908|3:SUPPRESS`)
80
+
81
+ The role field itself gets no row of its own — it is consumed entirely by the
82
+ presence field's row. This same three-column mechanism generalizes to other
83
+ n:1 merges beyond diagnosis role (e.g. a presence flag paired with a severity
84
+ or laterality modifier), not just condition status.
85
+
86
+ ## See also
87
+
88
+ `pre-coordinated-split` covers the related but distinct case where a *single*
89
+ source concept's vocabulary relationships decompose into an entity + value —
90
+ this pack covers two *separate* source fields describing one record.
@@ -0,0 +1,28 @@
1
+ name: diagnosis-role-modifier
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ When a diagnosis/finding is captured across two source fields — a presence
7
+ field and a separate role/status field (primary/contributing/non-contributing,
8
+ primary/secondary, admitting/discharge, etc.) — model it as one
9
+ CONDITION_OCCURRENCE record with condition_status_concept_id populated from
10
+ the role field, not as two independent records. A non-contributing/not-
11
+ applicable role suppresses the record entirely. Includes the
12
+ secondary_source_field/secondary_target_column/secondary_code_map
13
+ convention for representing this n:1 merge in a row-per-source-field ETL
14
+ output.
15
+
16
+ mechanisms:
17
+ - context-inject
18
+ - post-filter
19
+
20
+ applicability:
21
+ always: false
22
+ domains:
23
+ - Condition
24
+
25
+ see_also:
26
+ - pre-coordinated-split
27
+ - domain-routing
28
+ - standard-concept-preference
@@ -0,0 +1,45 @@
1
+ # Domain Routing
2
+
3
+ ## The fundamental rule
4
+
5
+ The `domain_id` of the **target standard concept** determines which CDM table a record is written to — not the domain implied by the source code, source vocabulary, or source table structure.
6
+
7
+ A source item coded as a procedure that maps to an RxNorm Ingredient goes to `DRUG_EXPOSURE`, not `PROCEDURE_OCCURRENCE`. A source item that looks like a diagnosis but maps to a LOINC measurement concept goes to `MEASUREMENT`. The target concept is authoritative.
8
+
9
+ ## Domain → CDM table mapping
10
+
11
+ | domain_id | Primary CDM table |
12
+ |-----------|-------------------|
13
+ | Condition | CONDITION_OCCURRENCE |
14
+ | Drug | DRUG_EXPOSURE |
15
+ | Measurement | MEASUREMENT |
16
+ | Observation | OBSERVATION |
17
+ | Procedure | PROCEDURE_OCCURRENCE |
18
+ | Device | DEVICE_EXPOSURE |
19
+ | Specimen | SPECIMEN |
20
+ | Visit | VISIT_OCCURRENCE |
21
+ | Provider | PROVIDER |
22
+
23
+ ## Scope: the entity concept only
24
+
25
+ This rule routes on the domain of the **entity** concept — the one going into
26
+ `measurement_concept_id`, `observation_concept_id`, `condition_concept_id`, etc.
27
+ It does not apply to `value_as_concept_id` or other subordinate value fields.
28
+ A value concept may legitimately come from a different domain than the record
29
+ it lives in — e.g. a Condition-domain concept (a plain diagnosis) used as
30
+ `value_as_concept_id` on an OBSERVATION row for a history-of or family-history
31
+ pattern (see `history-of-event`). Do not re-route a record to a different CDM
32
+ table because its value concept's domain differs from the entity concept's
33
+ domain — only the entity concept's domain decides the table.
34
+
35
+ ## Unmapped source items (concept_id = 0)
36
+
37
+ When no valid standard concept mapping exists, set `*_concept_id = 0` (not NULL). The record still belongs in a CDM table — route using the best available domain signal (source vocabulary, source table, or caller context). Document the zero-mapping so it surfaces in data quality review.
38
+
39
+ Do not omit a record solely because no mapping was found. The source value is preserved in `*_source_value` and the zero-mapped record remains queryable.
40
+
41
+ ## What not to do
42
+
43
+ - Do not route based on the source vocabulary (ICD codes are not always Condition; NDC codes are almost always Drug but verify the target concept's domain).
44
+ - Do not route based on the source table name or caller hint alone.
45
+ - Do not use concept_id = NULL — zero-mapping is the standard for unmapped concepts.
@@ -0,0 +1,20 @@
1
+ name: domain-routing
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ The domain_id of the target standard concept — not the source — is the
7
+ authoritative signal for which CDM table a record belongs in. Includes
8
+ the canonical domain-to-table mapping and the concept_id=0 fallback rule
9
+ for unmapped source items.
10
+
11
+ mechanisms:
12
+ - context-inject
13
+ - pre-compute
14
+
15
+ applicability:
16
+ always: true
17
+
18
+ see_also:
19
+ - standard-concept-preference
20
+ - one-to-many-mapping
@@ -0,0 +1,17 @@
1
+ domain_to_table:
2
+ Condition: CONDITION_OCCURRENCE
3
+ Drug: DRUG_EXPOSURE
4
+ Measurement: MEASUREMENT
5
+ Observation: OBSERVATION
6
+ Procedure: PROCEDURE_OCCURRENCE
7
+ Device: DEVICE_EXPOSURE
8
+ Specimen: SPECIMEN
9
+ Visit: VISIT_OCCURRENCE
10
+ Provider: PROVIDER
11
+
12
+ unmapped_concept_id: 0
13
+
14
+ routing_signal_priority:
15
+ - target_concept_domain_id # authoritative
16
+ - source_vocabulary_hint # fallback when target not yet resolved
17
+ - caller_context_hint # last resort
@@ -0,0 +1,52 @@
1
+ # Drug Concept Granularity
2
+
3
+ ## RxNorm concept hierarchy
4
+
5
+ RxNorm organises drug concepts into levels of specificity. Standard mapping targets
6
+ exist at multiple levels, and the appropriate level depends on what information the
7
+ source provides.
8
+
9
+ | RxNorm concept class | Contains | Example |
10
+ |---------------------|----------|---------|
11
+ | Ingredient | Active substance only | Metformin |
12
+ | Clinical Drug Component | Ingredient + strength | Metformin 500 MG |
13
+ | Clinical Drug Form | Ingredient + dose form | Metformin Oral Tablet |
14
+ | Clinical Drug | Ingredient + strength + dose form | Metformin 500 MG Oral Tablet |
15
+ | Branded Drug | Clinical Drug + brand name | Glucophage 500 MG Oral Tablet |
16
+ | Marketed Product | Branded + packaging | Glucophage 500 MG Oral Tablet [30 Tablet] |
17
+
18
+ ## Selection rule
19
+
20
+ **Use the most specific level the source data supports, up to Clinical Drug.**
21
+
22
+ - Source provides only drug name (no dose, no form) → map to **Ingredient**
23
+ - Source provides drug name + dose form → map to **Clinical Drug Form**
24
+ - Source provides drug name + strength + dose form → map to **Clinical Drug**
25
+ - Source provides dose but not form (or vice versa) → map to **Clinical Drug Component** or **Clinical Drug Form** as appropriate; prefer Ingredient if precision would be misleading
26
+
27
+ **Branded Drug and Marketed Product are not valid standard mapping targets.** Even
28
+ when the source specifies a brand name, map to the equivalent Clinical Drug or
29
+ Ingredient (which carries standard_concept = 'S'). Brand names are preserved in
30
+ `drug_source_value`.
31
+
32
+ ## Why this matters
33
+
34
+ Mapping all drug concepts to Ingredient level maximises analytical cohesion: queries
35
+ for "Metformin" will find records regardless of formulation. Mapping too high (always
36
+ Ingredient) when formulation is known loses clinically meaningful precision. Mapping
37
+ to Branded level fragments the same clinical drug across multiple non-standard
38
+ concepts and breaks standard vocabulary queries.
39
+
40
+ ## Dose form and strength information in source data dictionaries
41
+
42
+ Source data dictionaries often carry drug labels that include partial formulation
43
+ information ("Metformin HCl 500mg tabs"). Extract what is explicitly present in the
44
+ label; do not infer formulation from context. If the dose/form in the label is
45
+ ambiguous or inconsistent with known products, fall back to Ingredient.
46
+
47
+ ## RxNorm Extension
48
+
49
+ When no RxNorm Ingredient or Clinical Drug exists for a drug (typically non-US
50
+ drugs or novel agents), RxNorm Extension provides equivalent concepts with the
51
+ same class hierarchy. Apply the same granularity rules. Prefer core RxNorm over
52
+ RxNorm Extension when both cover the same entity.
@@ -0,0 +1,22 @@
1
+ name: drug-concept-granularity
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ RxNorm concept level selection for drug mappings: prefer Ingredient when
7
+ dose/form/strength are unavailable; prefer Clinical Drug when they are known.
8
+ Branded Drug and Marketed Product are not valid standard mapping targets.
9
+ Applies to all drug concept mapping regardless of source system.
10
+
11
+ mechanisms:
12
+ - context-inject
13
+ - post-filter
14
+
15
+ applicability:
16
+ always: false
17
+ domains:
18
+ - Drug
19
+
20
+ see_also:
21
+ - standard-concept-preference
22
+ - domain-routing
@@ -0,0 +1,22 @@
1
+ # Drug concept granularity selection rules
2
+ # Applied as post-filter to drug domain candidates.
3
+
4
+ target_concept_class_preference:
5
+ # Ordered from most preferred (given sufficient source data) to least.
6
+ - concept_class_id: Clinical Drug
7
+ requires_source_fields: [strength, dose_form]
8
+ - concept_class_id: Clinical Drug Form
9
+ requires_source_fields: [dose_form]
10
+ - concept_class_id: Clinical Drug Component
11
+ requires_source_fields: [strength]
12
+ - concept_class_id: Ingredient
13
+ requires_source_fields: [] # always acceptable fallback
14
+
15
+ reject_concept_classes:
16
+ - Branded Drug # standard_concept != 'S', brand name not a CDM target
17
+ - Marketed Product # packaging level, not a clinical concept
18
+ - Clinical Pack # multi-drug packages, not direct mapping targets
19
+
20
+ vocab_preference_within_class:
21
+ - RxNorm # always prefer core RxNorm
22
+ - RxNorm Extension # fallback for drugs absent from core RxNorm
@@ -0,0 +1,58 @@
1
+ # History-of-Event Pattern
2
+
3
+ ## The model
4
+
5
+ When a source item describes a patient's *history* of a condition, event, or
6
+ procedure, the correct OMOP representation is:
7
+
8
+ | Field | Value |
9
+ |---|---|
10
+ | Table | OBSERVATION |
11
+ | observation_concept_id | 4214956 (History of event) |
12
+ | value_as_concept_id | standard concept for the actual condition or event |
13
+
14
+ The entity concept is always 4214956. The clinical content goes in value_as_concept_id.
15
+ Do not populate CONDITION_OCCURRENCE for these items.
16
+
17
+ ## Detection
18
+
19
+ Applies when the source label contains phrasing such as:
20
+ "history of", "h/o", "past history of", "prior history of", "previous history of",
21
+ "hx of", "Hx:", "prior diagnosis of".
22
+
23
+ ## Family history
24
+
25
+ Family history follows the same structural pattern but uses a different entity concept:
26
+
27
+ | Field | Value |
28
+ |---|---|
29
+ | observation_concept_id | 4167217 (Family history of clinical finding) |
30
+ | value_as_concept_id | standard concept for the familial condition |
31
+
32
+ ### Choosing the value concept
33
+
34
+ Many vocabularies (SNOMED in particular) pre-coordinate a "Family history of X"
35
+ concept for common conditions (e.g. concept 4326002 "Family history of dementia",
36
+ concept_class Context-dependent). Do not use one of these pre-coordinated
37
+ concepts as value_as_concept_id — that doubles up the family-history context
38
+ (the entity concept already says "family history of"; the value should be the
39
+ *plain* condition, e.g. concept 4182210 "Dementia", not 4326002). Search for the
40
+ plain condition/finding concept for the value, the same as you would if mapping
41
+ the condition directly in CONDITION_OCCURRENCE, and pair it with the 4167217
42
+ entity concept.
43
+
44
+ ## What not to do
45
+
46
+ - Do not map "history of MI" to the myocardial infarction concept in
47
+ CONDITION_OCCURRENCE. The distinction between current condition and reported
48
+ history is clinically and analytically important.
49
+ - Do not leave value_as_concept_id empty. If the condition cannot be mapped,
50
+ the item is unmappable — do not create a partial record with only the history
51
+ entity concept.
52
+
53
+ ## Downstream implications
54
+
55
+ CONDITION_OCCURRENCE is not populated for history-of items. Cohort definitions
56
+ and prevalence queries that rely on CONDITION_OCCURRENCE will not count these
57
+ records. Analysts querying "ever had condition X" must also search OBSERVATION
58
+ for history-of records if the source data uses history-of phrasing.
@@ -0,0 +1,18 @@
1
+ name: history-of-event
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ OMOP model for source items describing a patient's history of a clinical event.
7
+ Source labels with "history of" phrasing always map to OBSERVATION 4214956 (History
8
+ of event) with the actual condition or finding in value_as_concept_id — never to
9
+ CONDITION_OCCURRENCE. Includes family history variant.
10
+
11
+ mechanisms:
12
+ - context-inject
13
+
14
+ applicability:
15
+ always: true
16
+
17
+ see_also:
18
+ - value-representation
@@ -0,0 +1,55 @@
1
+ # Standard Concept Selection
2
+
3
+ ## Concepts must be active
4
+
5
+ Before selecting any standard concept as a mapping target, verify it is active:
6
+ - `INVALID_REASON IS NULL` (not deprecated, not updated, not deleted)
7
+ - `VALID_END_DATE = 2099-12-31` (OHDSI convention for currently active concepts)
8
+
9
+ Deprecated concepts (INVALID_REASON = 'D') and concepts that have been replaced
10
+ (INVALID_REASON = 'U') must not be used as mapping targets, even if they have
11
+ standard_concept = 'S'. For replaced concepts, follow the `Maps to` relationship
12
+ from the deprecated concept to find its active successor.
13
+
14
+ ## Standard concepts are required for entity fields
15
+
16
+ A concept is standard when standard_concept = 'S' in the CONCEPT table.
17
+ All entity concept fields (condition_concept_id, measurement_concept_id,
18
+ observation_concept_id, drug_concept_id, etc.) must be populated with
19
+ standard concepts only.
20
+
21
+ Non-standard concepts belong in source_concept_id, not entity concept fields.
22
+
23
+ ## Classification concepts are not valid mapping targets
24
+
25
+ Concepts with standard_concept = 'C' are classification nodes — they exist in
26
+ the hierarchy to support ancestor queries but are not intended as direct mapping
27
+ targets. If a candidate search returns a classification concept, look at its
28
+ standard descendants to find the appropriate specific concept.
29
+
30
+ ## Vocabulary precedence by domain
31
+
32
+ When multiple vocabularies offer standard concepts for the same clinical entity,
33
+ prefer by domain:
34
+
35
+ **Condition**: SNOMED CT preferred. ICD-10CM/ICD-9CM are non-standard and require
36
+ a "Maps to" relationship to reach the standard SNOMED target.
37
+
38
+ **Drug**: RxNorm preferred. RxNorm Extension covers drugs not yet in core RxNorm;
39
+ use it only when no RxNorm equivalent exists for the same clinical drug.
40
+
41
+ **Measurement**: LOINC preferred for lab tests and clinical assessments with
42
+ established LOINC codes. SNOMED for findings without LOINC representation.
43
+
44
+ **Procedure**: SNOMED preferred for clinical EHR data. CPT4 preferred for US
45
+ procedural billing or claims context.
46
+
47
+ **Observation**: SNOMED preferred. LOINC for social and functional observations
48
+ that have LOINC panel/question codes.
49
+
50
+ ## Zero concept
51
+
52
+ When no suitable standard concept exists, use concept_id = 0 (no matching concept).
53
+ Do not use a non-standard concept as a fallback for an entity concept field.
54
+ concept_id = 0 is the explicit signal that mapping was not possible; it is
55
+ preferable to a misleading non-standard concept.
@@ -0,0 +1,16 @@
1
+ name: standard-concept-preference
2
+ layer: core
3
+ version: "1.0"
4
+ shareability: public
5
+ scope_summary: >
6
+ Rules for selecting standard concepts as mapping targets: standard_concept='S'
7
+ required for entity fields, classification concepts ('C') are not valid targets,
8
+ and vocabulary precedence by domain (SNOMED for conditions, RxNorm over RxNorm
9
+ Extension for drugs, LOINC for measurements).
10
+
11
+ mechanisms:
12
+ - context-inject
13
+ - post-filter
14
+
15
+ applicability:
16
+ always: true