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 ¶
- func ReconciliationToken(record PreparedRecord, decision string) string
- func Validate(e Event) error
- type Actor
- type Artifact
- type DispatchResult
- type Event
- type Exec
- type ExecExitError
- type ExecTimeoutError
- type JsonlWriter
- type NoOp
- type Outbox
- func (o Outbox) Abort(id string) error
- func (o Outbox) Commit(id string) error
- func (o Outbox) Enqueue(e Event, subscribers []Subscriber) error
- func (o Outbox) FindPreparedIntent(intent Event, facts json.RawMessage) (*Event, error)
- func (o Outbox) Prepare(e Event, subscribers []Subscriber) error
- func (o Outbox) PrepareIntent(e Event, subscribers []Subscriber, facts json.RawMessage) error
- func (o Outbox) Prepared() ([]PreparedRecord, error)
- func (o Outbox) Recover() error
- func (o Outbox) Replay(ctx context.Context, subscribers []Subscriber, name string, limit int) (ReplayResult, error)
- func (o Outbox) ReplayFrom(ctx context.Context, subscribers []Subscriber, name, from string, limit int) (ReplayResult, error)
- type PreparedRecord
- type ReplayResult
- type Subscriber
- type SubscriberFailure
- type ValidationError
Constants ¶
This section is empty.
Variables ¶
This section is empty.
Functions ¶
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 ¶
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 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 ¶
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.
type ExecExitError ¶
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 ¶
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.
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.
type Outbox ¶ added in v0.34.0
type Outbox struct{ Root string }
func (Outbox) Commit ¶ added in v0.34.0
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
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
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 ¶
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.