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 ConfiguredLedgerPath(projectRoot string) (string, error)
- func EventLedgerPath(projectRoot string) (string, error)
- func IsMergeInputError(err error) bool
- 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 MergeInputError
- type MergeOptions
- type MergeResult
- 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 ConfiguredLedgerPath ¶ added in v0.38.0
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
EventLedgerPath is an alias for ConfiguredLedgerPath for callers that use the event terminology directly.
func IsMergeInputError ¶ added in v0.38.0
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 ¶
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.
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
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.
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.