event

package
v0.39.1 Latest Latest
Warning

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

Go to latest
Published: Sep 3, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package event owns the shared event-dispatch plumbing for the `specscore` CLI: the Subscriber extension point, the Event envelope type, the envelope validator, the fan-out dispatcher, the built-in subscriber implementations (JsonlWriter, NoOp, Exec), and the events: config block loader.

See `spec/features/cli/event/README.md` for the full Feature contract.

This file currently scopes the package to the Subscriber interface and the Event/Actor/Artifact envelope types. Subscribers, validator, dispatcher, and config loader land in follow-on tasks.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

func ConfiguredLedgerPath added in v0.38.0

func ConfiguredLedgerPath(projectRoot string) (string, error)

ConfiguredLedgerPath returns the project's configured JSONL event ledger. A project may configure at most one JSONL sink for commands that merge ledgers; multiple JSONL sinks are ambiguous and fail closed. Projects with only non-file subscribers have no merge target.

func EventLedgerPath added in v0.38.0

func EventLedgerPath(projectRoot string) (string, error)

EventLedgerPath is an alias for ConfiguredLedgerPath for callers that use the event terminology directly.

func IsMergeInputError added in v0.38.0

func IsMergeInputError(err error) bool

IsMergeInputError reports whether err is an input-validation refusal.

func ReconciliationToken added in v0.34.0

func ReconciliationToken(record PreparedRecord, decision string) string

ReconciliationToken binds an explicit operator decision to the unresolved event and the artifact-existence evidence they reviewed.

func Validate

func Validate(e Event) error

Validate checks an Event against REQ:envelope-validation. It is pure: no I/O, no clock access. On a valid envelope it returns nil; otherwise it returns a *ValidationError naming the first failing field and the rule violated. Payload field-level inspection is explicitly out — the function only confirms the payload bytes parse as a JSON object.

Types

type Actor

type Actor struct {
	Kind string `json:"kind"`
	ID   string `json:"id"`
}

Actor identifies the originator of an event.

type Artifact

type Artifact struct {
	Type     string `json:"type"`
	ID       string `json:"id"`
	Path     string `json:"path"`
	Revision string `json:"revision"`
}

Artifact identifies the SpecScore artifact an event refers to.

type DispatchResult

type DispatchResult struct {
	// ValidationError is non-nil when envelope validation failed; in that
	// case no subscriber was invoked.
	ValidationError error
	// Delivered counts subscribers whose Deliver returned nil.
	Delivered int
	// Failed counts subscribers whose Deliver returned a non-nil error.
	Failed int
	// Failures lists each non-nil Deliver error in declared order.
	Failures []SubscriberFailure
}

DispatchResult is the structured outcome of a Dispatch call. The verb layer maps it to the standard exit-code contract (REQ:dispatch-exit-codes):

  • ValidationError != nil -> exit 2
  • Delivered > 0 OR len(subscribers) == 0 -> exit 0
  • Failed > 0 AND Delivered == 0 AND len(subscribers) > 0 -> exit 10

Exit codes 3 (missing project root) and other 2-class failures (config validation) are produced by callers above Dispatch.

func Dispatch

func Dispatch(ctx context.Context, e Event, subscribers []Subscriber) DispatchResult

Dispatch fans an envelope out to subscribers sequentially in declared order. It first validates the envelope; on validation failure it returns immediately with ValidationError populated and no subscriber invoked. On a valid envelope it calls each subscriber's Deliver; per-subscriber errors are logged to stderr in the contracted key=value form and the iteration continues to the next subscriber. Successful deliveries produce no stderr output (REQ:fan-out-dispatch).

The failure line format is:

event-dispatch failure: subscriber=<Name()> event=<e.Name> error="<err.Error()>"

Embedded double quotes and backslashes in err.Error() are escaped so the line round-trips through standard log parsers.

type Event

