Adding a Statistical Test
Embedding Pulse, not contributing to it? This recipe adds a built-in in
internal/processing/, which embedders cannot import. To add your own operator, implement the matching contract in the publicextendpackage instead — see Extension Points.
Audience: internals contributors adding a new TEST_* operator —
tier-1 (row-stream) or tier-2 (post-test on the materialised result
set).
The recipe mirrors the aggregator and feature recipes; the test-specific moving parts are streamability, the test catalog, and the registered-test capability table.
From CLAUDE.md, “Update Demand” rows for statistical tests and tier-2 post-test variants.
1. Decide tier
- Tier 1. Runs against the raw row stream, alongside aggregators.
Online-moments tests (
TEST_T,TEST_WELCH,TEST_CHISQ,TEST_ANOVA_F) stay in the streaming Process path. Sort-required tests (TEST_KS) force the buffered path. - Tier 2. Runs after the result set is materialised, in
req.PostTests. Always buffered.
2. Declare the type constant
Add to types/types.go:
const (
// ... existing constants ...
TEST_GINI_TREND TestType = "TEST_GINI_TREND"
)
Add it to types.AllTestTypes().
3. Implement and register
Tests live in internal/processing/test_*.go. Existing examples to mirror:
internal/processing/test_t.go— online tier-1 test.internal/processing/test_anova.go— tier-1 ANOVA with grouper support.internal/processing/test_post.goandinternal/processing/test_post_more.go— tier-2 post-tests.internal/processing/test_studentized.go— numerical integration utilities (used byTEST_TUKEY_HSD).
Register the test in internal/processing/test.go (the registry construction
calls). For tier-2 variants, declare both the base type and the
variant identifier the post-test surface uses.
4. Streamability
Add a case in types/streamability.go for the new TestType:
func (t TestType) Streamable() bool {
switch t {
// ...
case TEST_GINI_TREND:
return false // sort-based
}
}
Add the matching row in types/streamability_test.go so
TestStreamability_TestsKnown passes.
5. Capability declaration
Add a row to internal/descriptor/capabilities_tests.go:
- For a tier-1 test, declare it in the tier-1 catalog (
testCapabilities). - For a tier-2 post-test, declare it in
postTestCapabilities.
TestManifestTestsComplete and TestManifestPostTestsComplete
enforce that the manifest enumerates every registered test.
6. Skill update
Add an entry to skills/statistical-testing.md under “Operator
catalog”. Describe the test’s family, inputs, outputs (statistic, p,
df, effect size, …), and any preconditions (PULSE_TEST_* error
codes it can raise). For tier-2 variants, also document the variant
field shape since the post-test API exposes it.
7. Tests
Use the same TDD pattern as for aggregators. The processing package
has rich existing test files to model new cases against:
processor_test_pipeline_test.go, test_parametric_test.go,
test_nonparametric_test.go, test_post_more_test.go. Add hermetic
fixtures that exercise the streaming and buffered paths.
8. Error codes
If your test introduces a new failure mode, add a code to
errors/codes.go (mirror the existing PULSE_TEST_* family),
register its description row in internal/descriptor/capabilities_errors.go,
and document recovery in errors/fixup_metadata.go (codeMetadata
Message + Fixups, surfaced per-code via pulse_errors_lookup /
pulse errors lookup CODE). See the
Adding an Aggregator recipe for the same
pattern at the aggregator layer.
9. CLAUDE.md
There is no registered-test count or list in CLAUDE.md to update —
CLAUDE.md never hardcodes registered counts; the manifest is the source of
truth. The Update Demand operator row (.claude/reference/update-demand.md)
requires instead the atomic skill skills/op-test-<kebab>.md, the types/streamability.go case (§4), the capability declaration in
internal/descriptor/capabilities_tests.go — and an internal/examples/<dir>/*.json example whose _meta.operators
names the operator (TestEveryOperatorHasAnExampleTag). Edit CLAUDE.md only if the
operator introduces a contract it states directly, and mind
TestClaudeMdSizeBudget — long-form prose belongs in .claude/reference/.
A new preconditions class (e.g. paired sample, repeated measures)
belongs in skills/statistical-testing.md, not CLAUDE.md.
10. Run the gates
go test ./types/ -run TestStreamability_TestsKnown # declared TestType.Streamable() table
go test ./internal/processing/ -run 'TestProcessor_Tier1|TestProcessor_PostTest' # tier-1 streaming vs buffered, post-test dispatch
go test ./descriptor/ ./internal/descriptor/ -run TestManifest
go test ./internal/skills/ -run TestSkillsCoverAll
go test ./...
See The Update Demand for the full row that governs statistical-test changes.