Documentation
¶
Overview ¶
Package suggest implements the community suggestion queue: an ephemeral store aggregating anonymous patron suggestions and flags per (work, term) pair for staff review, generalized from qllpoc's single-vocabulary DynamoDB implementation onto the portable document store and arbitrary vocabularies (controlled TermRefs or folksonomy tags). The BIBFRAME graph remains the source of truth for approved assignments; nothing here is durable except the audit trail.
Index ¶
- Constants
- Variables
- type Abuse
- type ActorStats
- type AuditEntry
- type Caps
- type Decision
- type FolkStatus
- type FolkTerm
- type MonthStats
- type Promotion
- type Provenance
- type QueuePage
- type QueueQuery
- type Reason
- type Service
- func (s *Service) AcceptedFolkTerms(ctx context.Context, prefix string, limit int) ([]string, error)
- func (s *Service) ApprovedUnpublished(ctx context.Context) ([]Suggestion, error)
- func (s *Service) Audit(ctx context.Context, month string) ([]AuditEntry, error)
- func (s *Service) DecidePromotion(ctx context.Context, tag string, approve bool, actor string) (Promotion, error)
- func (s *Service) FolkTermStatus(ctx context.Context, norm string) (FolkTerm, error)
- func (s *Service) ForWork(ctx context.Context, workID string) ([]Suggestion, error)
- func (s *Service) GetPromotion(ctx context.Context, tag string) (Promotion, error)
- func (s *Service) ManualTerm(ctx context.Context, workID string, ref vocab.TermRef, workTitle, actor string) error
- func (s *Service) MarkPromotionExecuted(ctx context.Context, tag string, works int) error
- func (s *Service) MarkPublished(ctx context.Context, items []Suggestion, etag string) error
- func (s *Service) PipelineSuggest(ctx context.Context, workID string, term vocab.TermRef, confidence float64) error
- func (s *Service) Promotions(ctx context.Context, status Status) ([]Promotion, error)
- func (s *Service) ProposePromotion(ctx context.Context, rawTag string, term vocab.TermRef, actor string) (Promotion, error)
- func (s *Service) Queue(ctx context.Context, q QueueQuery) (QueuePage, error)
- func (s *Service) Review(ctx context.Context, decisions []Decision, actor string) error
- func (s *Service) SetClock(now func() time.Time)
- func (s *Service) SetFolkStatus(ctx context.Context, norm string, status FolkStatus, actor string) error
- func (s *Service) Stats(ctx context.Context, month string) (MonthStats, error)
- func (s *Service) Submit(ctx context.Context, in SubmitInput) (SubmitResult, error)
- func (s *Service) SubmitConcern(ctx context.Context, workID, note, workTitle, supporterHash string) error
- func (s *Service) WriteAudit(ctx context.Context, entry AuditEntry)
- func (s *Service) WriteTombstone(ctx context.Context, workID string, term vocab.TermRef, actor string) error
- type Session
- type Status
- type SubmitInput
- type SubmitResult
- type SuggType
- type Suggestion
Constants ¶
const ( ChallengeMinAge = 3 * time.Second ChallengeMaxAge = 2 * time.Hour )
Challenge age bounds: the floor filters scripted submissions that POST faster than a human could pick a vocabulary term; the ceiling expires stale dialogs.
const ConcernScheme = "concern"
ConcernScheme is the pseudo-scheme concern ids ride in Term -- reusing the suggestion aggregate's storage/queue/review machinery without a parallel record type (tasks/210). A concern never resolves against a vocabulary and never publishes.
Variables ¶
var ( ErrTombstoned = errors.New("suggest: term was rejected for this work and may not be re-suggested") ErrRateLimited = errors.New("suggest: rate limit exceeded") ErrBadTerm = errors.New("suggest: term not in a loaded vocabulary") ErrFolkBlocked = errors.New("suggest: folksonomy term is blocked") )
Sentinel errors; the API layer maps these to HTTP statuses.
var DefaultCaps = Caps{PerDay: 20, PerHour: 8, SupporterTTL: 90 * 24 * time.Hour}
DefaultCaps are deliberately tight -- suggesting subjects is a considered act, not a feed interaction.
var ErrPromotionExists = errors.New("suggest: promotion already proposed for this tag")
ErrPromotionExists reports an already-open proposal for the tag.
var Reasons = []Reason{ReasonDoesNotApply, ReasonTooBroad, ReasonOutdated, ReasonSpoiler}
Reasons lists every valid flag reason for input validation.
Functions ¶
This section is empty.
Types ¶
type Abuse ¶
type Abuse struct {
// contains filtered or unexported fields
}
Abuse pseudonymizes source IPs and issues the anonymous challenge tokens that gate patron submissions -- qllpoc's IPHasher, ported intact. The API stores HMAC(secret, ip), never the address itself.
func NewAbuse ¶
NewAbuse requires a non-trivial secret (32 random bytes in deployment secret storage).
func (*Abuse) Challenge ¶
Challenge issues an anonymous, short-lived proof-of-page-open token: "<unix-ts>.<hex hmac>". The patron client fetches one when the suggest dialog opens and echoes it on submit. No identity is involved -- the token binds only a timestamp.
func (*Abuse) VerifyChallenge ¶
VerifyChallenge checks the token's MAC and that its age is within bounds.
type ActorStats ¶
type ActorStats struct {
Actor string `json:"actor"`
Total int `json:"total"`
ByAction map[string]int `json:"byAction"`
Works int `json:"works"`
ActiveDays int `json:"activeDays"`
First time.Time `json:"first"`
Last time.Time `json:"last"`
Sessions []Session `json:"sessions"`
}
ActorStats rolls up one cataloger's audit activity for the month.
type AuditEntry ¶
type AuditEntry struct {
WorkID string `json:"workId,omitempty"`
At time.Time `json:"at"`
Action string `json:"action"` // REVIEW_APPROVE, REVIEW_REJECT, MANUAL_TERM, FOLK_ACCEPT, FOLK_BLOCK, PUBLISH_*
Actor string `json:"actor"`
Terms []string `json:"terms,omitempty"` // "<scheme>:<id>"
Note string `json:"note,omitempty"`
ETag string `json:"etag,omitempty"` // grain etag for publish events
}
AuditEntry records a staff decision or publish event. Editorial history for the published state itself lives in the grain store; this trail covers queue-side actions.
type Caps ¶
type Caps struct {
PerDay int
PerHour int
// SupporterTTL ages dedup markers off; rate counters use fixed short TTLs.
SupporterTTL time.Duration
}
Caps bounds anonymous submission volume per supporter hash.
type Decision ¶
type Decision struct {
WorkID string `json:"workId"`
Term vocab.TermRef `json:"term"`
Type SuggType `json:"type"`
Approve bool `json:"approve"`
// SubstituteTerm, when set on an approval, records that the reviewer
// slid to a neighbouring vocabulary term instead of the suggested one.
SubstituteTerm *vocab.TermRef `json:"substituteTerm,omitempty"`
Note string `json:"note,omitempty"`
// Tombstone on a rejection also blocks future re-suggestions of the pair.
Tombstone bool `json:"tombstone,omitempty"`
}
Decision is one staff review action inside a batch.
type FolkStatus ¶
type FolkStatus string
FolkStatus is the lifecycle of a novel folksonomy term itself (distinct from any per-work suggestion of it): PROPOSED terms are invisible to autocomplete until a moderator ACCEPTS them; BLOCKED terms cannot be suggested at all.
const ( FolkProposed FolkStatus = "PROPOSED" FolkAccepted FolkStatus = "ACCEPTED" FolkBlocked FolkStatus = "BLOCKED" )
type FolkTerm ¶
type FolkTerm struct {
Term string `json:"term"` // normalized text = identity
Status FolkStatus `json:"status"`
CreatedAt time.Time `json:"createdAt"`
// UseCount tracks how many suggestions cite the term (moderation signal).
UseCount int `json:"useCount"`
}
FolkTerm is the stored lifecycle record of one normalized community tag.
type MonthStats ¶
type MonthStats struct {
Month string `json:"month"`
Total int `json:"total"`
Actors int `json:"actors"`
Works int `json:"works"`
ByAction map[string]int `json:"byAction"`
PerActor []ActorStats `json:"perActor"`
}
MonthStats is the editing-activity rollup for a single YYYY-MM audit partition: overall totals plus a per-cataloger breakdown.
type Promotion ¶
type Promotion struct {
Tag string `json:"tag"` // normalized (vocab.NormalizeFolk)
Term vocab.TermRef `json:"term"`
Status Status `json:"status"` // PENDING -> APPROVED / REJECTED
ProposedBy string `json:"proposedBy"`
CreatedAt time.Time `json:"createdAt"`
DecidedBy string `json:"decidedBy,omitempty"`
DecidedAt time.Time `json:"decidedAt,omitzero"`
// Works counts the rewrites at execution (stamped by the publisher).
Works int `json:"works,omitempty"`
}
Promotion proposes elevating an uncontrolled tag to a controlled term (tasks/044): on approval the publisher rewrites every carrying Work (subject added; editorial tag retracted) and records lcat:tagAlias so the projector suppresses the tag where the term is present and pickers auto-suggest the term. One open promotion per normalized tag.
type Provenance ¶
type Provenance string
Provenance records where a suggestion originated.
const ( ProvenancePatron Provenance = "PATRON" ProvenancePipeline Provenance = "PIPELINE" // enrichment providers (tasks/039) ProvenanceLibrarian Provenance = "LIBRARIAN" )
type QueuePage ¶
type QueuePage struct {
Items []Suggestion `json:"items"`
Cursor string `json:"cursor,omitempty"`
}
QueuePage is one page of the review queue, supporter-count-descending.
type QueueQuery ¶
type QueueQuery struct {
Status Status // default PENDING
Scheme string
Provenance Provenance
Type SuggType
Limit int // default 50
Cursor string // opaque; from a prior QueuePage
}
QueueQuery filters the review queue.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service is the suggestion queue over the document store. Controlled terms are validated against the vocabulary index; folksonomy terms pass through the normalizer and their own moderation lifecycle.
func New ¶
New wires the service (zero-value caps fall back to DefaultCaps; a nil index rejects every controlled term but folk terms still work).
func (*Service) AcceptedFolkTerms ¶
func (s *Service) AcceptedFolkTerms(ctx context.Context, prefix string, limit int) ([]string, error)
AcceptedFolkTerms lists ACCEPTED folk terms matching prefix -- merged into autocomplete beside controlled vocabularies.
func (*Service) ApprovedUnpublished ¶
func (s *Service) ApprovedUnpublished(ctx context.Context) ([]Suggestion, error)
ApprovedUnpublished lists APPROVED aggregates not yet carried into the graph -- the publisher's worklist (tasks/036).
func (*Service) DecidePromotion ¶
func (s *Service) DecidePromotion(ctx context.Context, tag string, approve bool, actor string) (Promotion, error)
DecidePromotion resolves a pending proposal. Rejection just stamps it; approval stamps it and returns the promotion for the publisher to execute (the caller runs Publisher.PromoteTag and then MarkPromotionExecuted).
func (*Service) FolkTermStatus ¶
FolkTermStatus returns a folk term's lifecycle record.
func (*Service) ForWork ¶
ForWork returns the public per-work view: aggregates only, no supporter hashes, for the "N patrons suggested this" display.
func (*Service) GetPromotion ¶
GetPromotion returns one promotion by normalized tag.
func (*Service) ManualTerm ¶
func (s *Service) ManualTerm(ctx context.Context, workID string, ref vocab.TermRef, workTitle, actor string) error
ManualTerm lets a librarian add a term patrons and pipelines missed. The aggregate is born APPROVED (the librarian is the review) and flows to the graph on the next publish.
func (*Service) MarkPromotionExecuted ¶
MarkPromotionExecuted stamps the rewrite count after the publisher runs.
func (*Service) MarkPublished ¶
MarkPublished stamps aggregates with the grain etag that carried them.
func (*Service) PipelineSuggest ¶
func (s *Service) PipelineSuggest(ctx context.Context, workID string, term vocab.TermRef, confidence float64) error
PipelineSuggest lands one machine-produced candidate in the moderation queue: a PENDING aggregate with PIPELINE provenance and confidence, create-only (an existing aggregate for the pair -- from patrons or a prior run -- is left untouched, so re-running an enrichment source never spams the queue). The term is deliberately not gated by the vocabulary index: enrichment sources assert terms from vocabularies too large to load, and moderation is the gate. Tombstoned pairs are skipped silently.
func (*Service) Promotions ¶
Promotions lists proposals, optionally by status.
func (*Service) ProposePromotion ¶
func (s *Service) ProposePromotion(ctx context.Context, rawTag string, term vocab.TermRef, actor string) (Promotion, error)
ProposePromotion records a pending promotion. The target term must resolve in the vocabulary index -- promotions are into loaded controlled vocabularies.
func (*Service) Queue ¶
Queue lists aggregates in the requested status. It reads the status index partition and hydrates each aggregate; index items whose aggregate has moved on are deleted in passing (the index is repairable, the aggregate is truth).
func (*Service) Review ¶
Review applies a batch of staff decisions. Each decision flips a PENDING/DISPUTED aggregate to APPROVED or REJECTED, stamps the reviewer, and writes an audit entry. Rejections may tombstone the pair. Decisions against already-resolved items are skipped, not errors -- two reviewers may race on a hot queue.
func (*Service) SetFolkStatus ¶
func (s *Service) SetFolkStatus(ctx context.Context, norm string, status FolkStatus, actor string) error
SetFolkStatus accepts or blocks a folksonomy term. Accepted terms enter the autocomplete index; blocking removes them from it and refuses future suggestions.
func (*Service) Stats ¶
Stats aggregates a month's audit trail into editing statistics and session summaries. It is a read-only rollup over the same entries Audit returns; entries without an actor are ignored (they carry no editorial attribution).
func (*Service) Submit ¶
func (s *Service) Submit(ctx context.Context, in SubmitInput) (SubmitResult, error)
Submit records one anonymous suggestion/flag: term validation, tombstone and folk-lifecycle gates, per-supporter rate caps, supporter dedup, then the aggregate bump and dispute reconciliation. Unlike qllpoc's single TransactWriteItems this is a sequence of conditional writes -- a crash mid-sequence can lose one vote's count bump, which is acceptable for approximate supporter tallies (review is the arbiter); it can never double-count (the dedup marker is first) or corrupt review state.
func (*Service) SubmitConcern ¶ added in v0.61.0
func (s *Service) SubmitConcern(ctx context.Context, workID, note, workTitle, supporterHash string) error
SubmitConcern records an anonymous report-a-problem against a work: the freetext lands in Note, moderation happens in the same queue as term suggestions (resolve/dismiss), and nothing ever reaches the graph. The concern id is a content hash, so an identical resubmission is an idempotent no-op; rate caps are shared with term suggestions.
func (*Service) WriteAudit ¶
func (s *Service) WriteAudit(ctx context.Context, entry AuditEntry)
WriteAudit records a publish-lifecycle event not tied to one decision.
type Session ¶
type Session struct {
Start time.Time `json:"start"`
End time.Time `json:"end"`
Actions int `json:"actions"`
Works int `json:"works"`
}
Session is one contiguous run of a cataloger's activity -- actions no more than sessionGap apart -- with the works it touched.
type Status ¶
type Status string
Status is the review lifecycle of an aggregated (work, term, type) item.
type SubmitInput ¶
type SubmitInput struct {
WorkID string
Term vocab.TermRef
Type SuggType
Reason Reason // REMOVE only
SupporterHash string
WorkTitle string
SourceRef string
}
SubmitInput is one anonymous patron suggestion or flag. SupporterHash is HMAC(serverSecret, sourceIP) computed by the caller -- raw IPs never reach this package. Term.Label is display-only; identity is (Scheme, ID) and controlled terms must resolve in the vocabulary index.
type SubmitResult ¶
type SubmitResult struct {
// Duplicate is true when this supporter already voiced this exact
// (work, term, type) -- the call is an idempotent no-op.
Duplicate bool
// Disputed is true when the pair now has both ADD and REMOVE pressure.
Disputed bool
// FolkProposed is true when the submission's novel folksonomy term
// entered the PROPOSED lifecycle (held for moderation).
FolkProposed bool
}
SubmitResult reports the outcome of a Submit.
type SuggType ¶
type SuggType string
SuggType distinguishes a proposal to add a term from a flag to remove one.
type Suggestion ¶
type Suggestion struct {
WorkID string `json:"workId"`
Term vocab.TermRef `json:"term"`
Type SuggType `json:"type"`
Status Status `json:"status"`
SupporterCount int `json:"supporterCount"`
ReasonCounts map[Reason]int `json:"reasonCounts,omitempty"`
Provenance Provenance `json:"provenance"`
Confidence float64 `json:"confidence,omitempty"`
WorkTitle string `json:"workTitle,omitempty"`
SourceRef string `json:"sourceRef,omitempty"`
// Note carries a concern's freetext (TypeConcern only).
Note string `json:"note,omitempty"`
CreatedAt time.Time `json:"createdAt"`
LastActivityAt time.Time `json:"lastActivityAt"`
ReviewedAt time.Time `json:"reviewedAt,omitzero"`
ReviewedBy string `json:"reviewedBy,omitempty"`
ReviewNote string `json:"reviewNote,omitempty"`
// SubstituteTerm is set when the reviewer approved a neighbouring term
// instead of the suggested one; the publisher writes the substitute.
SubstituteTerm *vocab.TermRef `json:"substituteTerm,omitempty"`
PublishedAt time.Time `json:"publishedAt,omitzero"`
// PublishedETag is the grain ETag that carried the change into the
// graph (the git-commit-SHA analog for the S3 grain store).
PublishedETag string `json:"publishedEtag,omitempty"`
}
Suggestion is one aggregated (work, term, type) queue item. SupporterCount is the number of distinct supporter hashes; the hashes themselves live in separate TTL'd marker items and are never exposed.