classmesh

package module
v0.2.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 17, 2026 License: MIT Imports: 8 Imported by: 0

README

ClassMesh

ClassMesh is a Go library for building high-throughput, confidence-gated classification cascades over payload-agnostic records. The classmesh command is its reference application and operational tool.

Status: pre-v1. Rules, schema, and cascade configuration work end to end (make test passes, the examples below run as documented, and the parallel engine is available). The CLI configuration includes a deterministic mock stage for exercising gates and review routing.

Use as a library

Create stages, put them in a classmesh.Cascade, and classify one record at a time. This example uses the programmatic rules package, so it does not need YAML configuration:

ruleStage, err := rules.New([]rules.Rule{
    {
        ID:       "question-terminal",
        Category: "question",
        Regex:    []string{`[??]['")\]}»’”›]*$`},
    },
})
if err != nil {
    return err
}

mesh, err := classmesh.New(ruleStage)
if err != nil {
    return err
}

result, err := mesh.Classify(context.Background(), classmesh.Record{
    ID:   "sentence-1",
    Kind: classmesh.KindText,
    Data: []byte("Ready?"),
})

See the complete runnable program in examples/library, then run it in module mode with GOWORK=off go run ./examples/library.

Why

Classifying high-volume data (logs, events, records) with an LLM per record is slow and expensive; hand-rolled regex pipelines are cheap but rot. ClassMesh is the middle path: a confidence-gated cascade where each record exits at the cheapest stage that can decide it.

Design

source -> [ stage 1: rules ] -> [ stage 2: custom ] -> [ stage N ] -> sink
               │ confident?           │ confident?
               └── exit early ────────┴── exit early    uncertain -> review sink

The root library accepts any implementation of classmesh.Stage. The optional streaming tier also exposes source and sink interfaces. Today it includes text files, JSONL event streams, stdin adapters, in-memory adapters, and JSONL sinks.

For working implementations, see text (a Source), rules (a Stage), and jsonl (a Sink). docs/architecture.md explains how the pieces fit and why the core stays payload-agnostic.

Layout

  • Root classmesh: records, classifications, stages, gates, and Cascade
  • rules/ and schema/: programmatic built-in stages
  • stream/: optional engine, sources, sinks, and routing
  • internal/: CLI configuration and implementation details
  • cmd/classmesh/: the reference command
  • examples/: embedded-library and CLI examples

Use the CLI

The CLI is distributed as one cgo-free Go binary. Runnable examples live in examples/: one ruleset classifies both text logs and JSON events. Every block below runs from a fresh clone.

make build

# text logs, one record per line
./bin/classmesh run --rules examples/rules.yml examples/logs.txt

# JSON events, one object per line, classified on their fields
./bin/classmesh run --rules examples/rules.yml --input jsonl examples/events.jsonl

Each classified record is one JSON object on stdout with its category, confidence, the matched rule's reason, and (for events) the decoded fields. Records no rule matches are counted and reported on stderr.

The full multi-stage cascade runs from a fresh clone too. genprod.go writes production-shaped logs (access lines, probes, app chatter, payments, auth events, warns, errors, and a slice nothing recognizes; deterministic by seed), and classmesh.yaml declares a two-tier cascade over them: rules first, a gated model stand-in for the leftovers, review for what neither tier can decide, health-check noise dropped by route:

go run examples/genprod.go -n 1000000 > prod.log
./bin/classmesh validate --config examples/classmesh.yaml
./bin/classmesh run --config examples/classmesh.yaml prod.log > classified.jsonl

With the default seed, the million lines classify in under two seconds: the rules tier decides 88%, the model tier 6%, and 6% lands in examples/review.jsonl. The stderr stats line reads processed=1000000 classified=940162 review=59838 by_stage=map[model:60009 rules:880153]; classified counts the health-check records the noise route then discards, so classified.jsonl holds 720,490 lines.

Cascade config

