Registration surface
A pipe kind is not one class. Adding PipeFoo means touching a handful of separate places, none of which the others infer — there is no plugin registry for pipe kinds and no scan of the tree. This page is the checklist, and the explanation of which of those places are genuinely load-bearing versus which look like duplication and are not.
What a pipe kind is made of
Each kind lives in its own package under pipelex/pipe_operators/<kind>/ or pipelex/pipe_controllers/<kind>/, and holds three classes:
| file | class | role |
|---|---|---|
pipe_foo_blueprint.py |
PipeFooBlueprint |
the MTHDS language surface — what a .mthds file parses into |
pipe_foo.py |
PipeFoo |
the runnable pipe |
pipe_foo_factory.py |
PipeFooFactory |
blueprint → pipe |
PipeSignature follows the same shape from pipelex/pipe_signature/, with the caveat noted below.
The checklist
-
The kind's package — the three classes above, plus an
exceptions.pyif the kind raises its own errors (see the error-class location convention in the Python standards). -
The type tag — add the value to
PipeTypeinpipelex/pipe_machinery/pipe_blueprint.py, and give it an arm in thecategoryproperty that maps it toPipeCategory.PIPE_OPERATORorPipeCategory.PIPE_CONTROLLER. The match is exhaustive with nocase _, so the type checker walks you to every remaining site — that is the point of writing it that way. -
The blueprint union — add
PipeFooBlueprinttoPipeBlueprintUnioninpipelex/mthds_parsing/pipelex_bundle_blueprint.py. It is a pydantic discriminated union keyed on thetypefield, so a kind absent from the union is a parse error on any.mthdsfile that names it, not a silent skip. -
The registration manifest — add
PipeFooandPipeFooFactorytoPipeRegistryModelsinpipelex/pipe_machinery/registry_models.py, inPIPE_OPERATORS/PIPE_OPERATORS_FACTORYorPIPE_CONTROLLERS/PIPE_CONTROLLERS_FACTORY. This is the one whose omission is silent: boot succeeds either way, and the failure surfaces later as a kajson deserialization error on a pipe.tests/unit/pipelex/test_registry_models_split.pypins the manifests against the class registry for exactly that reason. -
The spec layer — add
PipeFooSpecunderpipelex/builder/pipe/, then register it in bothpipe_spec_map.py(pipe_type_to_spec_class, the authoring lookup) andpipe_spec_union.py(PipeSpecUnion, the discriminated union). The two are separate on purpose: the union carriesPipeSignatureSpec, the map does not, because a signature is not a user-selectable type.
Why core/ no longer holds the manifest
PipeRegistryModels used to live in pipelex/core/registry_models.py alongside core's value model. Importing it loads every pipe operator, every pipe controller and their factories — so a module filed under core/ pulled in the whole interpreter, and core read as a dependant of the pipe packages it is supposed to sit below. The manifest now lives in pipelex/pipe_machinery/; core.registry_models keeps CoreRegistryModels (the stuff-content classes) and imports nothing from pipe_operators / pipe_controllers / pipe_signature.
The two manifests land in one registry from the two halves of the boot: CoreRegistryModels is registered by RuntimeBoot.setup in pipelex/runtime_boot.py, PipeRegistryModels by Pipelex.setup in pipelex/pipelex.py — see Where the boot splits. They must stay disjoint — a class in both is a sign one half was edited without the other — and because registration is name-keyed dict insertion, which side registers first carries no meaning.
See Hub Layering for the layer boundary this split serves.
The spec/blueprint parallel is deliberate
Steps 3 and 5 look like the same list written twice. They are not, and collapsing them would be a mistake:
- Blueprints are the MTHDS language.
PipeFooBlueprintdefines what an author may write in a.mthdsfile. It is the reference for the standard. - Specs are an authoring convenience for AI agents.
PipeFooSpecis a simplified shape with convenience fields thatto_blueprint()transforms into blueprint fields. Spec-level fields deliberately differ from blueprint-level ones —PipeComposeSpec.target_formatis a spec convenience with no blueprint counterpart, for instance.
The two layers evolve for different reasons and answer to different consumers, so they carry their own unions. When adding validation or a field, decide which layer owns it first: language rules go on blueprints, authoring convenience goes on specs. pipelex/builder/CLAUDE.md is the canonical statement of that split.