type Event struct {
	Name      string          `json:"name"`
	Version   int             `json:"version"`
	UUID      string          `json:"uuid"`
	Timestamp time.Time       `json:"timestamp"`
	Actor     Actor           `json:"actor"`
	Artifact  Artifact        `json:"artifact"`
	Payload   json.RawMessage `json:"payload"`
}

Event is the common envelope passed to every Subscriber. Field shapes mirror the cross-repo event contract; see REQ:envelope-validation in spec/features/cli/event/README.md.

type Exec

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

Exec is a Subscriber that delivers events by spawning a child process and piping the JSON-serialized envelope to its stdin. It enforces a wall-clock timeout: on expiry the command tree is terminated using platform-specific process-group or tree controls. The configured env mapping is appended to the inherited process environment (additive, not replacement).

func NewExec

func NewExec(argv []string, env map[string]string, timeout time.Duration) *Exec

NewExec constructs an Exec subscriber. argv[0] is the executable and argv[1:] are positional arguments. env may be nil. timeout is the wall-clock budget for the child; the config-loader (task 6) enforces the [100, 30000] ms bounds — this constructor does not.

func (*Exec) Deliver

func (x *Exec) Deliver(ctx context.Context, e Event) error

Deliver runs the configured command with the event JSON piped to stdin. Returns *ExecTimeoutError on wall-clock timeout (including setup), *ExecExitError on non-zero exit, or a plain error for other setup failures (serialization, pipe creation).

func (*Exec) Name

func (x *Exec) Name() string

Name returns "exec:<argv[0]>" so the dispatcher's stderr failure log can identify which exec subscriber failed.

type ExecExitError

type ExecExitError struct {
	ExitCode int
	Cause    error
}

ExecExitError is returned when the child exited non-zero without hitting the timeout. ExitCode is the OS-reported exit status.

func (*ExecExitError) Error

func (e *ExecExitError) Error() string

func (*ExecExitError) Unwrap

func (e *ExecExitError) Unwrap() error

type ExecTimeoutError

type ExecTimeoutError struct {
	Timeout time.Duration
	Cause   error
}

ExecTimeoutError is returned when the configured wall-clock timeout elapsed before the child completed. It is distinguishable from *ExecExitError so the dispatcher's stderr log can name the failure mode.

func (*ExecTimeoutError) Error

func (e *ExecTimeoutError) Error() string

func (*ExecTimeoutError) Unwrap

func (e *ExecTimeoutError) Unwrap() error

type JsonlWriter

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

JsonlWriter is a Subscriber that appends each delivered Event as a single JSON line to a file. It is the default subscriber synthesized by the config loader when the `events:` block is omitted; see spec/features/cli/event/README.md.

Relative configured paths resolve against the project root supplied at construction time, NEVER against the current working directory. The dispatcher wires the project root in once, at startup.

func NewJsonlWriter

func NewJsonlWriter(path string, projectRoot string) *JsonlWriter

NewJsonlWriter constructs a JsonlWriter that will append to `path`. When `path` is relative it is joined against `projectRoot`; when it is absolute it is used as-is. The resolved path is cached on the struct so Deliver and Name remain stable across calls.

func (*JsonlWriter) Deliver

func (w *JsonlWriter) Deliver(_ context.Context, e Event) error

Deliver serializes e to single-line JSON and appends it (followed by a single newline) to the configured file. Parent directories are created at mode 0755 if absent; the file itself is opened with O_APPEND|O_CREATE |O_WRONLY at mode 0644.

func (*JsonlWriter) Name

func (w *JsonlWriter) Name() string

Name returns "jsonl:<resolved-path>" — using the resolved absolute path so failure logs point at the actual file on disk.

func (*JsonlWriter) Path added in v0.38.0

func (w *JsonlWriter) Path() string

Path returns the fully resolved ledger path used by this writer.

type MergeInputError added in v0.38.0

type MergeInputError struct{ Err error }

MergeInputError identifies a merge refusal caused by invalid or ambiguous input. CLI callers can map it to their invalid-arguments exit status while retaining a different status for publication failures.