A whole multi-stage cascade can be declared in one versioned YAML file, checked with validate and run with run --config:

./bin/classmesh validate --config examples/classmesh.yaml    # parse + validate only
./bin/classmesh run --config examples/classmesh.yaml app.log # build and run it
version: 1
input:  { type: text }                      # text | jsonl
stages:
  - { id: rules, type: rules, path: rules.yml, gate: 1.0 }  # gate is optional
sink:   { type: jsonl, stream: stdout }     # default sink for classified records
review: { type: jsonl, path: review.jsonl } # optional; the undecided go here

Unknown keys, duplicate stage ids, out-of-range gates, and unknown stage/sink types are rejected before any input is opened. run --config executes rules, schema, and mock stages (each honoring its per-stage gate) into the default sink and the review sink; every stage loads its declaration from the stage's path. The mock stage is a deterministic model stand-in that scores matched records with declared confidences below 1.0, so per-stage gates and review routing can be exercised before a real model stage exists. When the config declares category routes, classified records are dispatched by category (each route to its own sink, or drop to discard that category) with the default sink as the fallback for unrouted categories. A top-level workers: N (or --workers N with --rules) classifies records on N goroutines while preserving output order, error reporting, and stats exactly; the default stays serial.

Performance

Measured on a single core (AMD Ryzen 7 3800X, make bench):

Path Per record Throughput Allocations
Rules stage, first-rule hit 46 ns ~22M records/sec 0
Rules stage, 20-rule regex-heavy miss (benchmark ruleset) 7-8 µs ~130k records/sec 0
Full pipeline (engine + rules, discard sink) 500-550 ns ~1.8M records/sec 0
Integrated pipeline (text source -> rules -> JSONL sink) ~600 ns ~1.6M records/sec 2

Per-record cost depends on your ruleset: order rules by expected volume so the hot path exits early. The miss row is the benchmark ruleset's cost, not a general bound: a pattern with no extractable required literal defeats the prefilter and every such rule pays its full regex on a miss (BenchmarkClassifyRegexMissUnprefilterable: ~101 µs at 20 rules on the same machine). The pipeline row isolates engine + rules behind a discard sink; the integrated row adds the source read and JSON encode every CLI run pays. The structured path (JSONL source -> field rules -> structured output) is benchmarked in stream as well.

The comparison that motivates the cascade: classifying 1M short log lines with a budget LLM API (~25 input + 5 output tokens each at $0.15/$0.60 per million tokens) costs about $6.75 per million lines and runs at API latency. The rules stage does the same volume in well under a second per core for the cost of the electricity, and the cascade design only forwards the records rules can't decide to anything that costs money.

Reproduce: make bench for the table. For an end-to-end run at volume, go run examples/genlogs.go -n 1000000 > logs-1m.txt generates a deterministic weighted stream matching the example ruleset; 1M lines classify in under a second, including JSON output.

Development

make build   # compile binaries into ./bin
make test    # run all tests with -race
make lint    # run golangci-lint from the module root

License

MIT

Documentation

Overview

Package classmesh provides the core classification vocabulary and cascade.

Example
package main

import (
	"context"
	"fmt"

	"github.com/ClassMesh/classmesh"
	"github.com/ClassMesh/classmesh/rules"
)

func main() {
	ruleStage, err := rules.New([]rules.Rule{{ID: "question-terminal", Category: "question", Regex: []string{`[??]['")\]}»’”›]*$`}}})
	if err != nil {
		panic(err)
	}

	mesh, err := classmesh.New(ruleStage)
	if err != nil {
		panic(err)
	}

	result, err := mesh.Classify(context.Background(), classmesh.Record{ID: "sentence-1", Kind: classmesh.KindText, Data: []byte("Ready?")})
	if err != nil {
		panic(err)
	}

	fmt.Printf("category=%s confidence=%.0f stage=%s\n", result.Category, result.Confidence, result.Stage)
}

Index

Examples

Constants

This section is empty.

