Skip to content

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

text
provider_id: culsma.builtin.material_rulebook
provider_version: 1.0
capabilities:
  material.separation_fate: 1.0
  material.state_transition: 1.0

Rulebook Format

This document specifies the official built-in material provider. Its three tables are that provider's complete rule surface:

text
separation-fate path: Table 1 Classify → Table 2 Resolve Fractions → Table 3 Transition State
state-transition path: Table 1 Classify ─────────────────────────→ Table 3 Transition State

Every 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

text
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 decision

Provider Binding

text
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:

text
SeparationDecision {
  kind: separation_fate,
  fractions,
  decision_source,
  provenance
}

StateTransitionDecision {
  kind: state_transition,
  transitions,
  decision_source,
  provenance
}

Registered sep program dispatch is closed:

text
separation_fate:
  centrifuge
  filtration
  centrifugal_filtration
  precipitation
  magnetic
  phase_partition
  field
  generic sep

state_transition:
  disrupt
  cross-container material move

A 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 distinct content_ref.
  • content_ref: the declared content name referenced by that entry.
  • quantity: one value and unit, such as 300 uL, 2 mg, or 200000 cells.
  • Compatible quantities: quantities whose units describe the same kind of quantity and can be converted before addition, such as uL + mL. cells + uL is not compatible.
  • relation: the component's current physical state, such as free, container_surface, pellet, or bead_bound.
  • association_target: the typed physical object required by a relationship. Container-local relations use container(id); material-bound relations use component_entry(id). free has 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 as supernatant_output; it expires when that output scope no longer applies.

Component Merge Flow

mermaid
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.

text
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.
  • X predicates must be declared typed facts; free-text inference is forbidden.
  • free_phase_passes(c, OP) is true only when the operation contract explicitly routes every free entry to the pass-through output; built-in adherent_cell_surface aspiration declares this fact.
  • Every table has an exhaustive final row.
  • Every registered sep program must dispatch to separation_fate, state_transition, or an explicitly declared combined decision; no program bypasses the candidate transaction.
  • UNRESOLVED means the selected provider lacks sufficient scientific facts to return a typed decision.
  • KEEP_SEPARATE means mixing succeeds but two component entries cannot be compressed into one without losing state.
  • REJECT leaves committed material unchanged.
  • label never participates in canonical classification.

Closure Outcomes

The rulebook is closed when every component entry and operation produces one controlled outcome:

text
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 invalid

These outcomes distinguish three different problem classes:

  • An implementation defect violates a resolved rule, for example leaking retentate_output into later classification or copying a container-surface state across containers.
  • A representation gap occurs when valid material contains multiple states for one content_ref but 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 / priorityCanonical selectorResult G
C10 / 10bio_fluid/{plasma, serum, urine, saliva, lymph, cerebrospinal_fluid, tears, semen, ascites, synovial_fluid, bronchoalveolar_lavage_fluid}mobile_phase
C11 / 10chemical/{solvent, organic_compound, inorganic_compound, detergent, dye}mobile_phase
C12 / 10formulation/{buffer, medium, gradient_medium, supplement, master_mix}mobile_phase
C20 / 10bio_entity/{organism, organ, tissue}sedimentable_material
C21 / 10bio_cellular/{cell_line, primary_cells, cell_population, microbial_cells}sedimentable_material
C22 / 10bio_subcellular/{organelle, membrane, vesicle, cytoskeletal_structure}sedimentable_material
C23 / 10particulate/particlesedimentable_material
C30 / 10particulate/{beads, resin}capture_support
C40 / 10bio_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 / priorityOPGCurrent RContext guard XOutputsResult F
F_CEN_PRESERVE / 10centrifuge*pellet*(supernatant, pellet)(0, 1)
F_CEN_PRESERVE_PRECIPITATE / 10centrifuge*precipitate*(supernatant, pellet)(0, 1)
F_CEN_MOBILE / 20centrifugemobile_phasefree*(supernatant, pellet)(1, 0)
F_CEN_SEDIMENT / 20centrifugesedimentable_materialfree*(supernatant, pellet)(0, 1)
F_CEN_SUPPORT / 20centrifugecapture_supportfree*(supernatant, pellet)(0, 1)
F_CEN_TARGET / 20centrifugecontext_dependent_targetfree*(supernatant, pellet)(1, 0)
F_CEN_DEFAULT / 99centrifuge***(supernatant, pellet)UNRESOLVED
F_FIL_PRESERVE / 10{filtration, centrifugal_filtration}*membrane_bound*(filtrate, retentate)(0, 1)
F_FIL_SURFACE_PRESERVE / 10filtration*container_surfacesurface_preserved(c, OP) = true(filtrate, retentate)(0, 1)
F_FIL_FREE_PHASE / 15filtration*freefree_phase_passes(c, OP) = true(filtrate, retentate)(1, 0)
F_FIL_MOBILE / 20{filtration, centrifugal_filtration}mobile_phasefree*(filtrate, retentate)(1, 0)
F_FIL_RETAIN / 20{filtration, centrifugal_filtration}{sedimentable_material, capture_support, context_dependent_target}freefilter_retains(c, OP) = true(filtrate, retentate)(0, 1)
F_FIL_DEFAULT / 99{filtration, centrifugal_filtration}***(filtrate, retentate)UNRESOLVED
F_PRE_PRESERVE / 10precipitation*precipitate*(precipitate, supernatant)(1, 0)
F_PRE_MOBILE / 20precipitationmobile_phasefree*(precipitate, supernatant)(0, 1)
F_PRE_DEFAULT / 99precipitation***(precipitate, supernatant)UNRESOLVED
F_MAG_PRESERVE / 10magnetic*{bead_bound, field_retained}*(bound, flowthrough)(1, 0)
F_MAG_MOBILE / 20magnetic{mobile_phase, sedimentable_material}free*(bound, flowthrough)(0, 1)
F_MAG_SUPPORT / 20magneticcapture_supportfreeis_magnetic_support(c) = true(bound, flowthrough)(1, 0)
F_MAG_DEFAULT / 99magnetic***(bound, flowthrough)UNRESOLVED
F_PHASE_DEFAULT / 99phase_partition***(target_phase, other_phase)UNRESOLVED
F_FIELD_DEFAULT / 99field***(target_band_fraction, non_target_fraction)UNRESOLVED
F_GENERIC_DEFAULT / 999****Operation contract outputsUNRESOLVED

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:

text
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, otherwise

The Next R' column and the following closed target function jointly define the transition result. A non-free transition without the required A' is UNRESOLVED.

text
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                                 otherwise

selected 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 / priorityCurrent REffect EOutput OGroup guard GContext guard XNext R'Next L'Outcome
T_RESUSPEND / 10pelletresuspenddestination*declared = truefreeNoneACCEPT
T_DISSOLVE / 10precipitatedissolvedestination*declared = truefreeNoneACCEPT
T_ELUTE / 10bead_boundelutedestination*declared = truefreeNoneACCEPT
T_DISRUPT_TARGET / 10{free, container_surface, cell_bound}disruptresult_materialsedimentable_materialdisruption_target = truedisruptedlysate_materialACCEPT
T_DISRUPT_MOBILE / 10freedisruptresult_materialmobile_phasedeclared = truefreelysate_materialACCEPT
T_DISRUPT_FREE_TARGET / 10freedisruptresult_materialcontext_dependent_targettarget_remains_identifiable = truefreelysate_materialACCEPT
T_DISRUPT_DEFAULT / 15*disruptresult_material*No earlier disrupt row matchedunresolvedNoneUNRESOLVED
T_SURFACE_STAY / 20container_surfaceadd_onlysame_container*cross_container = falsecontainer_surfaceNoneACCEPT
T_FREE_MOVE / 20freemovedestination*cross_container = truefreeNoneACCEPT
T_SURFACE_RELEASE / 20container_surfacemovedestinationsedimentable_materialcross_container = true; release_declared = truefreeNoneACCEPT
T_SURFACE_INVALID / 20container_surfacemovedestination*cross_container = true; release_declared = falseunresolvedNoneREJECT
T_PRESERVE_CONTAINER_SURFACE / 20container_surfaceseparateretentate*surface_preserved(c, OP) = truecontainer_surfaceretentate_outputACCEPT
T_CREATE_PELLET / 30freeseparatepellet{sedimentable_material, capture_support}*pelletpellet_outputACCEPT
T_CREATE_PRECIPITATE / 30freeseparateprecipitate{sedimentable_material, context_dependent_target}precipitation_established = trueprecipitateprecipitate_outputACCEPT
T_CREATE_BEAD_BINDING / 30freeseparateboundcontext_dependent_targetbinding_established = truebead_boundbound_outputACCEPT
T_CREATE_FIELD_RETENTION / 30freeseparateboundcapture_supportfield_retention_established = truefield_retainedbound_outputACCEPT
T_FREE_OUTPUT / 40freeseparate***freelabel(OP,O)ACCEPT
T_PRESERVE_PELLET / 40pellet{move, separate}**release_declared = falsepelletNone for move; otherwise label(OP,O)ACCEPT
T_PRESERVE_PRECIPITATE / 40precipitate{move, separate}**release_declared = falseprecipitateNone for move; otherwise label(OP,O)ACCEPT
T_PRESERVE_DISRUPTED / 40disruptedmovedestination**disruptedNoneACCEPT
T_PRESERVE_BEAD_BOUND / 40bead_bound{move, separate}**binding_preserved = truebead_boundNone for move; otherwise label(OP,O)ACCEPT
T_PRESERVE_MEMBRANE_BOUND / 40membrane_bound{move, separate}**membrane_preserved = truemembrane_boundNone for move; otherwise label(OP,O)ACCEPT
T_PRESERVE_CELL_BOUND / 40cell_bound{move, separate}**cell_integrity_preserved = truecell_boundNone for move; otherwise label(OP,O)ACCEPT
T_PRESERVE_FIELD / 40field_retained{move, separate}bound*field_preserved = truefield_retainedbound_outputACCEPT
T_FIELD_RELEASE / 50field_retainedmovedestination*field_preserved = falsefreeNoneACCEPT
T_EXPIRE_LABEL / 50*{mix, cross_container_move, new_separation}**label_has_persistent_relation = falseunchangedNoneACCEPT
T_DEFAULT / 999*****unresolvedNoneREJECT

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:

text
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.

Public language reference for the current Culsma surface.