func (*MergeInputError) Error added in v0.38.0

func (e *MergeInputError) Error() string

func (*MergeInputError) Unwrap added in v0.38.0

func (e *MergeInputError) Unwrap() error

type MergeOptions added in v0.38.0

type MergeOptions struct {
	// DryRun validates and plans the union without changing the target.
	DryRun bool
}

MergeOptions controls MergeLedgersWithOptions.

type MergeResult added in v0.38.0

type MergeResult struct {
	Target   string
	Existing int
	Added    int
	Skipped  int
}

MergeResult describes an event-ledger union. Existing target bytes are never rewritten; Added is the number of source-only events appended.

func MergeJSONL added in v0.38.0

func MergeJSONL(target string, sources []string) (MergeResult, error)

MergeJSONL is the concise public spelling for MergeLedgers.

func MergeLedgers added in v0.38.0

func MergeLedgers(target string, sources []string) (MergeResult, error)

MergeLedgers unions one or more JSONL event ledgers into target. Target records retain their exact bytes and order. Source-only records are sorted by UUID before being appended, making the result independent of source argument order and concurrent branch completion order.

func MergeLedgersWithOptions added in v0.38.0

func MergeLedgersWithOptions(target string, sources []string, options MergeOptions) (MergeResult, error)

MergeLedgersWithOptions validates all input before publishing one atomic, durable replacement of target. Any malformed or conflicting source leaves target untouched.

type NoOp

type NoOp struct{}

NoOp is the explicit-opt-out Subscriber: it accepts every event, performs no work, and returns nil. It exists so an operator can configure events: with a "noop" entry and signal "I have considered subscribers and chosen none" rather than relying on the absence of configuration. See AC:noop-discards in spec/features/cli/event/README.md.

func (NoOp) Deliver

func (NoOp) Deliver(ctx context.Context, e Event) error

Deliver discards the event and returns nil. The implementation MUST NOT touch the filesystem, network, stdout, or stderr.

func (NoOp) Name

func (NoOp) Name() string

Name returns the literal identifier "noop".

type Outbox added in v0.34.0

type Outbox struct{ Root string }

func NewOutbox added in v0.34.0

func NewOutbox(projectRoot string) Outbox

func (Outbox) Abort added in v0.34.0

func (o Outbox) Abort(id string) error

func (Outbox) Commit added in v0.34.0

func (o Outbox) Commit(id string) error

Commit makes a prepared event deliverable and reconstructs every pending marker from the ledger recipient set. Repeating Commit is safe.

func (Outbox) Enqueue added in v0.34.0

func (o Outbox) Enqueue(e Event, subscribers []Subscriber) error

func (Outbox) FindPreparedIntent added in v0.34.0

func (o Outbox) FindPreparedIntent(intent Event, facts json.RawMessage) (*Event, error)

FindPreparedIntent locates the one unresolved event for a logical mutation. UUID and timestamp are intentionally excluded because a retry must recover the original random UUIDv4. The versioned, domain-separated fingerprint is stored only in the private ledger and used only as acceleration: a match is accepted after comparing the complete canonical envelope and private facts. A different intent in the same command/artifact scope fails closed.

func (Outbox) Prepare added in v0.34.0

func (o Outbox) Prepare(e Event, subscribers []Subscriber) error

Prepare durably records the envelope and its complete recipient set before an artifact mutation. Prepared records are visible/recoverable but not delivered until Commit.

func (Outbox) PrepareIntent added in v0.34.0

func (o Outbox) PrepareIntent(e Event, subscribers []Subscriber, facts json.RawMessage) error

PrepareIntent records private, non-secret mutation facts alongside the immutable event envelope. The facts and their fingerprint never enter the subscriber Event or public Prepared reconciliation projection.

func (Outbox) Prepared added in v0.34.0

func (o Outbox) Prepared() ([]PreparedRecord, error)

Prepared lists unresolved prepared records in deterministic UUID order. It never follows absolute or parent-traversing artifact paths while collecting local evidence.

