Sample-annotation architecture
Public object model
parser = AnnotationCompiler(...).compile(annotation_source)
annotation = parser.parse(parsed)
result = annotation.annotate()
The compiler owns generic delimited-source loading. The source-bound parser owns source
interpretation, matching, and policy validation. A concrete Annotation is created only when it is
valid for the supplied dataset, and therefore stores that ParsedLevels and its completed
AnnotationMatches. Application cannot accidentally combine evidence from one dataset with
another.
Parsing, matching, policy validation, and application remain separate internal functions and types. They are folded together only at the public construction boundary; matching is not hidden in a dataclass initializer.
Values and behavior
AnnotationTable, AnnotationCoverage, LevelAnnotationMatch, AnnotationMatches, and
AnnotationResult are frozen data values. They select no behavior. Runtime policies implement
keep, complete-coverage, or selection behavior, while concrete source packages own their
convention-specific diagnostic semantics.
prolfquapp validity depends on the configured application: keep accepts partial coverage, complete rejects it, and selection validates its Boolean fields before construction. Every policy rejects a level with zero matches because such an annotation is not an annotation for that dataset.
Dependency direction
apb2/
├── api.py AnnotationCompiler, AnnotationError, SdrfSource
├── cli/annotation.py `apb2 annotate`: result I/O + orchestration
└── annotation/
├── compiler.py generic delimited-source composition
├── prolfquapp.py source-bound parser + bound annotation
├── sdrf.py SDRF source view, source-bound parser + bound annotation
├── source/ CSV/TSV decoding
├── application/ retention and selection behavior
├── matching/ exact/fuzzy matching
└── data/ innermost values and errors
Modules directly in annotation/ compose children. The child packages import only the innermost
data package. Annotation computation depends on the ParsedLevels value model but never on result
I/O, Pydantic rule documents, pandas, AnnData, or MuData. cli/annotation.py is the outer adapter
that reads and writes physical results.
External packages use apb2.api.AnnotationCompiler like any other caller; apb-proteobench compiles its module samples as a prolfquapp table with unmatched="error". SdrfSource.read(path) gives them SDRF header lookup and data-file basenames without a second SDRF reader.
The optional rule declaration sample_annotation.matching is a Pydantic storage schema. Parser V2
projects its exact/fuzzy mode and optional key normalization into JSON-compatible level provenance.
Annotation matching constructs its runtime matcher from that persisted value and never imports the
vendor-rule package. Normalization changes comparison keys only; collision checks and diagnostic
evidence remain owned by matching.
Storage behavior
Annotation follows the APB metadata specification. It adds columns to each level's obs, records one source descriptor and configuration in ParsedLevels.metadata[tool]["provenance"]["annotation"], and records coverage, corrections and bounded mismatch evidence in ParsedLevel.metadata[tool]["result"]["annotation"], with its summary in ParsedLevel.metadata[tool]["summary"]. The tool key identifies the convention; there is no parallel metadata["annotation"] namespace or repeated convention alias.
Parse provenance remains in each owner's uns. Result writers project the tool namespaces into the owning root or level part; the parts are never merged. Recording annotation preserves unrelated operations in the same tool namespace; no backend-specific or tool-name-specific annotation implementation exists.