Skip to content
Merged
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
55 changes: 38 additions & 17 deletions docs/concepts.md
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
# Concepts and capability choices

Groundworkers is a read-only capability layer for working with OMOP vocabularies and source metadata. It combines deterministic vocabulary and graph operations with optional retrieval and model-assisted steps, then makes those capabilities available through MCP, REST, or direct Python.
Groundworkers provides read-only tools for working with OMOP vocabularies and source metadata. It combines deterministic vocabulary and graph operations with optional retrieval and model-assisted steps, available through MCP, REST, or direct Python.

The important distinction is between the work a capability performs and the transport used to call it. MCP, REST, and Python use the same runtime and, where a service exists, the same service implementation.
MCP, REST, and Python are different ways to call the same runtime. Where a service exists, they share its implementation.

## The main workflow

Most mapping-oriented work follows this shape:
Mapping usually starts with source text or fields. Text preparation or domain classification can help narrow the search. Retrieval finds candidate concepts; grounding tries to resolve the input using ranked rules. Mapping brings the candidates, evidence, and graph context together for a caller or reviewer to choose a mapping. Optional semantic projection then uses the grounded concept and source context to produce CDM rows.

```mermaid
flowchart LR
SOURCE[Source text or fields] --> PREP[Optional text or domain preparation]
PREP --> RETRIEVE[Retrieve OMOP candidates]
RETRIEVE --> GROUND[Ground or assemble evidence]
GROUND --> REVIEW[Caller or reviewer selects a mapping]
REVIEW --> CONTEXT[Add graph and mapping context]
CONTEXT --> PROJECT[Optional semantic projection]
flowchart TD
SOURCE["Source text or fields"] -->|Retrieval and grounding| CANDIDATES["Candidate concepts and evidence"]
CANDIDATES -->|Caller or reviewer chooses| SELECTED["Selected concept or concepts"]
SELECTED -. Optional semantic projection .-> ROWS["CDM rows"]
SOURCE -. Source context .-> ROWS
```

These stages are deliberately separate. Retrieval produces candidates; grounding applies a ranked resolution policy; mapping gathers evidence and context for a decision; semantic projection turns an already-grounded concept plus source context into CDM rows.

## Grounding, mapping, and retrieval

### Retrieval
Expand All @@ -37,15 +33,34 @@ Use the search tools when you want to choose the strategy yourself and inspect i

Grounding is a ranked, policy-driven attempt to resolve free text to concepts. `concept_ground` runs its configured tiers in order and stops at the first tier that produces results. It reports the matched tier and whether embedding retrieval was used. Domain, vocabulary, ancestry, standardness, and activity constraints narrow the search.

```mermaid
flowchart TD
INPUT["Text and constraints"] --> SEARCH["Search the next configured tier"]
SEARCH --> MATCH{"Any matches?"}
MATCH -->|Yes: stop searching| RESULT["Return candidates<br/>and the matched tier"]
MATCH -->|No| MORE{"More tiers?"}
MORE -->|Yes| SEARCH
MORE -->|No| EMPTY["Return no candidates"]
```

Grounding is not proof that the first result is the correct mapping. A caller that needs reviewable evidence should use `concept_candidate_bundle`, inspect the candidate union, and add `concept_mapping_context` for the selected concept.

### Mapping

Mapping is the broader decision workflow around a source term. Mapping tools combine retrieval channels, navigate non-standard concepts to standard targets, find broader standard ancestors, assemble graph context, follow `Maps to value` relationships, and evaluate predictions against references. They are useful when the caller must preserve evidence or ask a human or another model to make the final choice.

```mermaid
flowchart TD
CANDIDATES["Candidates from<br/>multiple searches"] --> REVIEW["Caller or reviewer<br/>assesses the evidence"]
SOURCE["Source meaning<br/>and domain rules"] --> REVIEW
GRAPH["Standard targets<br/>and graph context"] --> REVIEW
REVIEW --> DECISION["Mapping to one or more concepts<br/>with supporting evidence"]
REVIEW -->|Insufficient evidence| OPEN["Leave unresolved"]
```

