Documentation
¶
Overview ¶
Package rules is abcd's transport-agnostic modular rules loader (itd-3). It owns the whole capability behind two front doors: the `abcd rules [domain]` CLI verb and the Claude Code prompt-router hook. Nothing here writes to stdout or knows about a harness event — the front doors under internal/surface and the hook entrypoint marshal these results for their transport.
The model is a small set of binary-bundled default domains (embedded below) merged with an optional per-repo <repoRoot>/.abcd/rules.json override. Each domain carries recall keywords + aliases and a list of rules; a prompt is recall-matched against the active domains and only the matching rules are rendered for injection. A leading *<DOMAIN> star-command activates a domain unconditionally (overriding a dormant state, but never the top-level kill switch).
Validation is hand-rolled Go (zero new dependencies): a rules.json that fails to parse or validate is a fail-closed error the caller surfaces loudly — it never silently degrades to zero injection.
Index ¶
- Constants
- func LoadBackstop(repoRoot string) int
- func PruneState(maxAge time.Duration)
- func Render(domains []ResolvedDomain) string
- func ResetState(session string) error
- func SaveState(session string, st SessionState) error
- func Signature(d ResolvedDomain) string
- func Validate(rs RuleSet) error
- type Domain
- type InjectResult
- type ResolvedDomain
- type RuleSet
- type SessionState
Constants ¶
const ( StateActive = "active" StateDormant = "dormant" )
Domain state values. An empty string is treated as active.
const DefaultRefreshBackstop = 15
DefaultRefreshBackstop is the fixed-N full-refresh backstop (D1): the primary refresh is event-driven (a SessionStart/PreCompact reset clears the ledger), and this large counter only catches always-relevant domains that never recall-match. It is deliberately larger than CARL's every-5.
const RepoRelPath = ".abcd/rules.json"
RepoRelPath is the per-repo override file, relative to the repo worktree.
const StateTTL = 7 * 24 * time.Hour
StateTTL bounds how long a per-session ledger lives before the reset hook sweeps it (sessions are one-shot; a week is generous headroom).
Variables ¶
This section is empty.
Functions ¶
func LoadBackstop ¶
LoadBackstop reads rules.force_refresh_every_n from <repoRoot>/.abcd/config.json — the fixed-N full-refresh backstop (D1). Event-driven reset is the primary refresh, so this only bounds always-relevant domains; a missing file/key or a non-positive value falls back to DefaultRefreshBackstop.
func PruneState ¶
PruneState removes session-state files older than maxAge, bounding the growth of the temp state dir across many sessions. Best-effort housekeeping: a missing dir or a per-entry error is ignored.
func Render ¶
func Render(domains []ResolvedDomain) string
Render is the single renderer both front doors use. It emits a header plus one section per domain. An empty domain list renders to zero bytes (D3: no model-facing tokens on a no-match).
func ResetState ¶
ResetState clears a session's ledger (the event-driven refresh: the next prompt re-injects every matching domain). A missing file is not an error.
func SaveState ¶
func SaveState(session string, st SessionState) error
SaveState durably persists the ledger for a session through the canonical fsutil.WriteFileAtomic (temp-write, fsync, rename, parent-dir fsync), so the write is crash-safe and a reader sees either the old file or the complete new one. The state dir is pre-created 0700 (WriteFileAtomic's own MkdirAll is a no-op once it exists); the session file is 0600.
func Signature ¶
func Signature(d ResolvedDomain) string
Signature is the per-domain dedup key: an FNV-1a hash of the rendered block, so identical rendered content (defaults or override) dedups and any content drift invalidates. FNV is sufficient — this is dedup, not security.
func Validate ¶
Validate checks structural invariants: schema_version == 1, every domain name matches [A-Z][A-Z0-9_]*, every state is active/dormant (or empty), and no rule body is empty or whitespace-only. An empty rule body otherwise passes here and renders as a bare contentless "- " bullet in the injected block (iss-2608261550497978) — a loud refusal at load beats a silent empty bullet.
Types ¶
type Domain ¶
type Domain struct {
State string `json:"state,omitempty"`
Recall []string `json:"recall,omitempty"`
Aliases []string `json:"aliases,omitempty"`
Rules []string `json:"rules,omitempty"`
}
Domain is one keyed rule domain. The map key in RuleSet.Domains is its name; the name is therefore not a serialized field on the value.
type InjectResult ¶
type InjectResult struct {
Text string
Injected []string
State SessionState
}
InjectResult is the outcome of one prompt-router evaluation. Text is empty when nothing new is injected (a healthy no-match renders zero model-facing tokens, per D3).
func Inject ¶
func Inject(rs RuleSet, prompt string, prev SessionState, backstop int) InjectResult
Inject is the pure heart of the prompt-router hook: it recall-matches prompt, drops any matched domain already injected this session with an unchanged signature, renders the remainder, and returns the updated session state. It performs no I/O and never reflects prompt bytes into the output — the rendered text is abcd's own rule content only.
backstop is the fixed-N full-refresh interval; <= 0 uses DefaultRefreshBackstop. When the (incremented) prompt count is a multiple of the backstop, the ledger is cleared first so always-relevant domains re-inject.
type ResolvedDomain ¶
ResolvedDomain pairs a domain with its name for ordered rendering and dedup.
type RuleSet ¶
type RuleSet struct {
SchemaVersion int `json:"schema_version"`
Disabled bool `json:"disabled"`
Domains map[string]Domain `json:"domains"`
}
RuleSet is the merged, validated rule model: the bundled defaults overlaid with the per-repo override.
func Defaults ¶
func Defaults() RuleSet
Defaults returns a deep copy of the binary-bundled default rule set, safe for the caller to mutate.
func Load ¶
Load returns the defaults merged with <repoRoot>/.abcd/rules.json when that file exists. An absent file yields the defaults unchanged; a present file that cannot be parsed or fails validation is a fail-closed error.
func Merge ¶
Merge overlays over onto base. Domain fields are per-field: a field set on the override wins; an absent field inherits the base (so {"state":"dormant"} on a default domain silences it while keeping its recall and rules). New domain keys are added. The kill switch is sticky (either side can enable it).
func (RuleSet) Active ¶
func (rs RuleSet) Active() []ResolvedDomain
Active returns every injectable domain (state != dormant) in name-sorted order — the full set the diagnostic `abcd rules` render shows. The top-level kill switch yields nothing.
func (RuleSet) Lookup ¶
func (rs RuleSet) Lookup(name string) (ResolvedDomain, bool)
Lookup returns one domain by name regardless of its state (a dormant domain is still inspectable); ok is false when the name is absent.
func (RuleSet) Match ¶
func (rs RuleSet) Match(prompt string) []ResolvedDomain
Match returns the domains to inject for prompt, in deterministic (name-sorted) order. The top-level kill switch suppresses everything. Otherwise a domain is injected if a star-command names it (overriding dormant) or — when active — its recall keywords or aliases hit the prompt.
type SessionState ¶
type SessionState struct {
Count int `json:"count"`
Ledger map[string]string `json:"ledger"` // domain name -> last-injected signature
}
SessionState is the per-session dedup ledger plus the prompt counter that drives the fixed-N refresh backstop.
func LoadState ¶
func LoadState(session string) SessionState
LoadState reads the ledger for a session. A missing, oversized, or malformed file yields the zero state (a fresh session), never an error — dedup is a best-effort optimisation, not a correctness gate.