Documentation
¶
Overview ¶
Package nextaction resolves ONE advisory suggestion from operator-declared sources.
The one-slot constraint does the work ¶
Because exactly one suggestion is ever shown, several things that look like omissions are deliberate:
- No per-source `limit:`. A bounded candidate set is not lossy when only one candidate is displayed — sixty stalled tasks and six produce identical output — so the bound belongs to the engine, which knows the page budget, not to an operator guessing a number whose right value depends on every other source.
- No ranking within a band. See [pickStableRandom].
- No cache. See the Resolve doc.
Evaluation order ¶
Sources are grouped by band, bands are evaluated in operator-declared order (highest priority first), and evaluation STOPS at the first band that yields a candidate. A typical page therefore runs one or two queries rather than all of them, and the ambient/content source at the bottom is reached only when everything above is empty — which is also when it is cheapest to be there.
See RES-09YLLL for the design and the rejected alternatives.
Index ¶
Constants ¶
const DefaultCandidateCap = 20
DefaultCandidateCap bounds how many candidates a source may contribute to the pick. Engine-owned, not configurable — see the package doc.
const DefaultCooldown = 24 * time.Hour
DefaultCooldown applies to a source that declares none.
Deliberately non-zero: an operator who omits a cooldown has probably not thought about nag frequency, so the default assumes they got it wrong rather than assuming they meant "show this every single page load". Being too quiet is recoverable; being a nag is how the whole surface gets ignored.
Variables ¶
This section is empty.
Functions ¶
This section is empty.
Types ¶
type Candidate ¶
Candidate is one entity a source proposes, already ACL-filtered by the caller's reader.
type CandidateFunc ¶
type CandidateFunc func(ctx context.Context, src dataentryconfig.NextActionSource) ([]Candidate, error)
CandidateFunc produces candidates for one source. Supplied by the wiring site so this package depends on no store, searcher or ACL type: it is the consumer-side interface that keeps the engine testable without a graph.
Implementations MUST apply the caller's read gate. The engine never sees an entity the principal may not read, which is also why there is no cache here — see Engine.Resolve.
type Engine ¶
type Engine struct {
// contains filtered or unexported fields
}
Engine resolves suggestions. Construct with New; safe for concurrent use provided the injected collaborators are.
func New ¶
func New( cfg *dataentryconfig.Config, state userstate.Store, candidates CandidateFunc, opts ...Option, ) (*Engine, error)
New builds an Engine. Every collaborator is required: a nil userstate.Store would silently stop honoring snoozes and mutes, which the user experiences as the system ignoring them — precisely the deferred-failure-to-downstream- symptom the project's constructor rule exists to prevent.
func (*Engine) MarkShown ¶
MarkShown records that a suggestion was surfaced, starting its cooldown. Separate from Resolve because resolving is a read: a caller that resolves for a preview, or discards the result, must not start the clock.
func (*Engine) Resolve ¶
Resolve returns the single suggestion to show `user` now, or ok=false when nothing is owed.
`now` is injected rather than read from the clock so callers (and tests) control it, matching userstate.Store and predicatefns.Bind.
Deliberately NOT cached. rela gates reads by principal, with row-level semantics where a hidden entity is nonexistent, so a cache keyed on anything but the principal would defeat that gate — and the failure mode is showing someone a suggestion about an entity whose very existence is meant to be secret. Per-principal caching would be safe but is not worth it against one or two fast queries.
type MatcherFunc ¶
MatcherFunc reports whether a candidate satisfies one source's `condition:`. Supplied by the wiring site for the same reason as CandidateFunc: the expression is compiled against the metamodel by a predicate engine this package must not learn about, so the engine stays free of both.
Returns (nil, false) for a source that declares no condition — the caller then keeps every candidate. A compiled matcher is expected to be built ONCE at config load, so an unparseable expression fails loudly at startup rather than silently suppressing a suggestion forever.
An evaluation error is NOT treated as "does not match": it is returned, so a broken condition surfaces instead of quietly emptying a source.
type Option ¶
type Option func(*Engine)
Option configures an Engine at construction.
A construction-time option rather than a builder method: a builder returning a copy is silently a no-op when the caller discards it, and one mutating the receiver would race, since Engine is documented as safe for concurrent use and callers are invited to cache one. Neither mistake is available here.
func WithMatchers ¶
func WithMatchers(fn MatcherFunc) Option
WithMatchers supplies the per-source condition matchers. Without it a source's `condition:` is not evaluated at all, so the option is required whenever any source declares one — New enforces exactly that.
func WithOptions ¶
func WithOptions(fn OptionFunc) Option
WithOptions supplies the pick_one option resolver. Without it a pick_one affordance simply offers nothing rather than failing the page.
type OptionFunc ¶
OptionFunc resolves a pick_one option list. Supplied by the wiring site for the same reason as CandidateFunc: the query must go through the caller's read gate, and the engine must not learn how to reach a store.
A nil OptionFunc is not an error — the engine then renders the pick_one affordance with no options, which the UI omits. That keeps a deployment that has not wired it from failing every page.
type PickOption ¶
PickOption is one choice in a pick_one affordance.
type Suggestion ¶
type Suggestion struct {
// Source is the config id that produced this. Also the mute unit and half
// the suggestion key.
Source string
// Band is the id of the band it came from.
Band string
// EntityID is empty for a count-based (entity-less) source.
EntityID string
// Message is Suggest with {property} placeholders interpolated.
Message string
// Actions are the affordances, copied from config.
Actions []dataentryconfig.NextActionOffer
// PickOptions holds the render-time options for a pick_one affordance,
// keyed by the offer's index in Actions. Empty for every other kind.
//
// Resolved here rather than by the UI because the options come from a
// QUERY, and the query must run through the same ACL-gated path as the
// suggestion itself — a client-side fetch would be a second read surface
// to gate.
PickOptions map[int][]PickOption
// Key is the identity used for cooldown, snooze and dismissal.
Key userstate.Key
}
Suggestion is the resolved hint.