Variables

View Source
var ErrUnclassified = errors.New("stage: unclassified")

ErrUnclassified is returned by Classify when the stage cannot decide on a category for the record. The cascade then hands the record to the next stage; a record unclassified by every stage is routed for review.

Functions

This section is empty.

Types

type Cascade

type Cascade struct {
	// contains filtered or unexported fields
}

Cascade classifies records through an ordered set of stages.

func New

func New(stages ...Stage) (*Cascade, error)

New constructs a Cascade with no confidence gate or logger.

func NewWithOptions

func NewWithOptions(options Options) (*Cascade, error)

NewWithOptions validates options and constructs a Cascade.

func (*Cascade) Classify

func (c *Cascade) Classify(ctx context.Context, r Record) (Classification, error)

Classify returns the first admitted stage decision.

func (*Cascade) ClassifyBatch

func (c *Cascade) ClassifyBatch(ctx context.Context, records []Record) []Result

ClassifyBatch classifies records sequentially in input order.

func (*Cascade) ClassifyBatchConcurrent

func (c *Cascade) ClassifyBatchConcurrent(ctx context.Context, records []Record, workers int) []Result

ClassifyBatchConcurrent classifies records concurrently in input order.

type Classification

type Classification struct {
	// Category is the assigned label.
	Category string
	// Confidence is in [0, 1]. Deterministic stages emit 1.
	Confidence float64
	// Stage names the stage that produced this classification.
	Stage string
	// Reasons is optional evidence for the classification. Treat it as
	// read-only: a stage may hand back a shared slice (the rules stage does,
	// to stay allocation-free), so callers must not mutate it in place.
	Reasons []Reason
}

Classification is the outcome of running a Record through a stage.

func (Classification) IsValid

func (c Classification) IsValid() bool

IsValid reports whether the classification is well-formed: a non-empty category and a confidence within [0, 1].

type Gate

type Gate struct {
	// contains filtered or unexported fields
}

Gate is a minimum confidence. A decision scoring below it is treated as undecided, so the cascade escalates to the next stage. The zero Gate admits everything, which turns gating off; deterministic stages emit 1, so they always pass.

func NewGate

func NewGate(min float64) (Gate, error)

NewGate returns a Gate for min, or an error if min is outside [0, 1].

type Kind

type Kind string

Kind identifies the shape of a Record's payload. The zero value is KindUnknown.

const (
	KindUnknown Kind = ""
	KindText    Kind = "text"
	KindLog     Kind = "log"
	KindEvent   Kind = "event"
	KindRecord  Kind = "record"
	KindJSON    Kind = "json"
)

type Options

type Options struct {
	Stages        []Stage
	MinConfidence float64
	Logger        *slog.Logger
}

Options configures a Cascade.

type Reason

type Reason struct {
	// Code is a short tag for the evidence, like a rule ID.
	Code string `json:"code"`
	// Detail is a readable description.
	Detail string `json:"detail,omitempty"`
}

Reason explains why a stage picked a category. A stage may attach zero or more.

type Record

type Record struct {
	// ID uniquely identifies the record within a run. Sources assign it.
	ID string
	// Kind identifies the payload shape. Zero value is KindUnknown.
	Kind Kind
	// Data is the raw payload (a log line, a JSON document, ...).
	Data []byte
	// Fields holds structured attributes decoded from the payload, when a
	// source has them. Nil for unstructured payloads.
	Fields map[string]any
	// Meta carries source-specific context (file name, line number, ...).
	Meta map[string]string
}

Record is a single unit of data moving through the pipeline. Sources produce Records, stages classify them, sinks consume them.

type Result

type Result struct {
	Classification Classification
	Err            error
}

Result pairs a Classification with its classification error.

type Stage

type Stage interface {
	// Name identifies the stage in classifications, logs, and metrics.
	Name() string
	// Classify assigns a category to the record, or returns
	// ErrUnclassified when this stage cannot decide. It must not mutate
	// the record, which a concurrent engine shares with later consumers.
	Classify(ctx context.Context, r Record) (Classification, error)
}

