Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
2f2d6a2
test(OGC-1054): define profile control recognition runtime
pmanko Aug 22, 2026
d1033db
feat(OGC-1054): make control recognition profile-owned
pmanko Aug 22, 2026
4d8a064
test(OGC-1054): define recognition summary contract
pmanko Aug 22, 2026
364ea5c
feat(OGC-1054): expose control recognition summaries
pmanko Aug 22, 2026
2e5c89b
test(OGC-1054): require unique profile identities
pmanko Aug 22, 2026
f584b88
feat(OGC-1054): reject duplicate profile identities
pmanko Aug 22, 2026
1365814
test(OGC-1054): require recognition fingerprint summary
pmanko Aug 22, 2026
1151250
feat(OGC-1054): expose recognition fingerprints
pmanko Aug 22, 2026
db905ee
test(OGC-1054): define safe recognition authoring
pmanko Aug 22, 2026
79fa352
feat(OGC-1054): project safe recognition authoring
pmanko Aug 22, 2026
25cb812
test(OGC-1054): require recognition draft endpoints
pmanko Aug 22, 2026
558f433
test(OGC-1054): hide raw recognition fields
pmanko Aug 22, 2026
cd8d150
feat(OGC-1054): author recognition through profile drafts
pmanko Aug 22, 2026
01fff7b
test(OGC-1054): reject implicit control recognition
pmanko Aug 22, 2026
745f735
test(OGC-1054): reject invalid recognition runtime
pmanko Aug 22, 2026
dd5d704
feat(OGC-1054): require explicit control recognition
pmanko Aug 22, 2026
db5a40c
test(OGC-1054): expose recognition semantics
pmanko Aug 22, 2026
30382e0
feat(OGC-1054): expose recognition semantics
pmanko Aug 22, 2026
003b2b4
test(OGC-1054): require recognition catalog semantics
pmanko Aug 22, 2026
64d1b89
feat(OGC-1054): version recognition catalog semantics
pmanko Aug 22, 2026
30a3774
test(OGC-1054): isolate ASTM listener lifecycle
pmanko Aug 22, 2026
3396a4b
fix(OGC-1054): validate profile recognition patterns
pmanko Aug 22, 2026
a5a5f7e
refactor(OGC-1054): make ASTM result selection profile-owned
pmanko Aug 22, 2026
d826d6a
refactor(OGC-1054): make FILE parsing profile-owned
pmanko Aug 22, 2026
adf250f
Merge corrected OGC-1054 M1 into Bridge M2
pmanko Aug 25, 2026
3f1de23
Merge corrected OGC-1054 profile contract into Bridge M2
pmanko Aug 25, 2026
26d6536
ci(OGC-1054): pin corrected analyzer mock
pmanko Aug 25, 2026
445af81
Merge corrected Bridge M1 CI into M2
pmanko Aug 25, 2026
f53525f
Merge branch 'codex/ogc-1054-m1-profile-lifecycle' into codex/ogc-105…
pmanko Aug 26, 2026
b9a9b45
Merge branch 'codex/ogc-1054-m1-profile-lifecycle' into codex/ogc-105…
pmanko Aug 26, 2026
af0b8ea
fix(profiles): enforce explicit ASTM result selection
pmanko Aug 30, 2026
0b498c5
ci(profiles): pin matching M2 mock revisions
pmanko Aug 30, 2026
1deb85e
Merge branch 'codex/ogc-1054-m1-profile-lifecycle' into codex/ogc-105…
pmanko Aug 31, 2026
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
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ jobs:
uses: actions/checkout@v4
with:
repository: DIGI-UW/analyzer-mock-server
ref: 504ffec81a5b93f6be91231051e8fe0720f61eb1
ref: b56fc7f8b93b0912894ffcebaeb3bde61e32864b
path: analyzer-mock-server