func (Outbox) Recover added in v0.34.0

func (o Outbox) Recover() error

Recover rebuilds pending indexes for every committed ledger record. An ack is the durable per-subscriber cursor; a stale marker beside an ack is pruned.

func (Outbox) Replay added in v0.34.0

func (o Outbox) Replay(ctx context.Context, subscribers []Subscriber, name string, limit int) (ReplayResult, error)

func (Outbox) ReplayFrom added in v0.34.0

func (o Outbox) ReplayFrom(ctx context.Context, subscribers []Subscriber, name, from string, limit int) (ReplayResult, error)

ReplayFrom retries pending deliveries in immutable ledger order. When from is non-empty, replay starts inclusively at that committed event's position in the ledger. The cursor remains useful after that event is acknowledged: ordering is derived from the ledger record, never from pending directory enumeration.

type PreparedRecord added in v0.34.0

type PreparedRecord struct {
	EventUUID      string    `json:"event_uuid"`
	EventName      string    `json:"event_name"`
	ArtifactPath   string    `json:"artifact_path"`
	ArtifactExists bool      `json:"artifact_exists"`
	Timestamp      time.Time `json:"timestamp"`
}

PreparedRecord is an interrupted two-phase event awaiting an explicit commit/abort decision. ArtifactExists is evidence for that decision, not an automatic conclusion: some mutations target an artifact that existed before the event was prepared.

type ReplayResult added in v0.34.0

type ReplayResult struct {
	Delivered int
	Failed    []SubscriberFailure
	Pending   int
	Prepared  []PreparedRecord
}

type Subscriber

type Subscriber interface {
	// Deliver is invoked by the dispatcher with a validated envelope. It
	// returns nil on successful delivery and a non-nil error on any failure
	// (timeout, exec exit non-zero, filesystem error, etc.).
	Deliver(ctx context.Context, e Event) error

	// Name returns a stable identifier used in stderr failure logs. The
	// dispatcher does not interpret the string.
	Name() string
}

Subscriber is the extension point for receiving dispatched events. Any type implementing Subscriber may be registered via the events: config block in specscore.yaml. Delivery is at-least-once: a process can crash after the subscriber succeeds but before its durable acknowledgement is written. Implementations MUST therefore be idempotent by Event.UUID across processes and invocations, not merely safe to call repeatedly within one invocation.

func LoadSubscribers

func LoadSubscribers(projectRoot string) ([]Subscriber, error)

LoadSubscribers parses <projectRoot>/specscore.yaml and returns the configured Subscriber list. When the file does not exist OR exists but does not contain an `events:` key, the default JsonlWriter at `.specscore/events.jsonl` is synthesized. An explicit `events: {subscribers: []}` is honored as the zero-subscriber list (no synthesis).

type SubscriberFailure

type SubscriberFailure struct {
	Name string
	Err  error
}

SubscriberFailure pairs a failing subscriber's Name() with the error its Deliver returned. The dispatcher emits one stderr line per entry; the verb layer additionally inspects the slice when mapping to exit codes.

type ValidationError

type ValidationError struct {
	// Field is the dotted JSON path of the offending field (e.g. "name",
	// "actor.kind", "artifact.type").
	Field string
	// Rule is a human-readable description of the rule that was violated;
	// regex rules embed the pattern verbatim so callers can grep for it.
	Rule string
	// Value is the offending value rendered for stderr. Empty when the
	// rule is "must not be empty" and the field is a string.
	Value string
}

ValidationError is the deterministic error returned by Validate. The dispatcher prints its Error() string verbatim to stderr; the envelope-validation ACs assert that the message names both the offending Field and the Rule violated, so both elements MUST appear in the formatted string.

func (*ValidationError) Error

func (e *ValidationError) Error() string

Error formats the validation error as `envelope validation failed: field=<f> value=<v> rule=<r>`. The shape is stable so tests can string-match on it.

Jump to

Keyboard shortcuts

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