declaration

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: 19 Imported by: 0

Documentation

Overview

Package declaration reads the rule declarations a project keeps under its `.sloprail/` directory into typed, validated Go values.

It is the NEW-format counterpart to internal/guardrail's old-format loader. Where internal/guardrail reads one `guardrails/<name>/GUARDRAIL.md` per rule — frontmatter keyed by raw event kind, prose folded into a judge's rubric — this package reads the declaration formats the current spec defines that the engine actually loads and dispatches on (dot-dir-file-store/main.tsp):

  • file-guard/<name>/file-guard.yaml → FileGuard (a rule on a file's state)
  • gate/<name>/gate.yaml → Gate (a checkpoint on an event)
  • context/<name>/context.yaml → Context (an activatable scope)
  • file-guard/structure.yaml → StructureGate (a path allowlist per root)

A GOAL is deliberately NOT among them. A goal is a higher-level COMPOSITE — a project-level `goal/<name>/goal.yaml` (a sibling of `.sloprail/`, maintained by the user) paired with an ordinary Context whose `enter` reads it. The engine neither loads nor dispatches on the goal file; it is not a sloprail primitive, so it has no type, no Nature, and no place in the loader here.

What this slice does and does not do

It LOADS and VALIDATES. Given a `.sloprail` root it produces the in-memory representation the future dispatch slice consumes: every declaration parsed, every exactly-one-of / at-least-one / required-field constraint checked, every `match` and trigger `match` COMPILED against the right nature's scope (so a bad expression is a load error naming the problem, not a rule that silently never fires), every `on:` kind checked against the kinds its nature admits, and every `require` reference resolved (an unknown skill is allowed — it is a runtime fact — but a context prerequisite naming a context that does not exist is a configuration error caught here). It does NOT match events against these declarations or run their checks; that is the next batch.

Why a package of its own rather than extending internal/natures

internal/natures holds the runtime STATE maps a rule reads about the world — ContextState, GateState — the `{active, payload}` and `{status}` a matcher sees. Those are facts the engine writes and reads across a process boundary. The DECLARATIONS here are a different thing: the authored rule as it sits on disk, the shape a loader parses and a validator refuses. Keeping them apart keeps internal/natures free of the loader's dependencies (this package imports internal/guardrail for its scope compilers and internal/module for the event vocabulary; internal/natures imports neither), and lets a reader find "the state a rule reads" and "the rule a project declared" in two named places.

This package REUSES rather than reinvents. The runtime state shapes are internal/natures'; the match compilers (CompileFileMatch / CompileGateMatch / CompileContextMatch) and the expression evaluator underneath them are internal/guardrail's; the event vocabulary and its field declarations are internal/module's. What this package adds is the declaration types, the per-nature event-kind rules, the alias expansion, and the loaders that tie them together with load-time diagnostics.

Index

Constants

View Source
const (
	DefaultSubagentSilentAfterMinutes = 10
	DefaultSubagentStaleAfterMinutes  = 60
)

The defaults of the sub-agent silence policy, in minutes.

View Source
const (
	// KindPreFileCreate fires before a new file is written.
	KindPreFileCreate = "PreFileCreate"
	// KindPreFileUpdate fires before an existing file is changed.
	KindPreFileUpdate = "PreFileUpdate"
	// KindPreFileDelete fires before a file is deleted.
	KindPreFileDelete = "PreFileDelete"
	// KindPreCommandInvoke fires before a command is invoked.
	KindPreCommandInvoke = "PreCommandInvoke"
	// KindPreToolUse fires before any tool runs — the general refusable moment.
	KindPreToolUse = "PreToolUse"
	// KindPostFileCreate fires after a file is created.
	KindPostFileCreate = "PostFileCreate"
	// KindPostFileUpdate fires after a file is changed.
	KindPostFileUpdate = "PostFileUpdate"
	// KindPostFileDelete fires after a file is deleted.
	KindPostFileDelete = "PostFileDelete"
	// KindPostTagWrite fires when a tag is written in the trajectory, as the turn settles.
	KindPostTagWrite = "PostTagWrite"
	// KindStop fires at the end of a turn — a gate's usual moment.
	KindStop = "Stop"
)

The concrete event kinds, spelled exactly as the modules declare them and the events spec's EventKind enum names them. Named constants so the union sets and the alias expansions below cannot drift on a spelling.

View Source
const (
	// AliasPreFileWrite expands to PreFileCreate + PreFileUpdate. Admitted on both
	// a gate and a context (both may wake pre-write).
	AliasPreFileWrite = "PreFileWrite"

	// AliasPostFileWrite expands to PostFileCreate + PostFileUpdate. Admitted on a
	// CONTEXT only — a gate does not wake on settled content, so `PostFileWrite`
	// is not a gate alias.
	AliasPostFileWrite = "PostFileWrite"
)

The config-level aliases a trigger's `event` may name in place of a concrete kind. Not events the engine emits — a shorthand in `on` the loader expands on read, because a file written under a prefix arrives as either a create or an update, and repeating the same `match` on two triggers is the duplication the alias removes.

View Source
const FallbackStopHookBlockCap = 8

FallbackStopHookBlockCap is the finite cap of a harness with none of its own. The engine's cap is the loop breaker for a Stop gate that cannot be satisfied (a limit of 0 means it never lets go), so the default must never be 0: a harness that would block forever (Codex kept going through 30) still gets this.

Variables

View Source
var (
	// ErrBadScript: a declared script (a check's `script`/`prepare`, `subjects`,
	// a context's `enter`/`exit`) exists but cannot be exec'd directly — no
	// shebang, not executable, or an interpreter outside /bin and /usr/bin.
	// Scripts are never run as `sh <file>`, so the rule cannot run as written.
	// Environment fault: the rule stays loaded and enforced, it is reported
	// (Loaded.Degraded), and at run time the exec path refuses what it guards.
	ErrBadScript = errors.New("declaration: script cannot be run directly")

	// ErrMalformed: the declaration could not be read or parsed at all. There is
	// no rule to load. Declaration fault.
	ErrMalformed = errors.New("declaration: could not be read")

	// ErrBadMatch: a `match` or trigger `match` will not compile against its
	// nature's scope. The check the whole match-compilation exists for — a rule
	// whose match cannot run would silently never fire. Declaration fault.
	ErrBadMatch = errors.New("declaration: match cannot run in this scope")

	// ErrUnknownEventKind: a trigger's `on` names an event kind (or alias) the
	// nature does not admit — a Post event on a gate, a typo, Stop as a context
	// entry event. Nothing will ever wake it correctly. Declaration fault.
	ErrUnknownEventKind = errors.New("declaration: event kind not valid for this nature")

	// ErrExactlyOne: a field that must set EXACTLY one of a pair sets both or
	// neither — a Check's script/judge, a StructureEntry's glob/regex, a
	// Prerequisite's skill/context. Declaration fault.
	ErrExactlyOne = errors.New("declaration: exactly one of a pair must be set")

	// ErrAtLeastOne: a gate or file-guard carries neither `require` nor `checks`,
	// so it would select its subject — a gate waking on an event, a file-guard
	// matching a file — and have nothing to say about it. Declaration fault.
	ErrAtLeastOne = errors.New("declaration: at least one of require/checks must be present")

	// ErrMissingField: a required field is absent or empty — a file-guard's
	// `match`, a context's `enter`/`exit`, a trigger's `event`. Declaration fault.
	ErrMissingField = errors.New("declaration: required field is missing")

	// ErrUnknownContext: a `require: [{context: X}]` names a context X that no
	// declaration defines. A configuration error caught at load — the engine
	// cannot order against a context that does not exist, and the agent cannot fix
	// it by acting. Declaration fault.
	ErrUnknownContext = errors.New("declaration: prerequisite names an unknown context")

	// ErrBadFilesEntry: a `require: [{skill, files}]` prerequisite's `files` is
	// set without `skill` (a subpage of nothing), or one of its entries is not a
	// plain relative path inside the skill's own directory (empty, absolute, or
	// climbing out with `..`). Declaration fault — caught at load, not at
	// runtime, since the shape is checkable without reading the trajectory.
	ErrBadFilesEntry = errors.New("declaration: prerequisite's files entry is invalid")

	// ErrStrayModel: a check sets `model` or `timeout` without a `judge`. Both
	// tune a model call, so a script-only check — which makes no model call and
	// bounds its own runtime — has nothing to apply them to. The field can only
	// be a mistake, refused rather than ignored so the author learns it does
	// nothing. Declaration fault.
	ErrStrayModel = errors.New("declaration: model/timeout set on a check with no judge")

	// ErrBadModel: a check's `model` is not a well-formed modelset — an empty
	// set, or an entry that is empty (a stray or trailing comma). Mirrors what
	// sr-agent's own --model parsing refuses, so a set that would fail at the
	// judge is caught at load instead. Declaration fault.
	ErrBadModel = errors.New("declaration: judge model is not a well-formed modelset")

	// ErrBadTimeout: a check's `timeout` does not parse as a Go duration string,
	// or is <= 0 — a timeout that never fires is not a timeout. Declaration
	// fault.
	ErrBadTimeout = errors.New("declaration: judge timeout is not a positive duration")

	// ErrStrayAllowedTools: a check sets `allowed_tools` without a `judge`. It
	// grants tools to a judge's agent, so a script-only check — which names its own
	// tools by being an executable — has nothing to apply them to. Its own sentinel
	// rather than folded into ErrStrayModel because the fix names a different field.
	// Declaration fault.
	ErrStrayAllowedTools = errors.New("declaration: allowed_tools set on a check with no judge")

	// ErrBadAllowedTools: a check's `allowed_tools` carries an entry that is not
	// one tool rule — empty (a blank name grants nothing), two rules run together
	// in one item, or a scoped rule whose parentheses do not balance. Mirrors
	// ErrBadModel's empty-entry refusal. Declaration fault.
	ErrBadAllowedTools = errors.New("declaration: judge allowed_tools has a malformed entry")

	// ErrStrayDisallowedTools: a check sets `disallowed_tools` without a `judge`.
	// It denies tools to a judge's agent, and a script check has none. Declaration
	// fault.
	ErrStrayDisallowedTools = errors.New("declaration: disallowed_tools set on a check with no judge")

	// ErrBadDisallowedTools: a check's `disallowed_tools` carries an entry that is
	// not one tool rule, for the same reasons as ErrBadAllowedTools. A malformed
	// deny is worse than a malformed allow: it would silently deny nothing.
	// Declaration fault.
	ErrBadDisallowedTools = errors.New("declaration: judge disallowed_tools has a malformed entry")

	// ErrBadValue: a field that takes one of a fixed set of values names
	// something outside it — a file-guard's `deletions:` other than skip /
	// include / only. Refused rather than read as the default, because a typo
	// that silently became `skip` would switch off exactly the deletions the
	// author wrote the key to catch. Declaration fault.
	ErrBadValue = errors.New("declaration: field value is not one of its allowed values")

	// ErrRetiredKey: a declaration carries a key the engine no longer reads and
	// whose silent tolerance would leave a rule unenforced — a file-guard's
	// `preventive:`. Refused by name, with what to write instead. Declaration
	// fault.
	ErrRetiredKey = errors.New("declaration: key was removed")

	// ErrBadScope: a structure gate's `scope` is wrong for where it was found or
	// for what a scope is — a project's own structure declaring one (the
	// project's covers the whole tree), a scope entry that is a regex (glob-only
	// for now), a glob that does not name a folder (no trailing `/`), or one that
	// covers the whole tree (`**/`, `*/`, `/`). Declaration fault.
	ErrBadScope = errors.New("declaration: structure scope is not a folder a plugin may own")

	// ErrOutsideScope: a plugin's structure gate allows or denies a path outside
	// the literal folders its `scope` names. The entry could never decide
	// anything (a plugin's structure is inert outside its scope), so it can only
	// be a mistake — most likely a scope and an entry that disagree about where
	// the plugin's files live. Declaration fault.
	ErrOutsideScope = errors.New("declaration: structure entry lies outside the plugin's scope")
)

The faults a declaration can have. Sentinels so a caller can ask what went wrong with errors.Is rather than matching strings.

Functions

func DefaultStopHookBlockCap

func DefaultStopHookBlockCap() int

DefaultStopHookBlockCap is the cap a project that sets none gets: the current harness's own cap on consecutive Stop blocks (harness.StopBlockCap) when that is smaller than FallbackStopHookBlockCap, since a higher engine cap is one the harness never lets it reach; otherwise FallbackStopHookBlockCap.

func Disabling

func Disabling(problems []Problem) bool

Disabling reports whether these problems stop the rule loading. Environment faults alone do not.

func EnableSubagentStopCheck added in v0.4.0

func EnableSubagentStopCheck(root string) bool

EnableSubagentStopCheck reports whether the project opted in (`enable_subagent_stop_check: true`) to a sub-agent's Stop verifying the tracked ranges. An unreadable config is "not opted in".

func ExpandContextEvent

func ExpandContextEvent(name string) (kinds []string, known bool)

ExpandContextEvent is the exported form of the context `event` expansion, for the context-lifecycle dispatch (the next slice) to turn a loaded context trigger's `event` into concrete kinds. Same reasoning as ExpandGateEvent.

func ExpandGateEvent

func ExpandGateEvent(name string) (kinds []string, known bool)

ExpandGateEvent is the exported form of the gate `event` expansion, for the dispatch that must turn a loaded gate trigger's `event` into the concrete event kinds it fires on.

The loader validates a gate's `on` against this vocabulary but does NOT rewrite the trigger — a Gate.On still carries the `event` string the author wrote, alias and all. So the dispatch resolves it here, against the SAME table the loader validated with, rather than re-implementing the alias mapping and risking the two drifting. A trigger that loaded is always `known` here (the loader would have refused an unknown one); the bool is returned all the same so a caller reaching this with an unvalidated name gets an answer rather than a silent empty.

func IsFileDeleteKind

func IsFileDeleteKind(kind string) bool

IsFileDeleteKind reports whether an event kind is one of the two file-delete kinds, PreFileDelete or PostFileDelete.

func IsLiteralScope

func IsLiteralScope(glob string) bool

IsLiteralScope reports whether a scope glob names one fixed folder — no glob metacharacters — so it can be compared with a plain string prefix. Only literal scopes take part in the load-time checks (entries-inside-scope, overlap between plugins); a wildcard scope's extent is not knowable before a path arrives.

func Messages

func Messages(problems []Problem) []string

Messages renders problems for reporting, one line each.

func ScopeFolder

func ScopeFolder(glob string) string

ScopeFolder is the folder glob a scope entry denotes — the glob as written, less its trailing `/`. `.mdmap/` denotes the folder `.mdmap`; `**/.adr/` denotes every folder named `.adr` at any depth. A written path lies in the scope when one of its ANCESTOR folders matches this glob.

func StopHookBlockCap

func StopHookBlockCap(root string) (int, error)

StopHookBlockCap reads the project's `stop_hook_block_cap` from the config in root (a `.sloprail` directory): how many consecutive refusals a Stop may take before the engine stops judging it and lets the turn end.

A refused Stop is judged again on every retry, so an agent cannot end a turn by replying twice — it loops until its reply passes. The cap is the escape valve, and it is a PROJECT decision, written where the project's other overrides are rather than in an environment variable nobody reviews:

stop_hook_block_cap: 8   # default — refuse up to 8 times in a row
stop_hook_block_cap: 1   # refuse once, then let the retry end un-judged
stop_hook_block_cap: 0   # no engine cap (the harness's own cap still applies)

Absent means DefaultStopHookBlockCap (the harness's own cap when smaller than 8, else 8). A negative value is an error, and so is a config that exists and cannot be read; the caller decides what an error costs.

func SubagentThresholds added in v0.4.0

func SubagentThresholds(root string) (silent, stale time.Duration, err error)

SubagentThresholds reads how long a background sub-agent may stay silent (no hook of its own, no new line in its own transcript) before the parent's Stop says so, and before it stops being waited for and its ranges are judged:

subagent_silent_after_minutes: 10   # default: the Stop names it, its ranges stay unjudged
subagent_stale_after_minutes: 60    # default: it is marked stale and its ranges are judged

A display and escalation policy, not a correctness shortcut: CI verify covers every range either way. A silent threshold of 0 or less, or a stale one not above it, is an error, and the caller gets the defaults with it.

Types

type Check

type Check struct {
	// Script is a deterministic executable, resolved relative to the rule's
	// folder. Receives the payload on stdin and exits with a pass/fail code.
	// Exactly one of Script / Judge is set.
	Script string `yaml:"script"`

	// Judge is a model judgement, a Jinja2 prompt template (`.md.j2`) resolved
	// relative to the rule's folder. Exactly one of Script / Judge is set.
	Judge string `yaml:"judge"`

	// Prepare is an optional deterministic executable that runs before the check
	// (a Judge or a Script). What it returns under `additionalContext` is added to
	// the judge's prompt variables, or to the script's payload. It only builds context:
	// it names no fingerprint, and neither its output nor the rendered prompt is part of
	// the verdict's cache key (a `subjects:` script supplies a fingerprint).
	Prepare string `yaml:"prepare"`

	// Model is which model a Judge asks, in the same modelset format sr-agent's
	// `--model` takes (a `size-xs`..`size-xxl` alias, a concrete harness model
	// name, or a comma-separated preference list). Empty means the engine's
	// default (size-md). Only meaningful alongside a Judge — a script has no
	// model to choose — so Model set on a script-only check is a load error the
	// validator refuses. See
	// dot-dir-file-store/main.tsp Check.model.
	Model string `yaml:"model"`

	// Timeout is how long a Judge may take before it is killed and read as a
	// refusal, overriding the engine's default (defaultCheckTimeout, 600s). A Go duration string
	// (`45s`, `2m`, `1m30s`) parsed with time.ParseDuration; the validator
	// refuses one that does not parse or is <= 0. Only meaningful alongside a
	// Judge — a script's own runtime is the author's to bound — so Timeout set
	// on a script-only check is a load error. See
	// dot-dir-file-store/main.tsp Check.timeout.
	Timeout string `yaml:"timeout"`

	// AllowedTools are the tools a Judge's agent is permitted to use, threaded to
	// sr-agent's `--allowed-tools` (which maps to claude's own `--allowed-tools`).
	// A judge that must Read the file it judges, or WebFetch a URL it verifies
	// against, names those here; the engine's judge substrate already grants the
	// Write the verdict file needs, so this is only the tools the RUBRIC's own
	// work requires. Only meaningful alongside a Judge — a script names its own
	// tools by being an executable — so AllowedTools set on a script-only check is
	// a load error, mirroring the stray-model/timeout rule. See
	// dot-dir-file-store/main.tsp Check.allowed_tools.
	AllowedTools []string `yaml:"allowed_tools"`

	// DisallowedTools are harness permission rules the Judge's agent is DENIED,
	// threaded to sr-agent's `--disallowed-tools` (claude's own
	// `--disallowed-tools`). A deny beats every allow, so this is how a rule that
	// grants a command family — `Bash(curl:*)` — takes back the forms of it the
	// judge must not use (`Bash(curl * -o *)`). Each entry is one rule in the
	// harness's own syntax, parentheses and spaces included. Judge-only, like
	// AllowedTools.
	DisallowedTools []string `yaml:"disallowed_tools"`
}

Check is one check in a rule's ordered list (dot-dir-file-store/main.tsp Check). Exactly one of Script or Judge is set — a rule needing both deterministic and model judgement lists two checks. Prepare is optional and only meaningful alongside Judge: it runs before the judge and adds to what the prompt template sees.

Both Script and Prepare receive the nature's CheckPayload on stdin; a Judge's prompt renders against the nature's judge-input shape (FileJudgeInput on a file-guard, GateJudgeInput on a gate). See payload.go for those shapes.

type CheckPayload

type CheckPayload struct {
	// Event is the file event — a FileEvent variant (always a Post*: a file-guard
	// judges the settled file at Stop and never sees a Pre*). A FlatEvent, so its `kind` and fields are
	// read FLAT under `event` (`.event.path`, `.event.newContent`,
	// `.event.newMarkers`) the way the spec models it and every example reads it —
	// not through the nested `{kind, fields}` envelope the old format used.
	Event FlatEvent `json:"event"`

	// TranscriptPath names the session record a check reads for what the event
	// does not carry.
	TranscriptPath string `json:"transcriptPath"`
}

CheckPayload is what a file-guard's script and prepare receive on stdin, and what a judge receives when no prepare is set (dot-dir-file-store/main.tsp CheckPayload). `event` carries the file's own state; `transcriptPath` names the session record a check can query for context the event itself does not carry (which human message grounds this write). It carries no `context`: a file-guard judges committed bytes and cannot see session state.

type CitationPrerequisite added in v0.3.0

type CitationPrerequisite struct {
	// SourceTypes are the pools a citation may have resolved in — cite's
	// --source-types vocabulary. Required and non-empty.
	SourceTypes []string `yaml:"source_types"`
}

CitationPrerequisite is `require: [{citation: {source_types: [...]}}]`: the action must carry at least one citation that resolved in one of the named pools.

A citation travels on the ACTION, never in the content: an `sr-file write|edit|delete ... --cite:<pool> '<quote>'` invocation grounds the file it changes, and a `sr-session trajectory cite '<quote>' && <command>` chain grounds everything the command does. sloprail resolves each quote against the session's own record before the rule sees it, so this checks EXISTENCE — a real entry in the named pool. Whether the quote actually grounds the change is for the rule's judge, which reads `event.citations`.

The pools are always named: there is no shorthand and no default, so a rule reads as exactly what it accepts.

func (CitationPrerequisite) Pools added in v0.3.0

Pools is SourceTypes as typed pools. Names are validated at load, so an unknown one never reaches here from a loaded rule.

func (*CitationPrerequisite) UnmarshalYAML added in v0.3.0

func (c *CitationPrerequisite) UnmarshalYAML(n *yaml.Node) error

UnmarshalYAML accepts only a mapping with `source_types`. An unknown key is refused rather than ignored, so `citation: {sourceTypes: [tool_result]}` cannot quietly load as something else.

type Context

type Context struct {
	// Name is the context's name, its folder's name, and its key in the `context`
	// map. Not a YAML field.
	Name string `yaml:"-"`

	// On are the events that can (re-)run `enter` — the cheap first stage. Wider
	// than a gate's (ContextEventKind): a context may wake on the Post file
	// events and PostTagWrite, because it sometimes must recognise itself from a
	// file's SETTLED content. A trigger's `event` may be the alias `PreFileWrite`
	// or `PostFileWrite`, expanded to the matching Create + Update pair.
	On []ContextTrigger `yaml:"on"`

	// Require are preconditions that must hold before `enter` runs — see
	// Prerequisite. Optional.
	Require []Prerequisite `yaml:"require"`

	// Enter is the script that decides whether to (re-)activate and what the
	// context's payload should be. Runs on EVERY `on` trigger, active or not.
	// Required and non-empty.
	Enter string `yaml:"enter"`

	// Exit is the script that decides whether the context is done, consulted on a
	// Stop while active. Its verdict only flips this context's own `active`; it
	// does not refuse the Stop. Required and non-empty.
	Exit string `yaml:"exit"`

	// Dir is the context's own folder. Not a YAML field.
	Dir string `yaml:"-"`

	// Origin is where this context was found — the project's own `.sloprail`, or a
	// plugin that ships it. Not a YAML field; the loader fills it. The zero value
	// is a project's own context.
	Origin Origin `yaml:"-"`
}

Context is a context declaration: an activatable scope with a lifecycle, the third rule nature (dot-dir-file-store/main.tsp ContextDeclaration). Where a file-guard hangs on a file's state, a context hangs on nothing until it is entered — then it is a scope the agent is inside, contributing an entry in the `context` map that guards naming it can read. Purely lifecycle tracking: a context does NOT block a Stop itself; a gate bound to Stop does.

Stored at `context/<name>/context.yaml`.

func (Context) Attribution

func (c Context) Attribution() string

Attribution is this context's name as a diagnostic should carry it — bare for a project's own, "…" plus " from plugin X" for a shipped one.

func (Context) Qualified

func (c Context) Qualified() string

Qualified is this context's disable key, `<plugin>/context/<name>` for a shipped context and `context/<name>` for a project's own.

type ContextEnterPayload

type ContextEnterPayload struct {
	// Event is the trigger that fired — a ContextEventKind variant. A FlatEvent,
	// read FLAT under `event` (`.event.newContent`, `.event.path`, `.event.kind`,
	// `.event.tags`) the way the context example scripts read it.
	Event FlatEvent `json:"event"`

	// TranscriptPath names the session record enter reads.
	TranscriptPath string `json:"transcriptPath"`

	// Gates is every declared gate's most recent verdict, by name.
	Gates map[string]natures.GateState `json:"gates"`

	// CurrentContext is this context's own last state — enter runs on every
	// trigger, active or not, so it needs to see what was there.
	CurrentContext natures.ContextState `json:"currentContext"`
}

ContextEnterPayload is what a context's `enter` script receives on stdin (dot-dir-file-store/main.tsp ContextEnterPayload). `event` is the `on` trigger that fired — a ContextEventKind, since entering can be a Post event; `transcriptPath` names the session record the script reads to recognise "now it's the time" and extract the payload; `gates` lets enter gate its own activation on another gate's verdict; `currentContext` is THIS context's own last `{active, payload}`, so the script sees what was there before deciding whether to grow it, replace it, or leave it alone.

type ContextExitPayload

type ContextExitPayload struct {
	// Event is always the Stop that triggered the exit check. A FlatEvent, read
	// FLAT under `event` — a Stop carries only `.event.kind`, but the shape is the
	// same flat one every other payload uses so a script reads it uniformly.
	Event FlatEvent `json:"event"`

	// TranscriptPath names the session record exit reads.
	TranscriptPath string `json:"transcriptPath"`

	// CurrentContext is this context's own entry from the `context` map.
	CurrentContext natures.ContextState `json:"currentContext"`

	// Gates is every declared gate's most recent verdict, by name.
	Gates map[string]natures.GateState `json:"gates"`
}

ContextExitPayload is what a context's `exit` script receives on stdin (dot-dir-file-store/main.tsp ContextExitPayload). `event` is always the Stop that triggered the check — exit is never consulted on anything else — plus `transcriptPath`; `currentContext` is THIS context's own entry from the `context` map; and `gates`, every declared gate's most recent verdict, is how the paired context reads a gate's own verdict back to decide its own active/inactive.

type ContextTrigger

type ContextTrigger struct {
	// Event is the event kind. May be the config-level alias `PreFileWrite` or
	// `PostFileWrite`, each expanded to the matching Create + Update pair on load.
	Event string `yaml:"event"`

	// Match is an optional expression over the event's fields; absent means every
	// occurrence. Scoped to the context scope for whichever kind Event names — the
	// event's fields under `event` (`any(event.tags, …)`, `event.path startsWith …`).
	Match string `yaml:"match"`
}

ContextTrigger is one event a context wakes on to enter, optionally narrowed by a cheap expression (dot-dir-file-store/main.tsp ContextTrigger). Wider than a gate's: `event` is typed to ContextEventKind, which includes the Post file events and PostTagWrite, because a context sometimes must recognise itself from a file's SETTLED content.

type Deletions

type Deletions string

Deletions is a file-guard's `deletions:` value — whether the guard is asked about a file that was deleted. The three values are the whole vocabulary; the loader refuses anything else (ErrBadValue), so a typo cannot quietly become the default.

const (
	// DeletionsSkip: a deleted file is not this guard's business. It is not run
	// on PreFileDelete or PostFileDelete — only on creates and updates. The
	// DEFAULT, and what an absent key means: a guard that validates content has
	// nothing to validate once the content is gone, and asking it anyway hands
	// it an event with no newContent that each guard would otherwise have to
	// recognise and wave through on its own.
	DeletionsSkip Deletions = "skip"

	// DeletionsInclude: creates, updates AND deletes. For a guard whose rule
	// covers losing the file as well as changing it — "nothing under memories/
	// is removed without an ask", "an invariant-pinned file may not silently
	// disappear".
	DeletionsInclude Deletions = "include"

	// DeletionsOnly: deletes only. The guard is not run on creates or updates —
	// for a rule that exists purely to catch a file going away. On a delete a
	// check reads `oldContent` / `oldMarkers` (what is being or was lost); there
	// is no `newContent`.
	DeletionsOnly Deletions = "only"
)

func (Deletions) Mode

func (d Deletions) Mode() Deletions

Mode is the effective value: the one written, or DeletionsSkip when the key was absent.

type Env

type Env struct {
	// Registry is the module registry, for the field declarations a trigger's
	// `match` is compiled against. A gate/context trigger's `match` reads the
	// fired event's fields under `event`, and those fields are the kind's — so the
	// scope must be built from the kind's KindDecl, which the registry holds.
	Registry *module.Registry

	// Contexts is the set of declared context names, for resolving a
	// `require: [{context: X}]`. Built by the loader from the context folder
	// before any declaration is validated, because a prerequisite may name a
	// context declared in a different folder than the rule that requires it.
	Contexts map[string]bool
}

Env is what validation needs beyond the declaration itself: the event vocabulary, to know a kind's fields when compiling a trigger's `match` against its scope, and the set of context names a `require: [{context}]` may resolve to.

Passed in rather than discovered here for the reason internal/guardrail passes its registry in: this package's job is to validate declarations, and which modules a build ships and which contexts a project declared are facts assembled elsewhere (modules.Registry, and the load of the context folder). A validator that reached for them would couple to how they are found.

type Fault

type Fault int

Fault says whether a problem is with what the declaration SAYS or with the machine it was found on — the same distinction internal/guardrail draws, and for the same reason: it decides which way the rule fails when we get it wrong.

  • A DECLARATION fault means the rule could never fire correctly for anyone. No runtime saves it, so loading it buys nothing and it is refused.
  • An ENVIRONMENT fault means the rule knows what it wants and cannot currently do it (a script that is not executable). A runtime path handles that by refusing the action, so refusing to LOAD would throw away a working safety mechanism to punish a missing chmod.

This slice LOADS and VALIDATES declarations; it does not run their scripts, so it does not itself raise environment faults (a not-executable script is discovered by the dispatch slice, not here). The distinction is kept anyway, so this package's Problem is the same shape internal/guardrail's is and a caller reporting both can treat them uniformly, and so a later check that does touch the filesystem has the category ready.

const (
	// FaultDeclaration: the declaration is wrong. Refuse the rule.
	FaultDeclaration Fault = iota

	// FaultEnvironment: the declaration is right and the machine is not. Report
	// it and load the rule anyway.
	FaultEnvironment
)

type FileGuard

type FileGuard struct {
	// Name is the guard's name and its folder's name. Taken from the folder
	// rather than the file, the same reasoning the old loader records: a name
	// recorded twice is a name that can disagree with itself. Not a YAML field.
	Name string `yaml:"-"`

	// Match is which files this guard covers, and under which contexts. A
	// FileMatchExpression: either a bare glob (the common "this path" case) or a
	// full expression over a file's own facts — its path, the markers it carries,
	// and `context[<name>]`. Compiled against the file scope at load, so a bad
	// expression is refused with a diagnostic naming it rather than silently
	// never matching.
	Match string `yaml:"match"`

	// HadPreventiveKey records that the RETIRED `preventive:` key was present,
	// whatever its value. A file-guard judges only the settled result at Stop;
	// preventing a write is a gate's job (a PreFileWrite or PreFileDelete gate).
	// The field exists only so the loader can SEE the key and refuse the
	// declaration by name (ValidateFileGuard, ErrRetiredKey) — unknown keys are
	// otherwise tolerated, and a `preventive: true` quietly ignored would leave
	// the write it was written to stop unguarded. Nothing else reads it.
	HadPreventiveKey bool `yaml:"-"`

	// Deletions says whether a DELETED file is this guard's business: `skip`
	// (the default, and what an absent key means), `include`, or `only`. See
	// Deletions for the three values.
	//
	// One axis with three values, NOT a list of events — the same reasoning that
	// keeps a file-guard's other axes small. A file-guard binds to a
	// file's STATE, not to events; the one place the state question genuinely
	// forks is a file that no longer exists, which has no end state, no
	// newContent and no newMarkers. Most guards validate content and have nothing
	// to say about a file that is gone (so they skip it); a few exist precisely
	// to catch the loss (so they include it, or look at nothing else). An `on:`
	// list would reopen every create/update/delete combination a state rule has
	// no use for, so it is deliberately not offered.
	Deletions Deletions `yaml:"deletions"`

	// Require are preconditions that must hold before a guarded write is
	// permitted — see Prerequisite. Optional.
	Require []Prerequisite `yaml:"require"`

	// Subjects is an optional script, resolved from the rule's folder, that splits the
	// selected files into the units the rule is judged on, each cached on its own. Run with
	// the changeset payload on stdin and no session; prints a JSON array of
	// {"id", "files", "fingerprint"} (fingerprint optional: what that subject's verdict depends
	// on beyond its files' content). Absent: one subject made of every selected file.
	Subjects string `yaml:"subjects"`

	// Checks are the checks a guarded file must pass, in order. Each is a script
	// or a judge (both may appear across the list), and order is significant: a
	// cheap deterministic script runs first and can settle the matter before a
	// judge is paid for. The first check that refuses ends it.
	Checks []Check `yaml:"checks"`

	// Dir is the guard's own folder, so its scripts and judge prompts resolve
	// relative to it. Not a YAML field — the loader fills it from where the file
	// was found.
	Dir string `yaml:"-"`

	// Origin is where this guard was found — the project's own `.sloprail`, or a
	// plugin that ships it. Not a YAML field; the loader fills it from which root it
	// read the guard from, so a refusal can name the plugin a shipped guard came
	// from (see Origin). The zero value is a project's own guard.
	Origin Origin `yaml:"-"`
}

FileGuard is a file-guard declaration: a rule bound to a FILE'S STATE, not to an event (dot-dir-file-store/main.tsp FileGuardDeclaration). A guarded file must be FINE, and the check is the same whether the file was created or modified — a failing guard does not reject once; the file stays not-fine and the guard keeps failing every cycle until its content satisfies the checks.

Stored at `file-guard/<name>/file-guard.yaml`, with the scripts and judge prompts it names living as siblings in the same folder.

func (FileGuard) Attribution

func (g FileGuard) Attribution() string

Attribution is this guard's name as a refusal should carry it — the bare name for a project's own, and the name plus " from plugin X" for a shipped one, so the agent hears where a rule it cannot find in its tree actually lives. Mirrors guardrail.Declaration.Attribution.

func (FileGuard) Covers

func (g FileGuard) Covers(kind string) bool

Covers reports whether this guard is asked about a file event of the given kind, as its `deletions:` value decides: a delete kind only when the guard includes deletions (include / only), and a create or update kind unless the guard is deletions-only. Any other kind is not a file event and is answered true — this filter has no opinion on it; whether a guard runs on it at all is the dispatch's to decide.

The ONE place the filter is written, so the after-check (Post) path and the event-extraction binding cannot disagree about which events a guard sees.

func (FileGuard) Qualified

func (g FileGuard) Qualified() string

Qualified is this guard's disable key, `<plugin>/file-guard/<name>` for a shipped guard and `file-guard/<name>` for a project's own — what a consumer writes in `disabled:` to switch it off.

func (FileGuard) Root added in v0.4.0

func (g FileGuard) Root() string

Root is the `.sloprail` directory this guard was loaded from — the project's own or a plugin's: `<root>/file-guard/<name>`. The guard's rule hash does not read it: it covers the guard's own folder (g.Dir), the files git tracks there (changeset.RuleHashAt); the floor of its range is over the guard's folder too.

type FileJudgeInput

type FileJudgeInput struct {
	// CheckPayload's fields render unprefixed at the template's top level
	// (`{{ event.newContent }}`, `{{ transcriptPath }}`, `{{ context }}`).
	CheckPayload `json:",inline"`

	// Change is the unified diff from the event's oldContent to its newContent
	// (`{{ change }}`): what THIS change does to the file — at pre-tool the
	// pending write, at Stop everything since the session baseline. A judge of a
	// grounded change rules on it, not the whole file: a citation grounds the
	// change it was made with, and lines the change leaves alone were grounded,
	// or not, when they were written. Empty when the event carries no content.
	Change string `json:"change"`

	// AdditionalContext is present only when the check's own prepare returned one.
	AdditionalContext PreparedContext `json:"additionalContext,omitempty"`
}

FileJudgeInput is the wire/type contract for what a file-guard's judge receives (dot-dir-file-store/main.tsp FileJudgeInput): the standard CheckPayload spread flat at the template's top level, plus `additionalContext` set exactly when the check's own prepare ran and returned one. `payload` is ALWAYS present, never replaced — the standard payload and prepare's addition are additive, not either/or. A prepare script cannot overwrite `event` or `transcriptPath` by choosing a colliding key: its return renders under the single top-level `additionalContext` field, not spread into the payload's own namespace.

type FlatEvent

type FlatEvent event.Event

FlatEvent is an event.Event serialized FLAT — the on-the-wire shape a check's script and a judge's template read `event` as (dot-dir-file-store/main.tsp).

Why flat, and why a distinct type

The engine's event.Event marshals through its own envelope as `{"kind":…, "fields":{…}}` — the shape the OLD-format hooks read (`.event.fields.path`). The NEW declaration formats read the event FLAT: the spec's judge templates say `{{ event.newContent }}` and `{{ event.path }}` (main.tsp ~338), its CheckPayload doc names `event.newMarkers`/`event.oldMarkers` (~299), and every shipped example script and template reads `.event.newContent` / `.event.path` / `.event.kind` / `.event.newMarkers` directly — never under a `fields` sub-object. So the payloads these declarations receive must carry the event's own fields DIRECTLY under `event`, with `kind` alongside them: `{"kind":"PreFileUpdate","path":"…","newContent":"…","newMarkers":[…]}`.

A distinct type rather than a marshal option on the payload structs, because the flatness is a property of the EVENT's wire form specifically — `transcriptPath`, `context` and `gates` are already flat as ordinary fields, and only `event` needs its envelope opened up. Making it a type means every payload that carries an event gets the same flat shape by construction (its field is a FlatEvent), and a caller cannot accidentally reintroduce the nested envelope by marshaling a bare event.Event into an `event` key. The underlying struct is event.Event's, so a caller converts with FlatEvent(e) at no cost.

func (FlatEvent) Event

func (e FlatEvent) Event() event.Event

Event returns the flat event as an ordinary event.Event, for a Go consumer that wants the envelope form back after reading a payload.

func (FlatEvent) MarshalJSON

func (e FlatEvent) MarshalJSON() ([]byte, error)

MarshalJSON writes the event flat: its declared fields spread at the top level, with `kind` alongside them.

A nil Fields map is an event that carries nothing but its kind (a Stop), which marshals to `{"kind":"Stop"}` — an object, never a null, so a script indexing `.event.<anything>` gets a clean miss rather than an error, the same "always an object" discipline the event envelope keeps.

func (*FlatEvent) UnmarshalJSON

func (e *FlatEvent) UnmarshalJSON(data []byte) error

UnmarshalJSON reads a flat event back into {Kind, Fields}: `kind` becomes Kind and every other key becomes a field. The inverse of MarshalJSON, so a payload written by this package round-trips — which is what a test asserting on an assembled payload, and any Go consumer reading one back, relies on.

type Gate

type Gate struct {
	// Name is the gate's name and its folder's name. Not a YAML field.
	Name string `yaml:"-"`

	// On are the events this gate wakes on — pre-action only (GateEventKind). A
	// trigger's `event` may be the alias `PreFileWrite`, which the loader expands
	// to PreFileCreate + PreFileUpdate. Each trigger's optional `match` is
	// compiled against the gate scope for the kind it names.
	On []GateTrigger `yaml:"on"`

	// Require are preconditions that must hold before the gated action is
	// permitted — the same Prerequisite a file-guard uses. Optional, BUT a gate
	// must carry at least one of Require / Checks (a gate with neither would wake
	// and do nothing).
	Require []Prerequisite `yaml:"require"`

	// Checks are the checks the gated action must pass, in order — the same Check
	// shape a file-guard uses. Optional, subject to the same at-least-one rule as
	// Require.
	Checks []Check `yaml:"checks"`

	// Enabled, when false, ships the gate OFF: it is inert until the project lists its
	// qualified name under `enabled:` in `.sloprail/config.yaml`. Absent means on.
	Enabled *bool `yaml:"enabled"`

	// Dir is the gate's own folder. Not a YAML field.
	Dir string `yaml:"-"`

	// Origin is where this gate was found — the project's own `.sloprail`, or a
	// plugin that ships it. Not a YAML field; the loader fills it. The zero value
	// is a project's own gate.
	Origin Origin `yaml:"-"`
}

Gate is a gate declaration: a checkpoint on an event (dot-dir-file-store/main.tsp GateDeclaration). It wakes on a pre-action event, evaluates its Require and Checks, and passes or blocks the action. One-shot — it decides at the moment and is done, unlike a file-guard that re-fires until a file is fine.

Stored at `gate/<name>/gate.yaml`.

func (Gate) Attribution

func (g Gate) Attribution() string

Attribution is this gate's name as a refusal should carry it — bare for a project's own, "…" plus " from plugin X" for a shipped one.

func (Gate) Qualified

func (g Gate) Qualified() string

Qualified is this gate's disable key, `<plugin>/gate/<name>` for a shipped gate and `gate/<name>` for a project's own.

type GateCheckPayload

type GateCheckPayload struct {
	// Event is the fired pre-action event — a GateEventKind variant. A FlatEvent,
	// read FLAT under `event` (`.event.path`, `.event.invocations`) the way the
	// spec models it — see CheckPayload.Event.
	Event FlatEvent `json:"event"`

	// TranscriptPath names the session record for the trajectory-reading a gate's
	// checks usually do.
	TranscriptPath string `json:"transcriptPath"`

	// Context is every declared context, by name.
	Context map[string]natures.ContextState `json:"context"`
}

GateCheckPayload is what a gate's script, prepare and judge receive on stdin (dot-dir-file-store/main.tsp GateCheckPayload). Like a file-guard's CheckPayload but its `event` is any GateEventKind — a gate wakes on command, tool and Stop events too, never a Post variant. `context` is carried at top level, at parity with the gate's match scope, so a gate's own checks can read what an upstream context left behind rather than reconstruct it.

type GateJudgeInput

type GateJudgeInput struct {
	// GateCheckPayload's fields render unprefixed at the template's top level.
	GateCheckPayload `json:",inline"`

	// AdditionalContext is present only when the check's own prepare returned one.
	AdditionalContext PreparedContext `json:"additionalContext,omitempty"`
}

GateJudgeInput is the gate-nature counterpart to FileJudgeInput (dot-dir-file-store/main.tsp GateJudgeInput). Same rules: GateCheckPayload's own fields spread unprefixed at the template's top level, always present, with `additionalContext` only adding to that, never replacing it.

type GateTrigger

type GateTrigger struct {
	// Event is the event kind. May be the config-level alias `PreFileWrite`,
	// which the loader expands to PreFileCreate + PreFileUpdate. No `PostFileWrite`
	// alias here — a gate does not wake on settled content.
	Event string `yaml:"event"`

	// Match is an optional expression over the event's fields; absent means every
	// occurrence of the kind. Scoped to the gate scope for whichever kind Event
	// names, so the event's fields are read under `event` (`event.path`,
	// `any(event.invocations, …)`).
	Match string `yaml:"match"`
}

GateTrigger is one event a gate wakes on, optionally narrowed by a cheap expression over that event's own fields (dot-dir-file-store/main.tsp GateTrigger). Typed to GateEventKind — the pre-action subset, narrower than a context's (no Post events; a gate that already happened is too late to gate).

type Invalid

type Invalid struct {
	// Nature says which of the formats this was — so a diagnostic can say
	// "gate X" rather than a bare name that collides across natures (a gate and a
	// context may both be named `people-linked`).
	Nature Nature

	// Name is the declaration's name, from its folder. Empty for a structure gate,
	// which has no per-name folder.
	Name string

	// Origin says where this unloadable declaration was found. A broken rule needs
	// attribution more than a working one does, not less: the refusal it causes
	// tells an author to go and fix a file, and for a plugin's rule that file is not
	// in their project and not theirs to fix. The zero value is a project's own.
	// Mirrors guardrail.Invalid.Origin.
	Origin Origin

	// Path is where the unloadable declaration was found, so an author can open
	// the exact file.
	Path string

	// Problems is every fault found, each carrying a Kind a caller can branch on
	// with errors.Is.
	Problems []Problem

	// Reasons is Problems rendered one line each, and Reason is those joined —
	// derived at construction so they cannot drift from the list they summarise.
	Reasons []string
	Reason  string
}

Invalid is a declaration that could not be read or could not do what it says, and why. Modelled on internal/guardrail's Invalid: a broken rule needs attribution (which nature, which name) and every fault found, not the first, so an author fixing a declaration sees all of it at once rather than one reload per mistake.

func (Invalid) Attribution

func (iv Invalid) Attribution() string

Attribution is this broken declaration's name as a refusal or diagnostic should carry it — bare for a project's own, plus " from plugin X" for a shipped one, so a report about a rule that will not load names where the file lives. Mirrors guardrail.Invalid.Attribution.

func (Invalid) Qualified

func (iv Invalid) Qualified() string

Qualified names this declaration as its disable key — "<nature>/<name>" for a project's own, "<plugin>/<nature>/<name>" for a shipped one — the stable key a diagnostic and a sort use, and the one a consumer writes to switch a broken plugin declaration off (see Config). Unique across natures where a bare name is not, and across plugins where a bare nature/name is not.

Delegates to Origin.Qualified so a broken declaration and a sound one of the same identity produce the SAME key — the disable list must reach a declaration whose contents never parsed, so its key cannot depend on anything inside the file, only on its origin, nature and folder name (all known from the path).

func (Invalid) Remedy

func (iv Invalid) Remedy() string

Remedy is what to tell someone whose declaration could not load, and it differs by WHO OWNS THE FILE — the origin-aware repair guidance the old format carried on session_pre_tool.go's remedy, restored here beside Shadow.Message so the two diagnostics point at the same `disabled: [...]` mechanism with one wording.

Why it varies by owner

For a project's own rule the advice has always been "fix the declaration, or disable it" and both halves are actionable: the file is in the tree, the author wrote it, and switching it off is one edit away.

For a plugin's rule that advice is a trap. The declaration sits in an install cache the consumer did not write and must not edit — an edit there is silently undone by the next reinstall, so an author who followed it would fix the report, upgrade, and have the fault come back with no explanation. So a plugin's rule gets the mechanism that is genuinely theirs: `disabled: [<qualified>]` in their own config, the same key and file Shadow.Message quotes for a shipped rule they meant to switch off. The exact line is quoted, because advice the reader has to go and look up is advice they skip.

Why it varies again by whether it PARSED

A project's declaration that merely failed validation is a rule with a mistake in it, so the advice is to fix or disable it. One that could not be PARSED might not be a declaration at all — a stray file, a note left in the folder — so "remove that folder if it is not a declaration" is a real way out the merely-invalid case does not have. A plugin's rule collapses both: the consumer can neither edit nor remove a file inside an install cache, so the disable list is the one remedy either way.

This text accompanies a REPORT, not a refusal — an invalid declaration blocks nothing. That makes the wording matter more, not less: it is the only thing between a rule that silently stopped enforcing and a person who fixes it, so it has to name a remedy the reader can actually perform. sr:invariant loading/one-broken-rule-disables-only-itself

type Loaded

type Loaded struct {
	// FileGuards are the loaded file-guard declarations, sorted by name.
	FileGuards []FileGuard

	// Gates are the loaded gate declarations, sorted by name.
	Gates []Gate

	// Contexts are the loaded context declarations, sorted by name.
	Contexts []Context

	// Structures are the loaded structure gates — the project's own first (when
	// it declares one), then each enabled plugin's in precedence order. They are
	// COMBINED, not resolved: none shadows another. The project's covers the
	// whole tree; each plugin's covers only its declared `scope`. How they combine
	// on a written path is internal/dispatch's StructureSet.Decide.
	Structures []StructureGate

	// ScopeOverlaps are pairs of loaded plugin structures whose LITERAL scopes
	// overlap. Both stay loaded (a write in the overlap is refused at write time
	// as an ownership conflict); the overlap is reported so it is seen before an
	// agent meets it.
	ScopeOverlaps []ScopeOverlap

	// Degraded are declarations that ARE loaded and enforced but carry an environment
	// fault (a declared script that lost its shebang or execute bit): reported where
	// Invalid is, never dropped, because a rule that stopped loading would stop
	// refusing. At run time the exec path refuses what such a rule guards until the
	// file is fixed. Only declarations that are in force: one displaced (Shadowed) or
	// disabled is neither loaded nor enforced, so it is not reported here. Sorted by
	// qualified name.
	Degraded []Invalid

	// Invalid are the declarations that could not be loaded, across every nature,
	// sorted by their qualified name. Reported rather than fatal: the engine loads
	// every sound declaration and refuses only the ones that are not.
	Invalid []Invalid

	// Shadowed are plugin declarations a declaration of the same (nature, name)
	// displaced (never a structure gate — those combine rather than shadow) — the project's own, or an earlier-listed plugin's. Returned rather
	// than applied silently, for the reason guardrail.Resolution.Shadowed is: a
	// project that believes it has two protections and has one is the failure this
	// product exists to prevent. The precedence IS applied (the winner is what
	// loads); the consumer is merely told which rule they displaced and where it
	// lives, so overriding stays a choice rather than an accident. See Shadow.
	Shadowed []Shadow
}

Loaded is everything a load produced: the declarations in force, by nature, and the ones that could not be loaded.

One field per nature rather than a single heterogeneous list, because the dispatch slice consumes them by nature — it matches file events against file-guards, wakes gates on their triggers, enters contexts on theirs — and a typed field per nature is what lets it take each without a type switch.

func (Loaded) PluginStructures

func (l Loaded) PluginStructures() []StructureGate

PluginStructures are the loaded plugin structure gates, in precedence order.

func (Loaded) ProjectStructure

func (l Loaded) ProjectStructure() *StructureGate

ProjectStructure is the project's own structure gate, or nil when the project declares none (or disabled it).

type Nature

type Nature string

Nature names which of the rule natures a declaration is, so a loaded declaration says what it is without a caller having to type-switch on the concrete struct. The three file-backed natures plus the structure primitive each have one. A goal has none: it is a composite realised on the user's side (see the package doc), not a thing this loader reads.

const (
	// NatureFileGuard is a rule bound to a file's settled state.
	NatureFileGuard Nature = "file-guard"

	// NatureGate is a checkpoint on a pre-action event.
	NatureGate Nature = "gate"

	// NatureContext is an activatable scope with a lifecycle.
	NatureContext Nature = "context"

	// NatureStructure is the structure gate — a path allowlist, deny by
	// default. A primitive, not a fourth nature; it has no per-name folder and is
	// loaded from each root's one `file-guard/structure.yaml`: the project's
	// covers the whole tree, each plugin's covers only the `scope` it declares,
	// and all of them combine (see StructureGate).
	NatureStructure Nature = "structure"
)

type Origin

type Origin struct {
	// Plugin is the name of the plugin this declaration shipped inside, or "" for
	// a declaration the project declared itself.
	//
	// Supplied by the resolver, which read it out of the settings key that enabled
	// the plugin, rather than derived here from the directory name. The settings
	// key is what the USER wrote and what they will search for; a cache directory's
	// name is an artefact they never see, and the two can differ. (This is the
	// exact reasoning guardrail.Origin.Plugin records.)
	Plugin string

	// Root is the plugin installation directory this declaration was found under,
	// empty for a project's own. Kept so a diagnostic can name the actual path,
	// which is what an author needs when the rule is not where they would look.
	Root string
}

Origin says where a declaration was found, which is the difference between a rule the project wrote and a rule it installed.

It exists for the same reason guardrail.Origin does: a refusal has to be actionable. "file-guard X refused this" sends an author to .sloprail/file-guard/X, and for a plugin's rule there is nothing there — the file lives in an install directory the project never wrote to. A user meeting a rule they cannot find is a user who concludes the tool is broken, and the honest fix is for the rule to say where it came from.

Note what this type does NOT do: it does not discover plugins. The set of installed plugins is a fact about the project's harness configuration, read in internal/harness, which is allowed to know one harness's layout. This package is handed roots and origins already resolved — the same division guardrail.Origin documents and the same one the harness package's doc insists on.

func (Origin) Describe

func (o Origin) Describe() string

Describe renders the origin for a person reading a refusal or a diagnostic.

Empty for a project's own rule, deliberately — the same choice guardrail.Origin makes: a project author reading their own rule's name already knows where it is, and appending "(from this project)" to every refusal is noise that trains people to stop reading refusals. For a plugin's rule it names the plugin, which is the fact a user needs to find a file that is not in their tree.

func (Origin) FromPlugin

func (o Origin) FromPlugin() bool

FromPlugin reports whether this declaration came from an installed plugin rather than from the project itself.

func (Origin) Qualified

func (o Origin) Qualified(nature Nature, name string) string

Qualified is the declaration's name as a consumer must write it to disable it.

It namespaces on BOTH the plugin AND the nature, `<plugin>/<nature>/<name>` for a plugin's rule, `<nature>/<name>` for a project's own. Two axes, not one, because the new format identifies a declaration by (nature, name) — a gate and a context may both be named `people-linked`, so guardrail.Origin's `<plugin>/<name>` would be ambiguous here in a way it never is for the old format's single flat guardrails/ directory. The nature is therefore part of the disable key, mirroring how Invalid.Qualified already keys `<nature>/<name>` for a project's own declarations; the plugin prefix is the FromPlugin addition, mirroring guardrail.Origin.Qualified.

A project's own declarations are namespaced by nature but not by plugin, because they already live under one `.sloprail` and cannot collide across installs. A plugin's are namespaced by both, because two plugins may reasonably ship a file-guard called `no-secrets` and a disable list that could not tell them apart would switch off a rule the consumer did not mean.

type PreparedContext

type PreparedContext map[string]any

PreparedContext is the freeform context a prepare script's stdout carries under `additionalContext` (dot-dir-file-store/main.tsp PreparedContext): a flat JSON object with no fixed shape, folded into the judge's input under the same key. Named explicitly rather than left as prose, so a prepare script's contract is a real type. It is PURELY template context — it carries no control signal. The judge-or-skip decision travels on a SEPARATE, typed channel (`skip`, read off the same envelope; see PreparedOutcome and dispatch.parsePreparedContext), so that a freeform key an author happens to name cannot be mistaken for a control instruction and the skip decision stays explicit.

type PreparedOutcome

type PreparedOutcome struct {
	// AdditionalContext is the freeform template context, present only when the
	// prepare emitted one AND the judge is going to run. Ignored when Skip.
	AdditionalContext PreparedContext `json:"additionalContext"`

	// Skip, when true, is prepare asking this judge check to ABSTAIN: the judge is
	// not invoked and the check reaches no verdict of its own — other checks decide
	// (see the four-outcome contract above). Absent/false is the unchanged "run the
	// judge" default.
	Skip bool `json:"skip"`
}

PreparedOutcome is the parsed control channel of a prepare script's stdout envelope — the sibling of `additionalContext`, decoded as a dedicated typed signal rather than smuggled through the freeform map.

The prepare contract has FOUR outcomes

A JUDGE check may carry a prepare step that runs first. Its stdout envelope is `{"additionalContext": {...}, "skip": <bool>}`, both keys optional, and it resolves to one of four outcomes:

  1. prepare exits NON-ZERO -> the check FAILS CLOSED (refuse), carrying prepare's own words. (Unchanged; decided by exit code, not this envelope.)
  2. prepare exits ZERO, stdout MALFORMED (present but not this shape) -> the check FAILS CLOSED (refuse). An envelope the engine cannot read must never read as a skip or a half-prepared prompt. (Unchanged.)
  3. prepare exits ZERO, `skip` absent or false -> the judge RUNS, with any `additionalContext` folded into its input. Empty stdout is this case: a prepare that ran, added nothing, and printed nothing still lets the judge run. (Unchanged.)
  4. prepare exits ZERO and `skip` is true -> the judge is NOT invoked and this check ABSTAINS — it reaches NO verdict of its own. It does not refuse and it does not affirmatively pass; it DROPS OUT of the check chain, and the guard's outcome is decided by the OTHER checks. A check listed after it still runs and can still refuse; if no check refuses, the write is permitted by default (the same result reaching the end of the chain with no refusal gives). This is the outcome for a guard whose prepare inspects the subject and finds the judge would be meaningless (e.g. a task-review guard on a task that is not in_review): don't spend a model call, and don't force a verdict — let whatever else the guard says (or the default permit) stand. `additionalContext` is moot when skipping (there is no prompt to fold it into) and is ignored.

ABSTAIN, not permit — the distinction is deliberate and load-bearing. A skip that forced a pass would MASK a later check that would have refused; abstaining means first-refusal still wins across the remaining checks. Skip defaults to false, so the key's ABSENCE preserves outcome (3) exactly — every prepare written before this signal existed keeps letting the judge run. Skip is opt-in and abstain is its only effect, which keeps it a deliberate act rather than an accident of a malformed envelope: outcome (2) above still refuses. Empty stdout is NOT a skip either — it is outcome (3).

SPEC-CHANGE NOTE: `skip` is a NEW addition to the prepare/judge contract. The authoritative spec (dot-dir-file-store/main.tsp PreparedContext) records only the judge-runs / prepare-fails form; this typed ABSTAIN outcome is added here first (the Go type + engine is the source of truth for this change) and the spec's main.tsp must be extended to record the `skip` sibling key and the abstain semantics above — the spec wording must say skip means "this check reaches no verdict; other checks decide", NOT that skip permits or passes.

type Prerequisite

type Prerequisite struct {
	// Skill is the name of a skill that must have been loaded (a real Skill
	// tool_use in the trajectory, not a claim) before the write. Optional.
	Skill string `yaml:"skill"`

	// Files, optional and meaningful only alongside Skill, names one or more
	// files INSIDE that skill (relative to its own directory — the one holding
	// its SKILL.md, e.g. "script-checks.md", "file-guard.md") that must ALSO
	// have been read (a Read tool_use, or a file-reading Bash command) before
	// the write — the same evidence a bare `{skill}` already accepts for the
	// skill's own SKILL.md, extended to its subpages.
	//
	// This exists because loading a skill only guarantees its SKILL.md was
	// read; a skill's detail — a per-event-kind script skeleton, a judge
	// contract — often lives in a page SKILL.md merely links to, and an agent
	// that loaded the skill has no guarantee of ever opening it. Measured
	// against a real onboarding run: every agent loaded authoring-guardrails
	// yet none read file-guard.md, so every check script was written from
	// memory and refused several times before it was right.
	//
	// A relative path with no `..` and no leading `/` — it names a file inside
	// the skill's own directory, never an escape from it. Load error otherwise.
	Files []string `yaml:"files"`

	// Context is the name of a context declaration that must run first, when both
	// this rule and that context wake on the same event in the same cycle.
	// Optional. Validated to resolve against the loaded contexts.
	Context string `yaml:"context"`

	// Citation requires the action to carry a resolved citation. Optional.
	Citation *CitationPrerequisite `yaml:"citation"`

	// When, optional, is a script that decides whether this prerequisite
	// applies to the action at all — for a requirement that is conditional on
	// the change ("a citation, but only when the body changes"). Resolved
	// relative to the rule's folder and handed the same payload on stdin as a
	// script check. Exit 0: the prerequisite applies. Exit 1: it does not, and
	// is skipped. Any other outcome — another exit code, a script that cannot
	// run, a timeout — applies it: a condition that could not be decided must
	// not waive a requirement.
	When string `yaml:"when"`
}

Prerequisite is a precondition that must hold before a rule's own check runs (dot-dir-file-store/main.tsp Prerequisite). A single list carrying three kinds, exactly one field set per entry — like Check's script/judge. What differs between them is not the shape but WHO establishes it and WHEN a violation is discovered:

  • Skill is read from the trajectory at the moment the check runs — a fact the agent may or may not have produced, discovered at runtime and refused with a remedy the agent can act on ("load the skill"). An unknown skill NAME is therefore NOT a load error: the loader has no list of skills to check against, and a skill that does not exist yet is a runtime miss, not a malformed rule.
  • Context names another context declaration the engine must run first when both match the same event — a guarantee the engine gives by ordering, not a fact it goes looking for. Naming one that cannot resolve (unknown name) IS a configuration error caught when the rule loads.
  • Citation is read off the EVENT: the action must carry a citation that resolved in the session's record (see CitationPrerequisite). The agent remedies a miss by making the change the grounded way.

The spec's Prerequisite carries no `gate` field yet — dropped deliberately, no gate-depends-on-gate case exists among the units built so far.

type Problem

type Problem struct {
	// Kind is which sentinel this problem is an instance of. Compare with
	// errors.Is rather than by reading Detail.
	Kind error

	// Fault says whether this disables the rule or only warns about it.
	Fault Fault

	// Where locates the fault within the declaration for a person — "trigger 0",
	// "check 1", "require 0", or "" for a fault about the whole declaration. A
	// free-form breadcrumb rather than the old loader's fixed event/binding/hook
	// triple, because the new formats have several different nested shapes
	// (triggers, checks, prerequisites, structure entries) and one breadcrumb
	// string describes them all without a struct field per nesting.
	Where string

	// Detail is the specific complaint, without the location. Message joins the
	// two.
	Detail string
}

Problem is one thing wrong with a declaration: what kind of fault it is, where in the declaration it sits, and what to say about it.

A Kind (a sentinel error) rather than a bare string, so a caller can react to a class of fault with errors.Is without matching on prose — wording is the part most likely to be improved, and a test keyed to it would break every time it was.

func ValidateContext

func ValidateContext(c Context, env Env) []Problem

ValidateContext reports everything wrong with a context.

A context's distinctive rules: its `on` kinds must be ContextEventKinds (with the PreFileWrite AND PostFileWrite aliases), each trigger's `match` compiles against the CONTEXT scope, and `enter` and `exit` are both required and non-empty (a context with no enter cannot activate; with no exit cannot decide it is done). sr:invariant loading/rule-must-decide-something

func ValidateFileGuard

func ValidateFileGuard(g FileGuard, env Env) []Problem

ValidateFileGuard reports everything wrong with a file-guard, given the validation environment. An empty result means the guard can do what it says.

The order mirrors the struct: the required `match` compiles against the file scope, `deletions` is one of its three values (or absent), each prerequisite is exactly-one-of and resolves, each check is exactly-one-of with a well-placed prepare. A file-guard has no `on` list (it binds to a file's state, not to events). It carries the SAME at-least-one rule a gate does — `require` and `checks` are each optional individually, but a guard with NEITHER would select a file and decide nothing, so that is refused (a file-guard whose whole enforcement is a `require:` precondition is meaningful without any checks — the engine evaluates `require` before any check and refuses the write when it is unmet — so forcing it to carry a pass-through check solely to satisfy the validator was pure boilerplate). sr:invariant loading/rule-must-decide-something sr:invariant loading/retired-file-guard-keys-are-refused sr:invariant loading/trigger-match-compiles-at-load

func ValidateGate

func ValidateGate(g Gate, env Env) []Problem

ValidateGate reports everything wrong with a gate.

A gate's distinctive rules: its `on` kinds must be GateEventKinds (with the PreFileWrite alias), each trigger's `match` compiles against the GATE scope for the kind it names, and it must carry AT LEAST ONE of require/checks — the one rule unique to gates, since a gate with neither would wake and do nothing. sr:invariant loading/rule-must-decide-something

func ValidateStructureGate

func ValidateStructureGate(s StructureGate, _ Env) []Problem

ValidateStructureGate reports everything wrong with a structure gate.

Its rules: `allow` must be present (an absent allowlist denies everything, which is a structure gate that locks the whole project — or the whole of a plugin's scope — out: a mistake worth refusing rather than a deliberate empty allowlist), and every allow/deny entry is exactly-one-of glob/regex, with the pattern compiling. Then the `scope` rules, which depend on WHO declared it (validateStructureScope).

func (Problem) Error

func (p Problem) Error() string

Error makes a Problem usable as an error in its own right.

func (Problem) Is

func (p Problem) Is(target error) bool

Is lets errors.Is(problem, ErrBadMatch) work.

func (Problem) Message

func (p Problem) Message() string

Message is the human-readable line: where the fault is, then what it is.

type ScopeOverlap

type ScopeOverlap struct {
	// A and B are the two plugins, in load (precedence) order.
	A, B Origin

	// ScopeA and ScopeB are the overlapping scope globs as each plugin wrote them.
	ScopeA, ScopeB string
}

ScopeOverlap is two plugins whose LITERAL scopes overlap — the same folder, or one inside the other. Both structures stay loaded; a write inside the overlap is refused at write time as an ownership conflict. Reported at load so the conflict is visible before an agent meets it.

func (ScopeOverlap) Message

func (o ScopeOverlap) Message() string

Message renders an overlap for a person, naming both plugins, both scopes, and how to resolve it.

type Shadow

type Shadow struct {
	// Nature and Name are the (nature, name) both declarations share.
	Nature Nature
	Name   string

	// Plugin is the plugin whose declaration was displaced, and PluginDir is where
	// that displaced declaration sits — the path an author needs to read the rule
	// they have overridden.
	Plugin    string
	PluginDir string

	// WinnerDir is the folder that took precedence — the project's own, or an
	// earlier-listed plugin's.
	WinnerDir string

	// WinnerPlugin is the plugin the winning declaration came from, empty when the
	// project's own won. What distinguishes "you overrode this" from "one of your
	// two plugins is quietly not enforcing", which need different remedies.
	WinnerPlugin string
}

Shadow is one plugin declaration that a declaration of the same (nature, name) took precedence over. It mirrors guardrail.Shadow, extended by the nature axis the new format keys on (a plugin gate `x` does not shadow a project context `x`).

Why the project wins

A project must be able to override a rule it did not write. The alternative is that installing a plugin can impose a rule the project cannot adjust except by uninstalling the whole plugin — which makes the plugin all-or-nothing and guarantees the first rule anyone disagrees with takes the useful ones down with it.

Why it is REPORTED

Silently is the failure this product exists to prevent. Both declarations load, both are well-formed, and the tree looks exactly as it would if both enforced. So the shadowing travels back to the caller as a fact to announce.

func (Shadow) Message

func (sh Shadow) Message() string

Message renders a shadow for a person, naming both sides and what to do about it. One wording, here, because every hook point reports this and two copies would drift — the same single-source rule guardrail.Shadow.Message follows.

The two cases are worded apart because the remedy differs. A project overriding a shipped rule is usually deliberate and needs only to be visible. One plugin displacing another's rule is nobody's decision — the consumer installed both and got an ordering they never chose — so that message says which one is live, or the reader cannot tell which rule is running.

func (Shadow) Qualified

func (sh Shadow) Qualified() string

Qualified is the shadowed declaration's disable key, so a report can tell the consumer exactly what to write to silence the losing side if that is what they meant. Keyed on the displaced plugin's origin.

type Store

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

Store reads the declarations in force for a project: the ones it declares under its own `.sloprail/` directory, and the ones it installed, which live inside the plugins that ship them.

The two are read the same way and validated by the same code, deliberately — the exact stance the old format's now-deleted internal/guardrail.Store took. A plugin's declaration is not a second kind of thing with its own rules; it is the same declaration in a different place, and the moment the two paths diverge is the moment a rule can behave one way for its author and another for the project that installed it.

Rooted at the `.sloprail` dir itself (not the project root): the store owns every path beneath it, and a caller passes typed identifiers (a nature, a name), never a built path. A plugin's own `.sloprail` sits at `<pluginRoot>/.sloprail`, the same relative layout a project uses, so a rule can be developed in a project and shipped in a plugin without being rewritten.

func New

func New(root string) *Store

New returns a store rooted at a project's `.sloprail` directory, reading only the declarations the project itself declares.

func NewPlugin

func NewPlugin(plugin Origin) *Store

NewPlugin returns a store that reads ONE plugin's declarations and no project's — the inspection a plugin author runs on what they ship. The plugin's declarations are tagged with its Origin and validated by the plugin rules (a plugin's structure gate must declare a `scope`, for one), exactly as they would be once installed. There is no project, so no project config applies.

func NewWithPlugins

func NewWithPlugins(projectRoot string, plugins []Origin) *Store

NewWithPlugins returns a store that reads a project's own declarations AND the ones shipped by the installed plugins given.

projectRoot is the project's own `.sloprail` directory. The plugins are the installation ROOTS of the enabled plugins (the directory holding a plugin's `.sloprail/`, `hooks/`, `skills/`), each paired with the plugin NAME the user enabled it by — an Origin. This division mirrors guardrail.NewWithPlugins exactly: discovering which plugins a project installed means reading a specific harness's settings files and cache layout, knowledge that belongs in internal/harness and not spread into the loader. This package's job is to read declarations; it takes the set of places to read them from as an input.

The name arrives as part of the Origin rather than being derived from the directory, because the name a user knows a plugin by is the one they wrote in their settings, and a cache directory's name is an artefact they never see. Deriving it here would mean guessing, and the guess would appear in refusal messages and in the disable list — the two places a wrong name costs most.

func (*Store) Load

func (s *Store) Load(reg *module.Registry) (Loaded, error)

Load reads every declaration in force — the project's own and the plugins' — validating each against the event vocabulary the given registry declares, and resolving precedence and the project's disable list.

A project with no `.sloprail` directory, or one with none of a given nature's folders, has no declarations of that kind — not an error, the ordinary state of a project that has not adopted them. A plugin root with no `.sloprail` is the same: most plugins ship none.

The order of operations, mirroring guardrail.Store.Resolve

  1. PARSE every root — the project's own first, then each plugin's in order — tagging each declaration with its Origin. Parsing is separated from validating so the FULL set of context names (across all roots) is known before any declaration is validated: a `require: [{context: X}]` may name a context declared in another root, and it resolves as long as SOME root declares X, independent of who wins precedence.
  2. VALIDATE each parsed declaration against that environment. A declaration with any disabling problem becomes an Invalid.
  3. RESOLVE precedence: the project's declarations claim their (nature, name) first, so they win; between two plugins the earlier-listed wins. A displaced declaration is recorded as Shadowed rather than loaded — first writer wins, and it is never loaded even briefly. Structure gates are the exception: they do not compete for a name, so every sound one loads and they combine by scope (see StructureGate).
  4. DISABLE: the project's own config (`.sloprail/config.yaml` `disabled:`) is applied last, filtering both the loaded set and the Invalid set — the consumer's final say over everything above, and the only way to switch off a plugin declaration that will not load (its file is in an install cache they must not edit). This is the exact shape guardrail.Store.Resolve uses.

A nil registry parses and validates everything EXCEPT trigger `match` expressions, which have no kind declaration to compile against — the same position internal/guardrail.Load (versus LoadWith) takes. Every caller about to act on what it loaded passes modules.Registry. sr:invariant loading/one-broken-rule-disables-only-itself sr:invariant loading/unreadable-config-fails-the-load sr:invariant loading/precedence-and-shadowing

func (*Store) WithTrustedRev added in v0.4.0

func (s *Store) WithTrustedRev(rev string) *Store

WithTrustedRev says which commit's config.yaml may switch off a protected rule (the plugin's grounded-rule-changes): the commit the session began at, so neither the working tree nor the agent's own commits can. It returns the store for chaining.

type StructureEntry

type StructureEntry struct {
	// Glob is a bare glob pattern. Exactly one of Glob / Regex is set.
	Glob string `yaml:"glob"`

	// Regex is a full regex pattern, for the cases a glob cannot express (a
	// dated-folder shape, say). Exactly one of Glob / Regex is set.
	Regex string `yaml:"regex"`
}

StructureEntry is one entry in structure.yaml's allow/deny lists (dot-dir-file-store/main.tsp StructureEntry). Exactly one of Glob / Regex is set — a model with two optional fields in XOR, the same shape as Check's script/judge, tagged by key rather than by a gitignore-style string prefix.

type StructureGate

type StructureGate struct {
	// Scope is the part of the tree a PLUGIN's structure gate owns: a list of
	// folders, each a `glob` ending in `/` (".mdmap/", "**/.adr/"). Required for a
	// plugin's structure.yaml and forbidden in a project's (the project's covers
	// the whole tree implicitly). Glob-only for now; the object shape leaves room
	// for `regex` later. A scope may not cover the whole tree (`**/`, `*/`, `/`).
	Scope []StructureEntry `yaml:"scope"`

	// Allow is the allowlist. Each entry is a glob or a regex (exactly one set).
	// In a plugin's structure every entry must lie inside its scope when the
	// scope is a literal folder.
	Allow []StructureEntry `yaml:"allow"`

	// Deny carves exceptions out of Allow. Optional. Each entry is a glob or a
	// regex (exactly one set). A PROJECT deny also vetoes writes inside a
	// plugin's scope.
	Deny []StructureEntry `yaml:"deny"`

	// Dir is the folder the structure.yaml sits in — `.sloprail/file-guard`. Not
	// a YAML field.
	Dir string `yaml:"-"`

	// Origin is where this structure gate was found — the project's own
	// `.sloprail`, or a plugin that ships it. Not a YAML field; the loader fills it.
	// The zero value is a project's own structure gate. It decides which rules
	// apply: a plugin's must carry a scope, a project's must not.
	Origin Origin `yaml:"-"`
}

StructureGate is one structure gate (dot-dir-file-store/main.tsp StructureGateDeclaration): an allowlist of paths that may be written, deny by default. `deny` carves exceptions out of `allow`. A primitive, not a rule nature: one file per ROOT, `file-guard/structure.yaml`, sibling of the per-guard subfolders.

Combined, not a singleton

Every root may declare one — the project's own AND each enabled plugin's — and all of them load together; none shadows another. What keeps them from fighting is OWNERSHIP:

  • the PROJECT's structure covers the whole tree and must not declare a `scope`;
  • a PLUGIN's structure must declare a `scope` — the folders it owns — and its allow/deny only ever decide paths inside that scope.

A written path inside exactly one plugin's scope is decided by that plugin (the project's `allow` does not widen it, but a project `deny` still vetoes); a path inside two plugins' scopes is refused as an ownership conflict; a path in no plugin's scope is decided by the project's structure, or permitted when the project declares none. The runtime order lives in internal/dispatch (StructureSet.Decide); the load rules in ValidateStructureGate.

func (StructureGate) Attribution

func (sg StructureGate) Attribution() string

Attribution is the structure gate's provenance as a refusal should carry it — empty for a project's own, " from plugin X" for a shipped one. The structure gate has no per-name folder, so there is no name to quote; only the origin.

func (StructureGate) Describe

func (sg StructureGate) Describe() string

Describe names this structure gate for a person — "this project's structure gate" or "plugin \"x\"'s structure gate" — the one wording refusals and reports use.

func (StructureGate) Path

func (sg StructureGate) Path() string

Path is the structure.yaml this gate was read from, for a diagnostic that sends an author to the file.

func (StructureGate) Qualified

func (sg StructureGate) Qualified() string

Qualified is the structure gate's disable key, `<plugin>/structure` for a shipped one and `structure` for a project's own.

func (StructureGate) ScopeGlobs

func (sg StructureGate) ScopeGlobs() []string

ScopeGlobs are the scope entries as written (each ending in `/`), in order — what a report lists as the folders a plugin owns.

Jump to

Keyboard shortcuts

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