Skip to content

Write a Rust rule pack

Goal. Add domain-specific comparison behavior by registering correspondence rules in Rust.

Current status. Rule packs are an in-process Rust surface during pre-1.0. Python rule authoring and native rule-pack loading are deferred until the stable ABI tier is ready. Python plugins can author renderers today.

Choose the rule family

Most plugins contribute one or two focused rules, not a whole engine:

Need Rule family
Open a container and list children ExpandRule
Parse bytes into a typed artifact ParseRule
Propose that two side-tree nodes correspond PairRule
Explain one link as edits EditListWriter
Replace noisy edits with a shorter equivalent CompactionRule
Add final tags, item types, actions, or summaries ProjectionAnnotator

Prefer the narrowest family that owns the domain knowledge. A parser should not also decide renderer grouping; a writer should emit factual edits and tags, then let renderers decide presentation.

Minimal parse rule

A parse rule declares cheap selectors, reads source bytes through DataAccess, and publishes a typed artifact.

use binoc_sdk::{
    tabular_v1, BinocResult, DataAccess, ItemRef, NodeMatch, ParseDescriptor,
    ParseOutput, ParseRule,
};

pub struct FastaParseRule;

impl ParseRule for FastaParseRule {
    fn descriptor(&self) -> ParseDescriptor {
        ParseDescriptor {
            name: "biobinoc.parse.fasta".into(),
            input: NodeMatch {
                extensions: vec!["fasta".into(), "fa".into()],
                ..NodeMatch::default()
            },
            output: tabular_v1(),
            fires_beneath_settled: false,
        }
    }

    fn parse(&self, item: &ItemRef, data: &dyn DataAccess) -> BinocResult<ParseOutput> {
        let bytes = data.read_bytes(item)?;
        let artifact_bytes = parse_fasta_as_tabular_json(&bytes)?;
        Ok(artifact_bytes.into())
    }
}

# fn parse_fasta_as_tabular_json(_: &[u8]) -> BinocResult<Vec<u8>> {
#     Ok(Vec::new())
# }

If you publish a public artifact, document its schema and version it. If you can reuse an existing public artifact such as binoc.tabular.v1, do that instead of inventing a private format.

Minimal writer

An edit-list writer claims links whose artifacts it understands and returns open-vocabulary edits.

use binoc_sdk::{
    tabular_v1, BinocResult, DataAccess, Edit, EditListWriter, LinkCtx,
    NodeMatch, ShapeFilter, WriteOutput, WriterDescriptor,
};
use serde_json::json;

pub struct FastaWriter;

impl EditListWriter for FastaWriter {
    fn descriptor(&self) -> WriterDescriptor {
        WriterDescriptor {
            name: "biobinoc.write.fasta".into(),
            formats: vec![tabular_v1()],
            input: NodeMatch::default(),
            shape: ShapeFilter::Any,
        }
    }

    fn write(&self, ctx: &LinkCtx<'_>, data: &dyn DataAccess) -> BinocResult<Option<WriteOutput>> {
        let Some((left, right)) = load_fasta_artifacts(ctx, data)? else {
            return Ok(None);
        };
        if left.sequence == right.sequence {
            return Ok(Some(Vec::new().into()));
        }
        Ok(Some(
            vec![Edit::new(
                "biobinoc.sequence_changed",
                json!({ "from_len": left.sequence.len(), "to_len": right.sequence.len() }),
            )
            .with_item_type("biobinoc.fasta")
            .with_tag("biobinoc.sequence-changed")]
            .into(),
        ))
    }
}

# struct FastaData { sequence: String }
# fn load_fasta_artifacts(
#     _: &LinkCtx<'_>,
#     _: &dyn DataAccess,
# ) -> BinocResult<Option<(FastaData, FastaData)>> {
#     Ok(None)
# }

Return Ok(None) when the link is not yours. This is the producer-kind self-filter: shared artifacts can come from multiple packs, so a specialized writer must decline foreign payloads and let generic writers run.

Register the pack

In-process Rust consumers register rules by mutating CorrespondenceEngineConfig:

use std::sync::Arc;
use binoc_sdk::{CoreRule, CorrespondenceEngineConfig};

pub fn register_correspondence_rules(config: &mut CorrespondenceEngineConfig) {
    config.rules.insert(0, CoreRule::Parse(Arc::new(FastaParseRule)));
    config.writers.insert(0, Arc::new(FastaWriter));
}
# struct FastaParseRule;
# struct FastaWriter;

The in-tree model plugins use this pattern. See model-plugins/binoc-sqlite for a parser plus collection writer and model-plugins/binoc-row-reorder for a specialized writer.

Test with vectors

Use the shared vector harness from Rust. Register your rule pack into the engine config, materialize any source-tree fixtures, and run run_vector or run_vector_with_correspondence_engine_config.

Vectors should assert the user-visible behavior: actions, tags, child counts, diagnostics, and selected gold output. Name vectors for what they prove, such as fasta-sequence-change, not for implementation details.

Where to go next