## Deterministic and model-assisted capabilities

Groundworkers does not treat every capability as an LLM task.
Most vocabulary and graph operations run without an LLM. Some text preparation and classification tasks use a configured model.

| Capability | Deterministic? | Additional infrastructure |
|---|---:|---|
Expand All @@ -63,15 +78,21 @@ An embedding model and a chat model are separate configuration references. A dep

## Source planning and knowledge packs

Source planning is stateless analysis before ingestion. It decomposes an input artifact, normalizes its structure, annotates columns, and returns neutral planning artifacts such as column roles, domain hints, warnings, and an ingestion strategy. It does not own session state or decide how a particular orchestration job is persisted. `groundcrew` is one consumer of this worker-side capability.
Source planning analyses an input before ingestion. It decomposes the input, normalizes its structure, annotates columns, and returns results such as column roles, domain hints, warnings, and an ingestion strategy. The caller manages session state and job persistence. `groundcrew` uses this capability.

The deterministic `source_plan` path uses format and header evidence. `source_plan_assisted` is an explicit second step that can use the configured chat model when deterministic classification is not strong enough. The result records whether the assisted tier was used.
The deterministic `source_plan` path uses format and header evidence. Call `source_plan_assisted` as a separate step when that evidence is insufficient; it can use the configured chat model. The result records whether the assisted tier was used.

Knowledge packs are a separate discovery mechanism. Bundled baseline packs and optional site or localisation packs contain reusable guidance and rules. `knowledge_catalogue` finds packs applicable to a source system, domain, or section; `knowledge_pack` loads the selected pack. Discovery can happen before or after source planning.

## Prompt guidance and user decisions

An agent can use workflow prompts to organise a task and elicitation prompts to ask for missing information. Domain rules and tool results help it assess the answers. Answering a question may resolve one decision while leaving other work open.

See [Working with prompts](usage/prompt-handling.md) for choosing guidance, handling unanswered questions, and requesting more explanation when needed.

## Semantic projection

Grounding answers “which concept does this source item refer to?” Projection answers “given that concept and this source context, which CDM row or rows should be emitted?” It is useful for sibling-field modifiers, value-carried family history, deterministic suppression, and multi-row output definitions. Projection never chooses the concept and never calls an LLM. It is opt-in because a deployment must deliberately choose to expose the definition catalogue.
Grounding answers “which concept does this source item refer to?” Projection answers “given that concept and this source context, which CDM row or rows should be emitted?” It is useful for sibling-field modifiers, value-carried family history, deterministic suppression, and multi-row output definitions. Projection never chooses the concept and never calls an LLM. It must be enabled explicitly to expose the definition catalogue.

## Choose an interface

Expand All @@ -85,7 +106,7 @@ Use `groundworkers --describe` to inspect the active MCP tools, prompts, resourc

## Relationship to companion packages

Groundworkers composes several focused packages rather than replacing them:
Groundworkers uses these packages:

- `omop-alchemy` provides OMOP CDM models and database configuration;
- `omop-graph` provides graph-backed concept traversal and resolver primitives;
Expand Down
19 changes: 19 additions & 0 deletions docs/development/plugins.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Plugin architecture

For an overview and examples, start with [Plugins](../plugins.md). This guide
covers the implementation contract for plugin authors.

## What a plugin is

Groundworkers plugins are independently installed packages that add an adapter, service, and MCP surface without adding plugin-specific imports to Groundworkers. They register one ready-to-use plugin object under the `groundworkers.plugins` entry-point group.
Expand Down Expand Up @@ -205,6 +208,22 @@ Verification is read-only and returns a `PluginReadinessResult` containing a sta

The contract is headless: it contains no Groundskeeping types. Groundworkers uses the same result for generic setup-console rendering, while a plugin may return `result.as_dict()` from its own MCP status tool. Plugins own dependency- specific checks; the host only builds current plugin state, handles unavailable configuration safely, and maps readiness states to existing Groundskeeping status, table, empty, action, and detail contracts.

## Configuration linting

