module

package
v0.5.0 Latest Latest
Warning

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

Go to latest
Published: Oct 8, 2026 License: MIT Imports: 5 Imported by: 0

Documentation

Overview

Package module is the extension point.

A module is a domain sloprail understands — files, commands, later markers. It declares the events it can produce and what each carries, and produces them from what a harness reported. Everything else in the engine works from those declarations rather than from anything compiled into it, so adding a domain is adding a module rather than editing the parts that route, match and dispatch.

Adding one

Write a type satisfying Module, anywhere, named anything — nothing in this package or the tests around it cares where it lives or what its package is called, and Name() is the module's own and need not resemble either. Then add it to modules.All in internal/module/modules. That is the whole procedure, and the second step is not optional: a module absent from that list is dead code, and internal/module/modules.TestAll_HoldsEveryModuleInTheRepo fails until it is there. That test finds your type by type-checking the repo for implementations of Module, so it will find it wherever you put it.

There is exactly one module list in a build, and modules.Registry is the only registry a hook point should be handed. A hook point that assembles its own enforces against a vocabulary the load check never reported — see NewRegistry, and modules.TestOnlyModulesPackageBuildsARegistry, which is what holds that property.

Index

Constants

View Source
const (
	// InputPayload is what the harness reported, as it reported it.
	InputPayload = "payload"

	// InputPhase is which hook point is asking: what a cycle is about to do,
	// or what it turned out to have done.
	InputPhase = "phase"

	// InputMessages is the agent's own settled message text for this cycle, as a
	// []string, gathered by the caller from the record. The tag module reads it
	// to scan for `#tag` tokens; every other module ignores it. It is a separate
	// key rather than the payload because a Post dispatch carries two unrelated
	// things at once — the tree difference the file module reads, and the settled
	// messages the tag module reads — and one payload slot cannot be both. This
	// is exactly the growth the map shape exists to allow: a module added later
	// wanting an input the others do not.
	InputMessages = "messages"

	// InputSeenMessages is the part of this cycle's settled message text that an
	// EARLIER Stop in the same still-open cycle was already shown, as a []string,
	// oldest first. A cycle stays open until a Stop passes, so a refused reply's
	// text is delivered again with the retry; the tag module marks the tags found
	// only here as `seen`, so a rule can tell a re-sent tag from one the agent
	// wrote since the previous Stop. InputMessages then holds just the new text.
	InputSeenMessages = "seenMessages"
)

Well-known input keys. A module reads what it needs and ignores the rest.

View Source
const (
	PhasePre  = "pre"
	PhasePost = "post"
)

Phase values for InputPhase.

Variables

This section is empty.

Functions

func KnownFieldType

func KnownFieldType(t FieldType) bool

KnownFieldType reports whether a declared field type is one this build understands. Exported so the switches that read a FieldType can be tested against the same list they are checked against.

Types

type FieldDecl

type FieldDecl struct {
	Name string
	Type FieldType

	// Elem describes what a TypeList holds, when the module can say — or, on a
	// TypeMap whose keys are open (no Fields), what EVERY value holds, so a map
	// keyed by names the module cannot enumerate still has its values checked.
	// Ignored for every other type.
	//
	// It exists because the matchers that read a list read inside it:
	//
	//   any(invocations, .bin == "npm" && "--access" in .flags)
	//
	// is the form the spec documents, and it is where a command rule lives.
	// Without an element shape the collection is checked and the predicate
	// body is not, so `.bni` inside that expression would load and never fire —
	// precisely the silence declaring fields exists to prevent, reappearing one
	// level down.
	//
	// A list whose Elem is nil is a list of something unspecified, and a
	// predicate over it is left unchecked rather than refused. That is the
	// honest answer when a module has not said: unlike a map's keys, an
	// element's fields CAN be enumerated, so a module that knows them should
	// declare them and get the check.
	Elem *FieldDecl

	// Fields describes a TypeMap's or a list element's own fields, when the
	// module can enumerate them. Nil means it cannot, and reads of arbitrary
	// keys go unchecked rather than refused.
	Fields []FieldDecl
}

FieldDecl is one field on a kind.

type FieldType

type FieldType string

FieldType is what a matcher can expect a field to hold. Deliberately small: this exists to catch a misspelled field name and a comparison against the wrong shape, not to describe a schema.

const (
	TypeString FieldType = "string"
	TypeBool   FieldType = "bool"
	TypeList   FieldType = "list"
	TypeMap    FieldType = "map"

	// TypeInt is a whole number. It exists because a field that holds one and
	// is declared as anything else is checked wrongly in both directions: as a
	// string, `.line > 10` is refused for a comparison that is correct; as
	// nothing at all, `.line == "3"` loads for a comparison that can never
	// hold. A marker's line is the first such field.
	TypeInt FieldType = "int"
)

func FieldTypes

func FieldTypes() []FieldType

FieldTypes returns every known field type, in a stable order, for an error that has to name the alternatives.