- name: Set up JDK 21
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ Runtime configuration is read from `configuration.yml` (mounted into container a
| `org.itech.ahb.forward-http-server.max-attempts` | Outbound retry attempts | 3 |
| `org.itech.ahb.forward-http-server.backoff-ms` | Initial outbound retry backoff in ms | 1000 |
| **ASTM TCP** | | |
| `org.itech.ahb.astm.enabled` | Enable ASTM TCP listeners | true |
| `org.itech.ahb.listen-astm-server.port` | ASTM LIS1-A listen port | 12001 |
| `org.itech.ahb.listen-astm-e1381-95-server.port` | E1381-95 listen port | 12011 |
| **MLLP (HL7)** | | |
Expand Down
5 changes: 4 additions & 1 deletion configuration.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ bridge:

# File watcher configuration (M4)
# Runtime ownership note (014 remediation): Bridge is the active FILE watcher owner.
# OpenELIS app-side poller is fallback-only and disabled by default.
# OpenELIS configures and registers FILE analyzers but does not watch directories.
file:
enabled: false # Bootstrap default; watcher is activated by FILE registration lifecycle during remediation
watchDirectories:
Expand Down Expand Up @@ -113,6 +113,9 @@ org:
max-attempts: 3
backoff-ms: 1000

astm:
enabled: true

# ASTM LIS1-A Server - Listens for ASTM messages from analyzers
listen-astm-server:
port: 12001
Expand Down
53 changes: 52 additions & 1 deletion contracts/analyzer/v1/analyzer-profile.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,12 @@
]
}
},
"result_value_order": {
"type": "array",
"minItems": 1,
"uniqueItems": true,
"items": { "enum": ["result", "ctValue", "interpretation"] }
},
"sheet_detection": { "$ref": "#/$defs/sheetDetection" },
"default_test_mappings": {
"type": "array",
Expand Down Expand Up @@ -125,7 +131,7 @@
"minItems": 1,
"items": { "$ref": "#/$defs/testMapping" }
},
"configDefaults": { "$ref": "#/$defs/socketDefaults" },
"configDefaults": { "$ref": "#/$defs/astmSocketDefaults" },
"controlResultRecognition": {
"$ref": "#/$defs/astmControlResultRecognition"
}
Expand Down Expand Up @@ -385,6 +391,51 @@
"transforms": { "type": "object" }
}
},
"astmSocketDefaults": {
"allOf": [
{ "$ref": "#/$defs/socketDefaults" },
{
"required": ["extractionOverrides"],
"properties": {
"extractionOverrides": {
"$ref": "#/$defs/astmExtractionOverrides"
}
}
}
]
},
"astmExtractionOverrides": {
"type": "object",
"additionalProperties": false,
"required": ["resultRecordSelection"],
"properties": {
"resultRecordSelection": {
"$ref": "#/$defs/astmResultRecordSelection"
}
}
},
"astmResultRecordSelection": {
"oneOf": [
{
"type": "object",
"additionalProperties": false,
"required": ["mode"],
"properties": { "mode": { "const": "ALL" } }
},
{
"type": "object",
"additionalProperties": false,
"required": ["mode", "targetField"],
"properties": {
"mode": { "const": "FIELD_NON_BLANK" },
"targetField": {
"type": "string",
"pattern": "^R\\.[1-9][0-9]*\\.[1-9][0-9]*$"
}
}
}
]
},
"fileDefaults": {
"type": "object",
"additionalProperties": false,
Expand Down
9 changes: 7 additions & 2 deletions contracts/analyzer/v1/fixtures/analyzer-profile-astm.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,12 @@
"connectionRole": "SERVER",
"transport": "TCP/IP",
"aggregationMode": "PER_MESSAGE",
"extractionOverrides": {},
"extractionOverrides": {
"resultRecordSelection": {
"mode": "FIELD_NON_BLANK",
"targetField": "R.3.5"
}
},
"flagMappings": {},
"transforms": {}
},
Expand Down Expand Up @@ -139,7 +144,7 @@
},
"catalog": {
"revision": 1,
"revisionFingerprint": "sha256:5184c52a44ec58932116fb3c4e9495b6cd8f05e4e84916f183f57b428a24e4ee",
"revisionFingerprint": "sha256:c2168ff7438aaea72eb4dcd646e2348601899b4b401b3c235fae07b5804081b2",
"recognitionFingerprint": "sha256:68a4cf6c695d3a23662a7a5dc97983bbc78e38726f932517d09238d47aad701e",
"source": "SHIPPED",
"status": "ACTIVE",
Expand Down
3 changes: 2 additions & 1 deletion contracts/analyzer/v1/fixtures/analyzer-profile-file.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
"testCode": "testCode",
"WellPosition": "position"
},
"result_value_order": ["result", "interpretation"],
"default_test_mappings": [
{
"test_code": "VIH-1",
Expand Down Expand Up @@ -94,7 +95,7 @@
},
"catalog": {
"revision": 1,
"revisionFingerprint": "sha256:8d099084227b7de083a6f8f0511234c8f09540534182a380060fe921a7f28c21",
"revisionFingerprint": "sha256:a39955225875ee8cc4ae2a5a0c587acd5c9e376749485ef48ef1eca2cefcf770",
"recognitionFingerprint": "sha256:5c0de42e1135b772b23e4c1ea04b0162b30fe36fc08ecef76a70108ba031be26",
"source": "SHIPPED",
"status": "ACTIVE",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
"Well Position": "position",
"Task": "qcTask"
},
"result_value_order": ["result", "ctValue"],
"sheet_detection": {
"strategy": "header_scan",
"preferred_sheet_names": ["Results"],
Expand Down Expand Up @@ -100,7 +101,7 @@
},
"catalog": {
"revision": 1,
"revisionFingerprint": "sha256:b940cb5cc7191a44570a87326e7e5c2054f4ac6df42cdf653ae113b9df143e6e",
"revisionFingerprint": "sha256:8b0d72f2b60dbfa2b025599a796a83c89fe2fcca3d329fbde3400f7a1434896e",
"recognitionFingerprint": "sha256:e5c668abfd8b64220efeff01dd57682dcbd883f2652d68d187505bbd4ac707ab",
"source": "SHIPPED",
"status": "ACTIVE",
Expand Down
46 changes: 45 additions & 1 deletion contracts/analyzer/v1/fixtures/profile-catalog-response.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"testCode": "testCode",
"WellPosition": "position"
},
"result_value_order": ["result", "interpretation"],
"default_test_mappings": [
{
"test_code": "VIH-1",
Expand Down Expand Up @@ -99,7 +100,7 @@
},
"catalog": {
"revision": 1,
"revisionFingerprint": "sha256:8d099084227b7de083a6f8f0511234c8f09540534182a380060fe921a7f28c21",
"revisionFingerprint": "sha256:a39955225875ee8cc4ae2a5a0c587acd5c9e376749485ef48ef1eca2cefcf770",
"recognitionFingerprint": "sha256:5c0de42e1135b772b23e4c1ea04b0162b30fe36fc08ecef76a70108ba031be26",
"source": "SHIPPED",
"status": "ACTIVE",
Expand All @@ -111,6 +112,49 @@
"action": "SHIPPED",
"actor": "distribution",
"markedAt": "2026-08-19T00:00:00Z"
},
"controlRecognitionSummary": {
"recognitionFingerprint": "sha256:5c0de42e1135b772b23e4c1ea04b0162b30fe36fc08ecef76a70108ba031be26",
"mode": "RULES",
"description": "Any listed condition identifies a control result.",
"affirmedNoControlResults": false,
"conditions": [
{
"key": "positive-control-task",
"kind": "FIELD_VALUE_EQUALS",
"sourceLabel": "Control task",
"value": "Positive",
"description": "Control task equals Positive"
},
{
"key": "negative-control-task",
"kind": "FIELD_VALUE_EQUALS",
"sourceLabel": "Control task",
"value": "Negative",
"description": "Control task equals Negative"
},
{
"key": "standard-control-task",
"kind": "FIELD_VALUE_EQUALS",
"sourceLabel": "Control task",
"value": "Standard",
"description": "Control task equals Standard"
},
{
"key": "positive-control-prefix",
"kind": "SPECIMEN_ID_STARTS_WITH",
"sourceLabel": "Specimen ID",
"value": "C+",
"description": "Specimen ID starts with C+"
},
{
"key": "negative-control-prefix",
"kind": "SPECIMEN_ID_STARTS_WITH",
"sourceLabel": "Specimen ID",
"value": "C-",
"description": "Specimen ID starts with C-"
}
]
}
}
]
Expand Down
79 changes: 78 additions & 1 deletion contracts/analyzer/v1/profile-catalog-entry.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"title": "OpenELIS Analyzer Bridge profile catalog entry v1",
"type": "object",
"additionalProperties": false,
"required": ["profile", "publication"],
"required": ["profile", "publication", "controlRecognitionSummary"],
"properties": {
"profile": {
"$ref": "https://openelis-global.org/contracts/analyzer/v1/analyzer-profile.schema.json"
Expand All @@ -27,6 +27,83 @@
"actor": { "type": "string", "minLength": 1 },
"markedAt": { "type": "string", "format": "date-time" }
}
},
"controlRecognitionSummary": {
"type": "object",
"additionalProperties": false,
"required": ["recognitionFingerprint", "mode", "description", "affirmedNoControlResults", "conditions"],
"properties": {
"recognitionFingerprint": {
"type": "string",
"pattern": "^sha256:[0-9a-f]{64}$"
},
"mode": { "enum": ["RULES", "NONE"] },
"description": { "type": "string", "minLength": 1 },
"affirmedNoControlResults": { "type": "boolean" },
"conditions": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"required": ["key", "kind", "sourceLabel", "value", "description"],
"properties": {
"key": { "type": "string", "minLength": 1 },
"kind": {
"enum": [
"SPECIMEN_ID_STARTS_WITH",
"CONFIGURED_SPECIMEN_ID_PATTERN",
"FIELD_VALUE_EQUALS",
"FIELD_VALUE_CONTAINS"
]
},
"sourceLabel": { "type": "string", "minLength": 1 },
"value": { "type": ["string", "null"] },
"description": { "type": "string", "minLength": 1 },
"controlLevel": { "type": ["string", "null"] },
"controlType": { "type": ["string", "null"] }
},
"allOf": [
{
"if": {
"properties": {
"kind": { "const": "CONFIGURED_SPECIMEN_ID_PATTERN" }
}
},
"then": {
"properties": {
"value": { "type": "null" }
}
},
"else": {
"properties": {
"value": { "type": "string", "minLength": 1 }
}
}
}
]
}
}
},
"allOf": [
{
"if": { "properties": { "mode": { "const": "RULES" } } },
"then": {
"properties": {
"affirmedNoControlResults": { "const": false },
"conditions": { "minItems": 1 }
}
}
},
{
"if": { "properties": { "mode": { "const": "NONE" } } },
"then": {
"properties": {
"affirmedNoControlResults": { "const": true },
"conditions": { "maxItems": 0 }
}
}
}
]
}
}
}
21 changes: 14 additions & 7 deletions src/main/java/org/itech/ahb/config/AnalyzerRegistryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@
import lombok.Data;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.itech.ahb.fhir.TabularFileLayout;
import org.itech.ahb.profile.AstmResultRecordSelection;
import org.itech.ahb.profile.ControlResultRecognition;
import org.itech.ahb.profile.TabularResultValueSelection;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