A plugin may implement the runtime-checkable `GroundworkersPluginLinter` protocol when its settings need checks beyond the configuration schema. The hook is `lint_config(config) -> PluginLintReport`; it receives the plugin's validated `PackageConfigBase` instance, or `None` when the plugin has no config class. It runs without calling `build()`, so it can check configuration-dependent files or cross-field rules without constructing runtime services.

Return a report with the plugin name and zero or more `PluginLintDiagnostic` values. Each finding has a stable code, `error` / `warning` / `info` severity, concise message, and optional configuration path. Errors make `report.valid` false; warnings and informational findings do not. Keep diagnostics safe for display: do not include secret values, full connection strings, or raw exception text.

Headless consumers call `lint_plugin_config(plugin, config)`. It returns `None` when the optional protocol is not implemented. If a linter raises or returns a malformed report, Groundworkers logs a safe error summary and returns a generic error finding.

Run the installed plugin checks from the command line:

```bash
groundworkers lint-plugins --config-path /path/to/config.toml
```

The command prints a JSON report and exits with status 1 if any linter reports an error. Plugins without the optional hook appear under `skipped`; only plugins with the hook have their settings passed through their package schema and linter. It does not build the Groundworkers runtime or connect to OMOP services. This command covers plugin configuration only; prompt packs and Groundworkers knowledge packs should be validated by their owning subsystems.


## Lifecycle and diagnostics

Expand Down
133 changes: 133 additions & 0 deletions docs/development/standard-resolution-packs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
# Proposal: per-domain standard-resolution packs

Status: draft for review (2026-10-19). Replaces the single
`core/standard-concept-preference` pack with a slim, always-loaded core
plus one pack per OMOP domain.

## Why the broad pack falls short

`standard-concept-preference` mixes two kinds of content:

1. **Invariants** that hold in every domain: the target must be
`standard_concept = 'S'`, active, never a classification (`'C'`), and
`concept_id = 0` when nothing fits.
2. **Per-domain resolution rules**, compressed to one line each, e.g.
"Procedure: SNOMED preferred, CPT4 for US billing".

The second kind is where agents actually go wrong, and one line per domain
cannot carry it. Checking the procedure domain against the live vocabulary
(v5.0 27-FEB-26) shows the one-liner is misleading:

- Standardness is decided **per code, not per vocabulary**. CPT4 has
~10.2k standard codes and ~3.8k non-standard ones that map to SNOMED,
CVX or RxNorm. OPCS4 and ICD9Proc are split roughly half and half.
- A standard source code (CPT4, ICD10PCS, standard HCPCS) **maps to
itself**. There is no "prefer SNOMED over CPT4" step: rewriting a
standard code to SNOMED discards information and is wrong.
- The vocabulary ships cross-vocabulary links that *look* like equivalences
but aren't (`CPT4 - SNOMED eq` often points to a broader concept, or to
several). An agent told only "prefer SNOMED" will follow them.
- Concept sets built from SNOMED descendants miss standard CPT4/ICD10PCS
records (e.g. CPT4 27447 total knee arthroplasty is not a descendant of
SNOMED 43531648 Total knee replacement).

None of this applies to Condition, where one standard vocabulary (SNOMED)
absorbs everything through `Maps to`. Each domain resolves to
standardness differently, so each needs its own guidance.

## Proposed structure

```
packs/core/
standard-concept-preference/ # KEEP name (always: true) — invariants only
standard-resolution-condition/ # domains: [Condition]
standard-resolution-procedure/ # domains: [Procedure] ← drafted
standard-resolution-measurement/ # domains: [Measurement]
standard-resolution-observation/ # domains: [Observation]
standard-resolution-drug/ # domains: [Drug]
standard-resolution-device/ # domains: [Device]
drug-concept-granularity/ # unchanged; see_also from -drug
```

The catalogue only discovers `packs/<layer>/<pack>/manifest.yaml`, so the
domain packs are flat siblings rather than nested children. The link
between them is `see_also` plus `applicability.domains`, which
`PackApplicability.matches()` already filters on.

### Slim core: `standard-concept-preference`

