ADR-009: Place the reviewed science below the stages
| Status | 🟢 Accepted |
| Created | 2026-10-06 |
| Last Updated | 2026-10-09 |
| Deciders | Gemma Danks |
| Tags | layering, dependencies, architecture tests |
Context
The documented direction was
adapters → pipeline/public_api → science → stages → algorithms: science/
was described as composing the stages into the reviewed pipeline. The code
did not work that way. public_api.py runs the stages in order, and
science/ holds what the stages apply: the reviewed profile and
configuration, the composition records (models.py, catalogue_rows.py)
and the catalogue-row kernels (catalogues.py). On 6 October 2026 the
static import graph, counting function-level and TYPE_CHECKING imports,
ran both ways between the two packages:
- four imports from
stages/intoscience/:stages/catalogue_rows.pytakes the rows and the row kernels, andstages/islands.pythe row kernels and the island records; and - one import from
science/intostages/:science/configuration.pybuilt the detection stage'sDetectionStageConfig, which was defined instages/detection.py.
Apart from io/__init__.py re-exporting one astrometry function, no other
edge contradicted the documented layering. The architecture test had import
rules for algorithms/, data_models/, io/, config.py and
pipeline.py, and kept hebog.validation out of the public composition
only, so it could not see the cycle.
Problem Statement
The two packages imported each other, so no direction described the code. Which direction should the layering state, and what has to move for the code to follow it?
Options Considered
| Option | Description | One direction | Moves | Modules where readers expect them | Quick-check cost | Overall score |
|---|---|---|---|---|---|---|
| Weight | - | 3 | 2 | 2 | 1 | - |
| B: redraw the direction | public_api → stages → science → algorithms; move DetectionStageConfig into config.py beside the other per-stage configurations |
✅ | ✅ | ✅ | ✅ | 24 |
| C: keep the direction with exemptions | Exempt the four stage imports of science/ |
❌ | ✅ | ✅ | ✅ | 18 |
| A: move the science below the stages | Move the rows, the row kernels and the records into data_models/ and algorithms/ |
✅ | ❌ | ⚠️ | ❌ | 16 |
✅ = 3 (good), ⚠️ = 2 (acceptable), ❌ = 1 (poor)
Option A moves three modules. science/models.py imports two algorithm
modules, and algorithms/ imports data_models/, so the records cannot
join data_models/ without a cycle; the 1,734 lines of row kernels would
join algorithms/ under a name that no longer says they are the reviewed
catalogue. Moving science/catalogue_rows.py also changes the code that
identifies the quick check's cached PyBDSF references, so every reference
would run again once. After all that, science/ would hold only the
configuration and profile, and would still not compose the stages.
Option C keeps a cycle between two packages, so the table would describe no direction at all.
Decision Outcome
We will use option B. The direction is
adapters → pipeline/public_api → stages → science → algorithms, with
public_api → public_science → science, executors/ and io/ used by
stages/ and public_api.py, and data_models/ and config.py shared by
every layer. science/ is the reviewed science that the stages apply, and
imports no stage, executor or io module. DetectionStageConfig moved from
stages/detection.py to config.py, where the other per-stage
configurations already were. No module moved, and the products are
byte-identical.
Consequences
- Good, because the documented direction describes what the code does, and
names one place where the stages are composed:
public_api.pywhen this was decided, andstages/composition.pysince the sequence moved below the public boundary on 9 October 2026, within the same layering. - Good, because one class moved: no module path changed, and the quick check's PyBDSF references stay valid.
- Good, because
config.pynow holds every per-stage configuration. - Bad, because the name
science/no longer says where it sits: readers must learn that it is below the stages, not above them. - Risk: a catalogue-row or record change that needs a stage would reverse
the edge again. The table rejects that import; move the stage-facing part
into
stages/instead.
Confirmation
LAYER_IMPORTS in tests/unit/test_architecture.py states, as one table,
the layers every layer of hebog may import, counting every import
statement in any scope. The test requires the table to name every layer,
to form no cycle, and to keep science below stages and stages below
public_api. An import outside the table needs a named exemption with its
reason, and each exemption must still match an import the table rejects.
The same test keeps hebog.validation out of every production layer,
Rapthor, Prefect and LSMTool out of every module, and Dask inside
executors/, and keeps the imports that load public_api.py and the Dask
executor deferred.
Links
| Type | Links |
|---|---|
| ADRs | ADR-004, ADR-006, ADR-008 |
| Documentation | Architecture overview, Quality attributes |
| Plan | Implementation plan |