Built-in Material Separation Rulebook
Status: current built-in provider specification
Scope: official built-in scientific-model provider for every sep program, component fate, state transition, composition update, and repeated execution
PM coordination: culsma/culsma-pm#93 Extension contract: Scientific Model Extension Contract
provider_id: culsma.builtin.material_rulebook
provider_version: 1.0
capabilities:
material.separation_fate: 1.0
material.state_transition: 1.0Rulebook Format
This document specifies the official built-in material provider. Its three tables are that provider's complete rule surface:
separation-fate path: Table 1 Classify → Table 2 Resolve Fractions → Table 3 Transition State
state-transition path: Table 1 Classify ─────────────────────────→ Table 3 Transition StateEvery registered sep program enters one decision-and-commit cycle. When no valid authored decision exists, the selected provider supplies the typed decision; the default binding is this built-in Rulebook:
(M_t, OP_t) → ValidatedAuthorDecision ?? SelectedProviderDecision → D_t → Apply(M_t, D_t) → C_t → ValidateCommit(C_t) → M_(t+1)
The next operation restarts with Resolve(M_(t+1), op_(t+1)).
Closed Symbols
c = (content_ref, kind, type, quantity, relation, association_target, label)
M_t = committed component collections before operation t
G = calculation group
OP = operation contract
R = persistent physical relationship
A = typed relationship target: container(id) or component_entry(id)
E = declared operation effect
O = semantic output role
X = closed typed context predicates
F = ordered output fractions
L = operation-local output label
UNRESOLVED = selected provider cannot resolve the typed decisionProvider Binding
material.separation_fate → culsma.builtin.material_rulebook (default)
material.state_transition → culsma.builtin.material_rulebook (default)The built-in Rulebook is the default provider at the scientific-model extension boundary, not a second calculation layer after provider selection. A configured external provider may replace it or explicitly compose it as a fallback. Replacement and fallback order are runtime configuration and provenance; no implicit provider chain is permitted.
All providers return the same tagged decision contracts. Providers do not mutate runtime state, commit candidates, or bypass kernel validation.
D_t is a tagged decision rather than an always-fractional result:
SeparationDecision {
kind: separation_fate,
fractions,
decision_source,
provenance
}
StateTransitionDecision {
kind: state_transition,
transitions,
decision_source,
provenance
}Registered sep program dispatch is closed:
separation_fate:
centrifuge
filtration
centrifugal_filtration
precipitation
magnetic
phase_partition
field
generic sep
state_transition:
disrupt
cross-container material moveA future program may declare a combined decision, but the decision-selection step must not infer one effect kind from free text.
content_ref is the stable reference to one declared content-registry entry, such as HEK293T, PBS1, or RAB7DNA. It identifies what the entry contains and is the first eligibility check for compression; it is not a database ID, container ID, material-instance ID, or canonical type. One material may contain more than one component entry with the same content_ref when their physical states cannot be represented by one shared state.
Component Merge Vocabulary
component: one entry in a material's component collection. Rule lookup runs once per entry, not once per distinctcontent_ref.content_ref: the declared content name referenced by that entry.quantity: one value and unit, such as300 uL,2 mg, or200000 cells.- Compatible quantities: quantities whose units describe the same kind of quantity and can be converted before addition, such as
uL + mL.cells + uLis not compatible. relation: the component's current physical state, such asfree,container_surface,pellet, orbead_bound.association_target: the typed physical object required by a relationship. Container-local relations usecontainer(id); material-bound relations usecomponent_entry(id).freehas no target.preservation: the declared condition required to keep a relationship valid, such as remaining on the same surface or under the same field.label: the name of the current operation output, such assupernatant_output; it expires when that output scope no longer applies.
Component Merge Flow
flowchart TD
A[Input component A and component B] --> B{Same content_ref?}
B -- No --> C[Keep two separate component entries]
C --> C1[Apply later operation rules to each entry independently]
B -- Yes --> D{Do both quantities use compatible units?<br/>Example: uL and mL}
D -- No --> X[REJECT<br/>Do not change committed material]
D -- Yes --> E[Apply the current operation's state change<br/>to A and B separately]
E --> F{Are both resulting states free/mobile?}
F -- Yes --> G[Merge as free]
F -- No --> H{Same physical-state name?}
H -- No --> C
H -- Yes --> I{Does this state refer to<br/>a surface, bead, membrane, or field?}
I -- No --> J{Are its preservation conditions compatible?}
I -- Yes --> K{Same associated physical object?}
K -- No --> C
K -- Yes --> J
J -- No --> C
J -- Yes --> L[Preserve the common physical state]
G --> M{Does the old output label<br/>still describe the merged result?}
L --> M
M -- Yes --> N[Keep the label]
M -- No --> O[Clear the label]
N --> P[Convert compatible units and add quantities]
O --> P
P --> Q[Return one merged component]The diagram distinguishes mixing from compression. Keep two separate component entries is a valid mixed-material result, including when both entries share one content_ref. REJECT is reserved for invalid data or an operation whose required state transition is not defined.
G = {
mobile_phase,
sedimentable_material,
capture_support,
context_dependent_target,
composite_or_unknown
}
R = {
free,
container_surface,
pellet,
precipitate,
disrupted,
bead_bound,
membrane_bound,
cell_bound,
field_retained,
unresolved
}Decision-Table Semantics
- Rows are evaluated by ascending priority; the first matching row wins.
*matches every value in that column.{a, b}is a closed set match.Xpredicates must be declared typed facts; free-text inference is forbidden.free_phase_passes(c, OP)is true only when the operation contract explicitly routes everyfreeentry to the pass-through output; built-inadherent_cell_surfaceaspiration declares this fact.- Every table has an exhaustive final row.
- Every registered
sepprogram must dispatch toseparation_fate,state_transition, or an explicitly declared combined decision; no program bypasses the candidate transaction. UNRESOLVEDmeans the selected provider lacks sufficient scientific facts to return a typed decision.KEEP_SEPARATEmeans mixing succeeds but two component entries cannot be compressed into one without losing state.REJECTleaves committed material unchanged.labelnever participates in canonical classification.
Closure Outcomes
The rulebook is closed when every component entry and operation produces one controlled outcome:
RESOLVED = exact or authored/provider fractions and a valid next state
UNRESOLVED = scientific facts are insufficient; no fraction is guessed
KEEP_SEPARATE = the material is valid, but two entries cannot share one state
REJECT = the input or requested transition is structurally invalidThese outcomes distinguish three different problem classes:
- An implementation defect violates a resolved rule, for example leaking
retentate_outputinto later classification or copying a container-surface state across containers. - A representation gap occurs when valid material contains multiple states for one
content_refbut an implementation can store only one entry. Such an implementation must report unsupported representation; it must not collapse the states. - Scientific uncertainty occurs when required experimental facts are absent. It resolves to
UNRESOLVED, not to a guessed default.
Table 1. Classification Rules
G(c) = first_match(kind, type). The canonical kind/type remains unchanged and is reclassified on every operation.
| Rule / priority | Canonical selector | Result G |
|---|---|---|
C10 / 10 | bio_fluid/{plasma, serum, urine, saliva, lymph, cerebrospinal_fluid, tears, semen, ascites, synovial_fluid, bronchoalveolar_lavage_fluid} | mobile_phase |
C11 / 10 | chemical/{solvent, organic_compound, inorganic_compound, detergent, dye} | mobile_phase |
C12 / 10 | formulation/{buffer, medium, gradient_medium, supplement, master_mix} | mobile_phase |
C20 / 10 | bio_entity/{organism, organ, tissue} | sedimentable_material |
C21 / 10 | bio_cellular/{cell_line, primary_cells, cell_population, microbial_cells} | sedimentable_material |
C22 / 10 | bio_subcellular/{organelle, membrane, vesicle, cytoskeletal_structure} | sedimentable_material |
C23 / 10 | particulate/particle | sedimentable_material |
C30 / 10 | particulate/{beads, resin} | capture_support |
C40 / 10 | bio_molecule_or_virus/{dna, rna, protein, virus} | context_dependent_target |
C99 / 99 | * | composite_or_unknown |
Classification coverage is total because C99 matches every canonical type not covered above, including whole_blood, buffy_coat, and open other_* types.
Table 2. Separation-Fate Rules
Table 2 applies only when D_t.kind = separation_fate. For each component entry:
Inside the built-in provider:
F_ref(c, OP_t) = first_match(OP_t, G(c), c.relation, X) ?? UNRESOLVED
At the decision-selection boundary:
D_t = validated_author_decision ?? selected_provider_decision
Output tuples follow the ordered roles shown in the Outputs column. Exact tuples are the built-in provider's ideal routing. Non-exact recovery or carryover must be authored or returned by a selected provider with provenance.
| Rule / priority | OP | G | Current R | Context guard X | Outputs | Result F |
|---|---|---|---|---|---|---|
F_CEN_PRESERVE / 10 | centrifuge | * | pellet | * | (supernatant, pellet) | (0, 1) |
F_CEN_PRESERVE_PRECIPITATE / 10 | centrifuge | * | precipitate | * | (supernatant, pellet) | (0, 1) |
F_CEN_MOBILE / 20 | centrifuge | mobile_phase | free | * | (supernatant, pellet) | (1, 0) |
F_CEN_SEDIMENT / 20 | centrifuge | sedimentable_material | free | * | (supernatant, pellet) | (0, 1) |
F_CEN_SUPPORT / 20 | centrifuge | capture_support | free | * | (supernatant, pellet) | (0, 1) |
F_CEN_TARGET / 20 | centrifuge | context_dependent_target | free | * | (supernatant, pellet) | (1, 0) |
F_CEN_DEFAULT / 99 | centrifuge | * | * | * | (supernatant, pellet) | UNRESOLVED |
F_FIL_PRESERVE / 10 | {filtration, centrifugal_filtration} | * | membrane_bound | * | (filtrate, retentate) | (0, 1) |
F_FIL_SURFACE_PRESERVE / 10 | filtration | * | container_surface | surface_preserved(c, OP) = true | (filtrate, retentate) | (0, 1) |
F_FIL_FREE_PHASE / 15 | filtration | * | free | free_phase_passes(c, OP) = true | (filtrate, retentate) | (1, 0) |
F_FIL_MOBILE / 20 | {filtration, centrifugal_filtration} | mobile_phase | free | * | (filtrate, retentate) | (1, 0) |
F_FIL_RETAIN / 20 | {filtration, centrifugal_filtration} | {sedimentable_material, capture_support, context_dependent_target} | free | filter_retains(c, OP) = true | (filtrate, retentate) | (0, 1) |
F_FIL_DEFAULT / 99 | {filtration, centrifugal_filtration} | * | * | * | (filtrate, retentate) | UNRESOLVED |
F_PRE_PRESERVE / 10 | precipitation | * | precipitate | * | (precipitate, supernatant) | (1, 0) |
F_PRE_MOBILE / 20 | precipitation | mobile_phase | free | * | (precipitate, supernatant) | (0, 1) |
F_PRE_DEFAULT / 99 | precipitation | * | * | * | (precipitate, supernatant) | UNRESOLVED |
F_MAG_PRESERVE / 10 | magnetic | * | {bead_bound, field_retained} | * | (bound, flowthrough) | (1, 0) |
F_MAG_MOBILE / 20 | magnetic | {mobile_phase, sedimentable_material} | free | * | (bound, flowthrough) | (0, 1) |
F_MAG_SUPPORT / 20 | magnetic | capture_support | free | is_magnetic_support(c) = true | (bound, flowthrough) | (1, 0) |
F_MAG_DEFAULT / 99 | magnetic | * | * | * | (bound, flowthrough) | UNRESOLVED |
F_PHASE_DEFAULT / 99 | phase_partition | * | * | * | (target_phase, other_phase) | UNRESOLVED |
F_FIELD_DEFAULT / 99 | field | * | * | * | (target_band_fraction, non_target_fraction) | UNRESOLVED |
F_GENERIC_DEFAULT / 999 | * | * | * | * | Operation contract outputs | UNRESOLVED |
Fate coverage is total for every fraction-producing sep program because each recognized operation has a default and F_GENERIC_DEFAULT covers future or unknown fraction-producing operations. disrupt does not enter this table. An UNRESOLVED result is explicit incomplete scientific coverage, not a guessed fraction.
Table 3. Relationship Transition Rules
Table 3 runs once for every component entry affected by a state_transition decision and once for every positive output component produced by a separation_fate decision.
Only a separation decision uses the quantity formula:
q*_t(c,O) = q_t(c) × f_t[c,O]
τ(R, A, E, O, G, X) → (R', A', L', outcome)
Stable content_ref/kind/type is copied unchanged. Entries with different content_ref values remain separate. Entries with the same content_ref are compressed only when their quantities can be converted and their post-operation physical states can be represented by one shared state. Incompatible quantity units are rejected; incompatible physical states remain separate entries.
For same-content_ref relationship merging:
merge_relation(a, b, operation) =
1. apply the operation's state change to a and b independently
2. free, when both resulting states are free/mobile
3. the common state, when state name, associated object, and preservation are compatible
4. KEEP_SEPARATE, otherwiseThe Next R' column and the following closed target function jointly define the transition result. A non-free transition without the required A' is UNRESOLVED.
target(R', O, A, X) =
None when R' = free
selected component_entry(id) when R' = bead_bound and binding_established = true
unchanged component_entry(id) when R' in {bead_bound, membrane_bound, cell_bound} and its preservation guard is true
output container(id) when R' in {container_surface, pellet, precipitate, disrupted, field_retained}
UNRESOLVED otherwiseselected component_entry(id) must identify exactly one support entry in the same candidate component collection. Container IDs and component-entry IDs are different target kinds even when their string values happen to match.
| Rule / priority | Current R | Effect E | Output O | Group guard G | Context guard X | Next R' | Next L' | Outcome |
|---|---|---|---|---|---|---|---|---|
T_RESUSPEND / 10 | pellet | resuspend | destination | * | declared = true | free | None | ACCEPT |
T_DISSOLVE / 10 | precipitate | dissolve | destination | * | declared = true | free | None | ACCEPT |
T_ELUTE / 10 | bead_bound | elute | destination | * | declared = true | free | None | ACCEPT |
T_DISRUPT_TARGET / 10 | {free, container_surface, cell_bound} | disrupt | result_material | sedimentable_material | disruption_target = true | disrupted | lysate_material | ACCEPT |
T_DISRUPT_MOBILE / 10 | free | disrupt | result_material | mobile_phase | declared = true | free | lysate_material | ACCEPT |
T_DISRUPT_FREE_TARGET / 10 | free | disrupt | result_material | context_dependent_target | target_remains_identifiable = true | free | lysate_material | ACCEPT |
T_DISRUPT_DEFAULT / 15 | * | disrupt | result_material | * | No earlier disrupt row matched | unresolved | None | UNRESOLVED |
T_SURFACE_STAY / 20 | container_surface | add_only | same_container | * | cross_container = false | container_surface | None | ACCEPT |
T_FREE_MOVE / 20 | free | move | destination | * | cross_container = true | free | None | ACCEPT |
T_SURFACE_RELEASE / 20 | container_surface | move | destination | sedimentable_material | cross_container = true; release_declared = true | free | None | ACCEPT |
T_SURFACE_INVALID / 20 | container_surface | move | destination | * | cross_container = true; release_declared = false | unresolved | None | REJECT |
T_PRESERVE_CONTAINER_SURFACE / 20 | container_surface | separate | retentate | * | surface_preserved(c, OP) = true | container_surface | retentate_output | ACCEPT |
T_CREATE_PELLET / 30 | free | separate | pellet | {sedimentable_material, capture_support} | * | pellet | pellet_output | ACCEPT |
T_CREATE_PRECIPITATE / 30 | free | separate | precipitate | {sedimentable_material, context_dependent_target} | precipitation_established = true | precipitate | precipitate_output | ACCEPT |
T_CREATE_BEAD_BINDING / 30 | free | separate | bound | context_dependent_target | binding_established = true | bead_bound | bound_output | ACCEPT |
T_CREATE_FIELD_RETENTION / 30 | free | separate | bound | capture_support | field_retention_established = true | field_retained | bound_output | ACCEPT |
T_FREE_OUTPUT / 40 | free | separate | * | * | * | free | label(OP,O) | ACCEPT |
T_PRESERVE_PELLET / 40 | pellet | {move, separate} | * | * | release_declared = false | pellet | None for move; otherwise label(OP,O) | ACCEPT |
T_PRESERVE_PRECIPITATE / 40 | precipitate | {move, separate} | * | * | release_declared = false | precipitate | None for move; otherwise label(OP,O) | ACCEPT |
T_PRESERVE_DISRUPTED / 40 | disrupted | move | destination | * | * | disrupted | None | ACCEPT |
T_PRESERVE_BEAD_BOUND / 40 | bead_bound | {move, separate} | * | * | binding_preserved = true | bead_bound | None for move; otherwise label(OP,O) | ACCEPT |
T_PRESERVE_MEMBRANE_BOUND / 40 | membrane_bound | {move, separate} | * | * | membrane_preserved = true | membrane_bound | None for move; otherwise label(OP,O) | ACCEPT |
T_PRESERVE_CELL_BOUND / 40 | cell_bound | {move, separate} | * | * | cell_integrity_preserved = true | cell_bound | None for move; otherwise label(OP,O) | ACCEPT |
T_PRESERVE_FIELD / 40 | field_retained | {move, separate} | bound | * | field_preserved = true | field_retained | bound_output | ACCEPT |
T_FIELD_RELEASE / 50 | field_retained | move | destination | * | field_preserved = false | free | None | ACCEPT |
T_EXPIRE_LABEL / 50 | * | {mix, cross_container_move, new_separation} | * | * | label_has_persistent_relation = false | unchanged | None | ACCEPT |
T_DEFAULT / 999 | * | * | * | * | * | unresolved | None | REJECT |
Transition coverage is total because T_DEFAULT matches every unlisted combination. Successful R' values remain inside the closed relation set and can enter the next operation directly.
disrupt changes material state without requiring a component-fate percentage. If disruption makes an existing quantity no longer meaningful—for example, an intact-cell count after complete disruption—the transition decision must explicitly retire or replace that quantity. When that consequence is not declared, T_DISRUPT_DEFAULT returns UNRESOLVED; the runtime must not preserve a misleading count or invent transformed products.
The existing disrupt_program slot contract (lysate, debris_or_residue) is a compatibility surface, not permission to infer a percentage split. A debris_or_residue result may be populated only by an authored or typed combined transformation-and-separation decision. Otherwise the state-transition result is one result_material, and no debris percentage is invented.
The optional disrupt_program.method value records an authored, device-independent disruption mechanism. It does not change this material-transition contract, justify an inferred component-fate split, or bind execution equipment.
Adding lysis buffer through an add_only program, including the Case 09 Sentry Future program, is not disrupt and does not enter these rows.
Candidate And Commit Contract
Coordinator decisions and Table 3 results are not committed separately:
D_t = SeparationDecision { fractions: F_t, ... }
or StateTransitionDecision { transitions, ... }
C_t = {
source_before: M_t,
operation: OP_t,
effect_decision: D_t,
candidate_after: M*_t,
transitions
}Validation checks the fields required by the tagged decision. A separation decision requires known output roles, finite fractions, per-component-entry fraction sum = 1, and recalculated quantities. A state-transition decision requires a declared source state, next state, a typed next association target for every non-free state, affected entries, and any quantity invalidation or replacement. A component_entry(id) target must resolve inside the same candidate component collection. Both decision kinds require known content_ref, non-negativity, conservation of every unaffected quantity, safe compression, valid transition outcome, provenance, and capacity. Repeated content_ref values are valid when their entries retain distinguishable states. A valid C_t commits atomically as M_(t+1); otherwise M_t remains unchanged.
On the next operation, every component entry is read again from committed M_(t+1), classified through Table 1, and dispatched by the next sep program. A separation-fate decision continues through Table 2; a state-transition decision continues directly through Table 3. Two entries with the same content_ref but different states are resolved independently. Old F_t and output labels remain provenance only and are never reused as canonical type or a new fate decision.
Every non-authored scientific decision is resolved by the explicitly selected provider. By default, the selected provider is the built-in Rulebook defined here. An external provider may replace it, or an explicitly configured composite provider may route unresolved built-in results elsewhere.
Provider selection does not choose canonical groups, execute accepted transitions, merge components, validate conservation, or mutate runtime state. The built-in provider owns the group classification and decision tables defined in this document; an external provider is constrained by the same request/decision contracts but need not implement these internal tables.