type Input

type Input map[string]any

Input is what a module is given to extract from.

A map rather than a struct for the same reason an event's fields are: what a module needs to answer honestly is its own business. The file module wants the guardrails in play, to know what it may leave out; the command module wants nothing but the payload. A shared struct would have to grow a field for each, and a module added later could not grow it at all.

type KindDecl

type KindDecl struct {
	// Name is what happened, and when — "PreFileCreate". It does not say which
	// module produced it: the registry knows that because the module declared
	// it, and it rejects a second module claiming the same name.
	Name string

	// Fields a matcher may read on this kind.
	Fields []FieldDecl
}

KindDecl is one event kind and what it carries.

type Module

type Module interface {
	// Name identifies the module — how the engine reports what produced an
	// event, and how a module is referred to when one is switched off.
	Name() string

	// Kinds are the events this module can produce, with the fields each
	// carries.
	//
	// Declared rather than discovered, so that a matcher naming a field the
	// kind does not have is refused when the guardrail loads. A rule that
	// silently never fires is worse than one that will not load: the first
	// looks like a rule being satisfied.
	Kinds() []KindDecl

	// Extract turns what a harness reported into events. Producing none is
	// ordinary — most of what happens in a session concerns most modules not
	// at all.
	//
	// A module may return events ALONGSIDE a non-nil error, and a caller must
	// not discard them: one input a module could not make sense of is not a
	// reason to drop the events it did produce from the rest.
	Extract(Input) ([]event.Event, error)
}

Module is a domain and the events it produces.

type Registry

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

Registry holds the modules this build knows about, and which kinds each owns.

They are registered rather than discovered, for now. Shaping the boundary as an interface costs a name and a contract; discovering modules at runtime would cost a plugin mechanism, a compatibility promise and a security boundary, and nobody has asked for one. This is the seam where that becomes possible later without being paid for now.

func NewRegistry

func NewRegistry(mods ...Module) (*Registry, error)

NewRegistry returns a registry holding the given modules.

A build has exactly one module list, and it is modules.All. Call modules.Registry rather than this: a hook point that assembles its own list enforces against a vocabulary the load check never reported, which is a divergence no test can see — the help command's registry is the only one observable from outside the binary. That has reached main twice.

This is exported because internal/module/modules cannot reach an unexported one and the module list cannot move into this package — the modules import it, so a list here is an import cycle. Nothing structural stops a caller under services/ from calling this; what stops it is modules.TestOnlyModulesPackageBuildsARegistry, which walks the repo and fails on any caller outside internal/module/modules.

func NewRegistryForTest

func NewRegistryForTest(mods ...Module) (*Registry, error)

NewRegistryForTest builds a registry from arbitrary modules, for tests that need a vocabulary of their own.

The loader, the validator and the matcher are checked against modules declared in the test rather than against filemod, deliberately: the engine is supposed to work from declarations rather than from anything compiled into it, and a test borrowing a real module's kinds would stop proving that.

Separate from NewRegistry so the one-list check above can tell the two apart: a test needing its own vocabulary is legitimate, a hook point building a second real list is the defect. It panics outside a test binary, so reaching for it in shipped code crashes on first use rather than diverging silently.

func (*Registry) DeclaredKinds

func (r *Registry) DeclaredKinds() []string

DeclaredKinds returns every kind this registry can produce, in a stable order. Used to tell a rule author which kinds exist when their declaration names one that does not.

func (*Registry) KindDeclFor

func (r *Registry) KindDeclFor(kind string) (KindDecl, bool)

KindDeclFor returns the declaration of one kind, and whether it exists. This is what a matcher is checked against when a guardrail loads.

func (*Registry) Lookup

func (r *Registry) Lookup(kind string) (Module, bool)

Lookup returns the module that owns a kind, and whether one does. A kind nothing owns is a declaration naming an event that will never arrive.

func (*Registry) ModuleNames

func (r *Registry) ModuleNames() []string

ModuleNames returns every registered module, in a stable order, whether or not it declared a kind.

Registered rather than owning-a-kind is the distinction that matters. Kind ownership was the only thing the help reported, which left a module declaring no kinds invisible from outside the binary — it was in the build, it was enforcing nothing, and nothing could see either fact. A module with no kinds is a defect worth surfacing, not a module worth hiding: it produces no events, so anything a project bound to it would sit there looking enforced.

func (*Registry) Needed

func (r *Registry) Needed(boundKinds []string) []Module

Needed returns the modules that some binding actually asks for, given the kinds bound to across the enabled guardrails.

Producing an event nobody has bound to is work done to be discarded, and it is not cheap work: finding the programs a command line runs means walking its whole structure, on every command, whether or not any rule asks about commands. A project with no rule about commands should pay nothing for the fact that commands can be inspected at all.

Directories

Path Synopsis
Package modules holds the one list of modules a build ships with.
Package modules holds the one list of modules a build ships with.

Jump to

Keyboard shortcuts

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