Expand Down Expand Up @@ -264,6 +268,12 @@ public void setIdentifierPattern(String identifierPattern) {
/** Number of metadata rows to skip before header detection (default 0). */
private int skipRows;

/** Profile-owned instructions for locating the tabular result header. */
private TabularFileLayout tabularFileLayout;

/** Profile-owned precedence for selecting a reportable tabular value. */
private TabularResultValueSelection tabularResultValueSelection;

/**
* Profile-derived file-wide analyzer test code for FILE exports that do
* not carry a row-level {@code testCode}. This is materialized only when
Expand All @@ -281,14 +291,11 @@ public void setIdentifierPattern(String identifierPattern) {
/** OE test codes this analyzer is allowed to emit (whitelist, not a default). */
private Set<String> mappedTestCodes = Collections.emptySet();

/** Control-result recognition rules from the pinned Bridge profile. */
private java.util.List<org.itech.ahb.qc.QcRule> qcRules = new java.util.ArrayList<>();
/** Complete control-result recognition from the pinned Bridge profile. */
private ControlResultRecognition controlResultRecognition;

/**
* Transitional parser input. The registration contract never populates
* operational control lots; target recognition comes only from the profile.
*/
private java.util.List<org.itech.ahb.qc.ControlLotDto> controlLots = new java.util.ArrayList<>();
/** Profile-owned selection of ASTM R records that carry reportable results. */
private AstmResultRecordSelection astmResultRecordSelection;

/**
* Analyzer test_code → LOINC mapping materialized from the pinned profile's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.itech.ahb.lib.astm.servlet.ASTMServlet;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

/**
* Component for triggering an ASTM server runner for each ASTM transmission servlet.
*/
@Component
@ConditionalOnProperty(name = "org.itech.ahb.astm.enabled", havingValue = "true", matchIfMissing = true)
@Slf4j
public class ASTMServerRunnerTrigger {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,12 @@ public ResponseEntity<Map<String, Object>> scanFile(
Files.write(tempFile, file.getBytes(),
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING);

ScanResult scanResult = scanner.scan(tempFile, columnMappings, allowedCodes, getSynonyms(entry));
ScanResult scanResult = scanner.scan(
tempFile,
columnMappings,
entry.getTabularResultValueSelection(),
allowedCodes,
getSynonyms(entry));
if (scanResult instanceof ScanResult.SelfDeclared selfDeclared) {
response.put("suggestion", selfDeclared.testCode());
response.put("confidence", "selfDeclared");
Expand Down
Loading
Loading