Keep the name. It is always loaded and the bundled-baseline test asserts it
exists (`tests/unit/test_knowledge_catalogue.py:218`). Contents:

- Active-target rule. Follow `Maps to` from `U`; never target `D`.
- `S` required for `*_concept_id`. `C` is never a target. Non-standard goes
in `*_source_concept_id`.
- **New:** standardness is per code. Check `standard_concept` on the
concept, never infer it from `vocabulary_id`.
- **New:** a standard source concept maps to itself. Do not re-map it to a
"preferred" vocabulary.
- **New:** `Maps to` is the only mapping edge. `eq`, `cat`, `poss_eq` and
`Is a` are evidence, not mappings.
- `concept_id = 0` fallback.
- Loading rule: "after resolving a target's `domain_id`, load
`standard-resolution-<domain>`".
- The `vocab_preference` block is removed and moves into the domain packs.

### Domain pack template

Every `standard-resolution-<domain>` pack uses the same sections, so an
agent knows where to look:

| Section | Purpose |
|---|---|
| 1. Standard vocabularies in this domain | Which vocabularies hold `S` concepts here, and roughly what share. Snapshot counts carry a `verified_against` vocabulary version. |
| 2. Resolution path by input type | (a) code from a standard vocabulary, (b) non-standard code, (c) free text. Each has its own path. |
| 3. Relationship semantics | Which relationship_ids are mappings, which are evidence, and the known failure modes of each. |
| 4. Concept-set implications | Whether descendants of one vocabulary cover the others. If not, how to build a set that does. |
| 5. Traps | Concrete patterns the vocabulary gets wrong, with concept IDs. |
| 6. Verification checklist | What to check before accepting a target. |

`rules.yaml` carries the machine-readable half under the same keys:
`standard_vocabularies`, `resolution_paths`, `relationship_semantics`,
`concept_set_roots`, `traps`, `checks`. Worked cases go in
`examples.yaml`, which `PackContent` already loads.

### What each domain pack must cover (outline)

- **Condition:** SNOMED is the only standard vocabulary. ICD10CM/ICD9CM/ICD10
reach it via `Maps to`, sometimes 1:n (see `one-to-many-mapping`). Covers
`Maps to value` for pre-coordinated codes (see `pre-coordinated-split`),
Cancer Modifier / ICDO3 for oncology, and the domain-change trap (an
ICD10CM code mapping into Observation).
- **Procedure:** see the draft. Multiple standard vocabularies, self-maps,
`eq`/`cat` semantics, attribute-based alignment, concept sets with
several roots.
- **Measurement:** LOINC and SNOMED are both standard. LOINC wins for
lab tests. Covers CPT4 lab codes (1.3k standard CPT4 Measurement codes,
plus `CPT4 - LOINC eq`) and the test+result split.
- **Observation:** SNOMED / LOINC / CPT4 E&M / HCPCS. Covers history-of
(see `history-of-event`) and E&M visit codes landing in Observation.
- **Drug:** RxNorm before RxNorm Extension. Covers HCPCS J-codes and CPT4
vaccine/drug codes that are non-standard and map to RxNorm/CVX.
Granularity is delegated to `drug-concept-granularity`.
- **Device:** HCPCS Level II is the main standard source (5.2k S). Covers
SNOMED physical objects and the Procedure/Device split for
implant-plus-procedure codes.

## Mechanism note

`post-filter` is declared in manifests, but nothing reads `rules.yaml`
programmatically today. `knowledge_pack` only returns it in the payload
(`tools/knowledge_tools.py:189`). The domain packs keep `rules.yaml`
declarative so a future post-filter can use it. The guidance is written to
stand on its own when it is context-injected.

## Migration

1. Land `standard-resolution-procedure`. It adds content and changes
nothing existing.
2. Draft the remaining domain packs from the template.
3. Slim `standard-concept-preference` to the invariants and add the
loading rule. Bump it to `version: "2.0"`.
4. Mirror into `gw-plugins/kb-prompts/packs/core/`, whose
`standard-concept-preference` is currently byte-identical.
Loading
Loading