Stage classifies records. Implementations range from deterministic rule matching to in-process model inference to remote API calls.

func WithGate

func WithGate(inner Stage, gate Gate) Stage

WithGate wraps inner so a decision below gate escalates instead of winning.

func WithName

func WithName(inner Stage, name string) Stage

WithName wraps inner so Name reports name.

type StageError

type StageError struct {
	// Stage is the stage that failed.
	Stage string
	// Err is the underlying cause.
	Err error
}

StageError reports that a stage itself broke, as opposed to ErrUnclassified, which just asks the cascade to try the next stage. Stage names the stage that failed and Unwrap gives the cause, so callers can tell which stage failed and why with errors.As and errors.Is.

func (*StageError) Error

func (e *StageError) Error() string

Error implements error.

func (*StageError) Unwrap

func (e *StageError) Unwrap() error

Unwrap returns the cause so errors.Is and errors.As reach through.

Directories

Path Synopsis
cmd
classmesh command
Command classmesh is the ClassMesh CLI: a classification cascade pipeline in one binary.
Command classmesh is the ClassMesh CLI: a classification cascade pipeline in one binary.
examples
library command
internal
cli
Package cli wires sources, stages, and sinks into the classmesh CLI.
Package cli wires sources, stages, and sinks into the classmesh CLI.
config
Package config parses and validates a declarative cascade configuration: a versioned YAML document that declares the input, the ordered stages and their gates, and where classified records are routed.
Package config parses and validates a declarative cascade configuration: a versioned YAML document that declares the input, the ordered stages and their gates, and where classified records are routed.
fieldpath
Package fieldpath reads nested values out of a decoded JSON object (map[string]any) by path.
Package fieldpath reads nested values out of a decoded JSON object (map[string]any) by path.
logfields
Package logfields extracts common fields from a text log line, best-effort: a leading timestamp, a level token, and logfmt key=value pairs.
Package logfields extracts common fields from a text log line, best-effort: a leading timestamp, a level token, and logfmt key=value pairs.
mockstage
Package mockstage implements the internal deterministic mock stage.
Package mockstage implements the internal deterministic mock stage.
tokenizer/wordpiece
Package wordpiece implements BERT-style WordPiece tokenization in pure Go, with no cgo and no external tokenizer runtime.
Package wordpiece implements BERT-style WordPiece tokenization in pure Go, with no cgo and no external tokenizer runtime.
version
Package version exposes build metadata injected at link time via -ldflags.
Package version exposes build metadata injected at link time via -ldflags.
Package rules implements deterministic matching as a classmesh.Stage.
Package rules implements deterministic matching as a classmesh.Stage.
Package schema implements a classmesh.Stage that validates a structured record's decoded Fields against a declared shape.
Package schema implements a classmesh.Stage that validates a structured record's decoded Fields against a declared shape.
services
cli module
shared module
Package stream moves records from a Source through a Cascade into a Sink.
Package stream moves records from a Source through a Cascade into a Sink.
sink
Package sink defines where classified records go: stdout, files, review queues, downstream pipelines.
Package sink defines where classified records go: stdout, files, review queues, downstream pipelines.
sink/jsonl
Package jsonl implements sink.Sink as JSON Lines: one JSON object per classified record, the pipe-friendly lingua franca of log tooling.
Package jsonl implements sink.Sink as JSON Lines: one JSON object per classified record, the pipe-friendly lingua franca of log tooling.
source
Package source defines where Records come from.
Package source defines where Records come from.
source/jsonl
Package jsonl implements source.Source over JSON Lines: one JSON object per line.
Package jsonl implements source.Source over JSON Lines: one JSON object per line.
source/text
Package text implements source.Source over line-oriented text: files and stdin.
Package text implements source.Source over line-oriented text: files and stdin.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL