Help us shape the future of Go! Take the 2026 Go Developer Survey

coachapi

package
v0.10.0 Latest Latest
Warning

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

Go to latest
Published: Oct 12, 2026 License: BSD-2-Clause Imports: 4 Imported by: 0

Documentation

Overview

Package coachapi is the coach platform's job domain: job, finding, and report types; the stable API contract (error codes and envelope); the authenticated Principal; and the JobStore and WorkerJobStore persistence ports. Adapters live in subpackages that depend on this one: httpapi (the /v1/jobs HTTP surface), store/memory and store/postgres (the ports' implementations), queue (the TaskQueue port and its adapters), baseline (the repo_baseline_scan use case), and worker (claim/heartbeat/settle).

Index

Constants

View Source
const (
	ErrorCodeUnauthenticated    = "unauthenticated"
	ErrorCodeUnauthorized       = "unauthorized"
	ErrorCodeInvalidRequest     = "invalid_request"
	ErrorCodeJobNotFound        = "job_not_found"
	ErrorCodeJobNotCompleted    = "job_not_completed"
	ErrorCodeUnsupportedJobKind = "unsupported_job_kind"
	ErrorCodeRepoNotAuthorized  = "repo_not_authorized"
	ErrorCodeNotFound           = "not_found"
	ErrorCodeInternalError      = "internal_error"
)

Stable machine-readable API error codes.

View Source
const ReportVersion1 = "1"

ReportVersion1 is the frozen groundwork-era report_version value.

View Source
const TaskPayloadSchemaVersion1 = 1

TaskPayloadSchemaVersion1 is the supported queue.Task.Payload schema version for this package. ADR-006 requires versioned queue payloads; the worker decodes this wire shape independently of the job row.

Variables

View Source
var ErrClaimLost = errors.New("coachapi: claim lost")

ErrClaimLost is returned by fenced worker writes (Heartbeat, InsertFindings, InsertDiagnostics, CompleteJob, FailJob, ReleaseClaim) when (claimed_by, attempt) no longer match the caller's lease. The worker must abandon the job without acking its queue message.

View Source
var ErrJobNotFound = errors.New("coachapi: job not found")

ErrJobNotFound is returned (wrapped, so errors.Is works) by JobStore methods that look up a job or report by id when the id does not exist. Any other non-nil error from a JobStore method is a store failure and callers must fail closed (503), not treat it as "not found".

View Source
var ErrNotClaimable = errors.New("coachapi: job not claimable")

ErrNotClaimable is returned by ClaimJob when the job exists but is not in a claimable state (not queued, and not a stale running job).

Functions

func MarshalTaskPayload added in v0.2.0

func MarshalTaskPayload(jobID string) ([]byte, error)

MarshalTaskPayload returns the ADR-006 versioned queue.Task.Payload body for jobID. POST /v1/jobs and the worker requeue reconciler must use this helper so submit and recovery publish the same wire shape.

func WithPrincipal added in v0.2.0

func WithPrincipal(ctx context.Context, p Principal) context.Context

WithPrincipal returns a copy of ctx carrying p, retrievable via PrincipalFromContext.

Types

type APIError

type APIError struct {
	Code    string `json:"code"`
	Message string `json:"message"`
}

APIError is the machine+human pair inside the stable error envelope.

type ClaimLease added in v0.2.0

type ClaimLease struct {
	JobID     string
	WorkerID  string
	Attempt   int
	StartedAt time.Time
}

ClaimLease is the worker's exclusive right to process one attempt of a job after a successful ClaimJob. Attempt is the jobs.attempt value assigned by that claim (1-based after the first successful claim).

type Completion added in v0.2.0

type Completion struct {
	Attempt     int
	CommitSHA   string
	Findings    []JobFinding
	Diagnostics []JobDiagnostic
	Versions    ReportVersions
	FinishedAt  time.Time
	GeneratedAt time.Time
}

Completion is the input to JobStore.RecordCompletion: everything needed to finalize one successful job attempt and later assemble its Report.

type CreateJobRequest

type CreateJobRequest struct {
	Kind   JobKind         `json:"kind"`
	Params json.RawMessage `json:"params"`
}

CreateJobRequest is the body of POST /v1/jobs.

type CreateJobResponse

type CreateJobResponse struct {
	ID string `json:"id"`
}

CreateJobResponse is the 202 body after a durable submit.

type Diagnostic

type Diagnostic struct {
	Scope   string `json:"scope"`
	Message string `json:"message"`
}

Diagnostic is one report diagnostic entry.

func ReportDiagnostics added in v0.10.0

func ReportDiagnostics(jobDiagnostics []JobDiagnostic) []Diagnostic

type ErrorEnvelope

type ErrorEnvelope struct {
	Error APIError `json:"error"`
}

ErrorEnvelope is the stable JSON error shape for every API error response.

type Finding

type Finding struct {
	Source        FindingSource   `json:"source"`
	RubricID      *string         `json:"rubric_id"`
	RubricVersion *string         `json:"rubric_version"`
	ModelIdentity *string         `json:"model_identity"`
	Payload       json.RawMessage `json:"payload"`
}

Finding is one report finding with provenance fields. Deterministic findings carry null rubric_id, rubric_version, and model_identity.

func ReportFindings added in v0.10.0

func ReportFindings(jobFindings []JobFinding) []Finding

type FindingSource

type FindingSource string

FindingSource distinguishes deterministic analysis from agent judgments.

const (
	FindingSourceDeterministic FindingSource = "deterministic"
	FindingSourceAgent         FindingSource = "agent"
)

type Job

type Job struct {
	ID                string          `json:"id"`
	Kind              JobKind         `json:"kind"`
	Params            json.RawMessage `json:"params"`
	Status            JobStatus       `json:"status"`
	Error             *string         `json:"error"`
	CreatedAt         time.Time       `json:"created_at"`
	StartedAt         *time.Time      `json:"started_at,omitempty"`
	FinishedAt        *time.Time      `json:"finished_at,omitempty"`
	ClaimedBy         *string         `json:"claimed_by,omitempty"`
	HeartbeatAt       *time.Time      `json:"heartbeat_at,omitempty"`
	Attempt           int             `json:"attempt"`
	CreatedByProvider string          `json:"created_by_provider"`
	CreatedBySubject  string          `json:"created_by_subject"`
	CreatedByLogin    string          `json:"created_by_login"`
}

Job is the persisted jobs row (domain model, not the HTTP status view).

type JobDiagnostic

type JobDiagnostic struct {
	ID        string    `json:"id"`
	JobID     string    `json:"job_id"`
	Attempt   int       `json:"attempt"`
	Scope     string    `json:"scope"`
	Message   string    `json:"message"`
	CreatedAt time.Time `json:"created_at"`
}

JobDiagnostic is one attempt-scoped diagnostic row.

type JobFinding

type JobFinding struct {
	ID            string          `json:"id"`
	JobID         string          `json:"job_id"`
	Attempt       int             `json:"attempt"`
	Source        FindingSource   `json:"source"`
	RubricID      *string         `json:"rubric_id"`
	RubricVersion *string         `json:"rubric_version"`
	ModelIdentity *string         `json:"model_identity"`
	Payload       json.RawMessage `json:"payload"`
	PayloadHash   string          `json:"payload_hash"`
	CreatedAt     time.Time       `json:"created_at"`
}

JobFinding is one attempt-scoped finding row. For Source=deterministic, RubricID, RubricVersion, and ModelIdentity are null.

type JobKind

type JobKind string

JobKind identifies a supported async job type.

const (
	JobKindRepoBaselineScan JobKind = "repo_baseline_scan"
)

type JobStatus

type JobStatus string

JobStatus is the lifecycle state of a job row.

const (
	JobStatusQueued    JobStatus = "queued"
	JobStatusRunning   JobStatus = "running"
	JobStatusCompleted JobStatus = "completed"
	JobStatusFailed    JobStatus = "failed"
)

type JobStatusResponse

type JobStatusResponse struct {
	ID        string    `json:"id"`
	Kind      JobKind   `json:"kind"`
	Status    JobStatus `json:"status"`
	Attempt   int       `json:"attempt"`
	Error     *string   `json:"error"`
	ReportURL string    `json:"report_url,omitempty"`
}

JobStatusResponse is the body of GET /v1/jobs/{id}. Error is always present (JSON null when unset), matching Report.Error so clients use one nullability rule for job error fields.

type JobStore added in v0.2.0

type JobStore interface {
	// CreateJob persists a new job row. Callers set ID, Kind, Params,
	// Status (must be JobStatusQueued), CreatedAt, Attempt (must be 0), and
	// CreatedByProvider/Subject/Login before calling; CreateJob does not
	// default or mutate them. Returns an error (never ErrJobNotFound) on
	// store failure, including a duplicate ID. Does not enqueue on any
	// TaskQueue — that is the HTTP handler's job, per the submit-durability
	// rule (persist, then enqueue, then 202).
	CreateJob(ctx context.Context, job Job) error

	// GetJob returns the current row for id, or an error wrapping
	// ErrJobNotFound.
	GetJob(ctx context.Context, id string) (Job, error)

	// GetReport assembles and returns the Report for a completed job.
	// Callers (the HTTP handler) are responsible for checking
	// Job.Status == JobStatusCompleted first (via GetJob) and returning 409
	// job_not_completed themselves. Calling GetReport for a job id that
	// does not exist, or that has never had a successful RecordCompletion,
	// both return an error wrapping ErrJobNotFound; normal callers won't
	// hit the latter case because they check Job.Status first.
	GetReport(ctx context.Context, id string) (Report, error)

	// RecordCompletion finalizes one attempt of a job as successful: sets
	// status=completed, attempt=completion.Attempt,
	// finished_at=completion.FinishedAt, and durably records
	// completion.Findings/completion.Diagnostics (each already stamped
	// with completion.Attempt by the caller) plus the report-level
	// CommitSHA/Versions/GeneratedAt needed to assemble the Report later.
	// Returns an error wrapping ErrJobNotFound if the job id does not
	// exist.
	RecordCompletion(ctx context.Context, jobID string, completion Completion) error

	// RecordFailure finalizes a job attempt as failed: sets status=failed,
	// finished_at=finishedAt, error=errMsg. Returns an error wrapping
	// ErrJobNotFound if the job id does not exist.
	RecordFailure(ctx context.Context, jobID string, errMsg string, finishedAt time.Time) error
}

JobStore is the persistence seam POST/GET /v1/jobs handlers depend on. It has an in-memory implementation (store/memory) and a Postgres implementation (store/postgres). No method may leak an in-memory-only detail (e.g. no method returns a pointer into internal map storage) — implementations must return values a caller cannot use to mutate the store's internal state.

type Principal

type Principal struct {
	Provider string `json:"provider"`
	Subject  string `json:"subject"`
	Login    string `json:"login"`
}

Principal is the only identity type job handlers and authz checks consume. v1 GitHub maps Provider="github", Subject=<numeric user id string>, Login=<github login>.

func PrincipalFromContext added in v0.2.0

func PrincipalFromContext(ctx context.Context) (Principal, bool)

PrincipalFromContext returns the Principal attached by WithPrincipal, if any.

type RepoBaselineScanParams

type RepoBaselineScanParams struct {
	RepoOwner string `json:"repo_owner"`
	RepoName  string `json:"repo_name"`
	Ref       string `json:"ref,omitempty"`
}

RepoBaselineScanParams is the submit-time params schema for repo_baseline_scan. There is no client-supplied clone URL field; git_url/clone_url must be rejected at the API boundary.

type Report

type Report struct {
	ReportVersion string          `json:"report_version"`
	JobID         string          `json:"job_id"`
	Kind          JobKind         `json:"kind"`
	Params        json.RawMessage `json:"params"`
	CommitSHA     string          `json:"commit_sha"`
	Summary       ReportSummary   `json:"summary"`
	Findings      []Finding       `json:"findings"`
	Diagnostics   []Diagnostic    `json:"diagnostics"`
	Error         *string         `json:"error"`
	Versions      ReportVersions  `json:"versions"`
	GeneratedAt   time.Time       `json:"generated_at"`
}

Report is the frozen snake_case job report contract (report_version "1"). Error is always present: JSON null when the job succeeded, a string when it failed. Findings and Diagnostics always serialize as JSON arrays (never null); a nil Summary.FindingCounts serializes as {}.

func (Report) MarshalJSON

func (r Report) MarshalJSON() ([]byte, error)

MarshalJSON keeps the frozen wire shape: nil slices become [] and a nil finding_counts map becomes {}, so producers cannot accidentally emit null for fields the contract documents as arrays/objects.

type ReportSummary

type ReportSummary struct {
	FindingCounts map[string]map[string]int `json:"finding_counts"`
	PRCount       *int                      `json:"pr_count,omitempty"`
	PRFailedCount *int                      `json:"pr_failed_count,omitempty"`
}

ReportSummary is a named-field summary (not a free-form map) so kind-specific counters cannot collide with rule/rubric ids in finding_counts.

func SummarizeFindings added in v0.10.0

func SummarizeFindings(findings []JobFinding) ReportSummary

SummarizeFindings groups findings by source then rule id (deterministic, parsed from the finding payload's rule_id field) or rubric id (agent, from the finding's own RubricID) so counts cannot collide across sources.

type ReportVersions

type ReportVersions struct {
	Analyzer string            `json:"analyzer"`
	Rubrics  map[string]string `json:"rubrics,omitempty"`
}

ReportVersions records analyzer and rubric id→version pairs used to build a report.

type WorkerJobStore added in v0.2.0

type WorkerJobStore interface {
	JobStore

	// ClaimJob claims a queued job, or reclaims a running job whose
	// heartbeat is older than staleAfter (relative to now). On success it
	// increments attempt, deletes prior job_findings/job_diagnostics for the
	// job, sets status=running, claimed_by, heartbeat_at, and started_at, and
	// returns the new lease. Returns ErrJobNotFound or ErrNotClaimable.
	ClaimJob(ctx context.Context, jobID, workerID string, now time.Time, staleAfter time.Duration) (ClaimLease, error)

	// Heartbeat updates heartbeat_at when (claimed_by, attempt) match.
	// Returns ErrClaimLost on fence mismatch.
	Heartbeat(ctx context.Context, jobID, workerID string, attempt int, now time.Time) error

	// InsertFindings appends findings when (claimed_by, attempt) match.
	// Implementations stamp each finding's JobID and Attempt from the lease
	// args (caller-supplied values are ignored). Returns ErrClaimLost on
	// fence mismatch.
	InsertFindings(ctx context.Context, jobID, workerID string, attempt int, findings []JobFinding) error

	// InsertDiagnostics appends diagnostics when (claimed_by, attempt) match.
	// Implementations stamp each diagnostic's JobID and Attempt from the
	// lease args (caller-supplied values are ignored). Returns ErrClaimLost
	// on fence mismatch.
	InsertDiagnostics(ctx context.Context, jobID, workerID string, attempt int, diagnostics []JobDiagnostic) error

	// CompleteJob fenced-terminal-transitions the job to completed and
	// records report assembly fields. Findings already inserted for this
	// attempt (via InsertFindings) are kept; completion.Findings are also
	// inserted when non-empty. Returns ErrClaimLost on fence mismatch.
	CompleteJob(ctx context.Context, jobID, workerID string, attempt int, completion Completion) error

	// FailJob fenced-terminal-transitions the job to failed.
	// Returns ErrClaimLost on fence mismatch.
	FailJob(ctx context.Context, jobID, workerID string, attempt int, errMsg string, finishedAt time.Time) error

	// ReleaseClaim fenced-transitions a running job back to queued and clears
	// claim fields so a retryable handler failure can Nack(false) and the next
	// ClaimJob can take ownership. It does not increment attempt (ClaimJob
	// does that on the next successful claim). Returns ErrClaimLost on fence
	// mismatch.
	ReleaseClaim(ctx context.Context, jobID, workerID string, attempt int) error

	// ListQueuedOlderThan returns queued jobs with created_at strictly before
	// olderThan, for the requeue reconciler.
	ListQueuedOlderThan(ctx context.Context, olderThan time.Time) ([]Job, error)

	// ReleaseStaleRunning sets running jobs whose heartbeat is older than
	// staleAfter (relative to now) back to queued and clears claim fields so
	// the requeue reconciler can re-enqueue them. It does not increment
	// attempt (ClaimJob does that on the next successful claim).
	ReleaseStaleRunning(ctx context.Context, now time.Time, staleAfter time.Duration) (released int, err error)
}

WorkerJobStore is the persistence seam coach-worker depends on: JobStore plus claim/heartbeat/fenced-write and reconciler listing. MemoryStore and PostgresStore implement it; HTTP handlers continue to use JobStore only.

Directories

Path Synopsis
Package baseline runs one repo_baseline_scan job attempt: it resolves and reads the repository tree (GitHub Contents API, or the operator-configured smoke fixture), analyzes it through agentloop tools into deterministic findings, then judges hidden-mutation and change-cohesion findings with rubric tools, persisting rows through a fenced JobWriter as it goes.
Package baseline runs one repo_baseline_scan job attempt: it resolves and reads the repository tree (GitHub Contents API, or the operator-configured smoke fixture), analyzes it through agentloop tools into deterministic findings, then judges hidden-mutation and change-cohesion findings with rubric tools, persisting rows through a fenced JobWriter as it goes.
Package httpapi serves POST /v1/jobs, GET /v1/jobs/{id}, and GET /v1/jobs/{id}/report over a coachapi.JobStore, an authz.RepoAuthorizer, and a queue.TaskQueue.
Package httpapi serves POST /v1/jobs, GET /v1/jobs/{id}, and GET /v1/jobs/{id}/report over a coachapi.JobStore, an authz.RepoAuthorizer, and a queue.TaskQueue.
Package queue defines the application-owned TaskQueue and EventBus ports required by ADR-006 (docs/architecture/ADR-006-watermill-queue-abstraction.md): coach-api and coach-worker depend only on these interfaces, never on Watermill or a specific broker's types, so the backend (Redis Streams, SQS, ...) can be swapped without touching domain code.
Package queue defines the application-owned TaskQueue and EventBus ports required by ADR-006 (docs/architecture/ADR-006-watermill-queue-abstraction.md): coach-api and coach-worker depend only on these interfaces, never on Watermill or a specific broker's types, so the backend (Redis Streams, SQS, ...) can be swapped without touching domain code.
redisstream
Package redisstream implements the internal/coachapi/queue.TaskQueue port (ADR-006, docs/architecture/ADR-006-watermill-queue-abstraction.md) on top of Watermill's Redis Streams pub/sub (github.com/ThreeDotsLabs/watermill-redisstream), for coach's non-AWS / self-hosted deployment target.
Package redisstream implements the internal/coachapi/queue.TaskQueue port (ADR-006, docs/architecture/ADR-006-watermill-queue-abstraction.md) on top of Watermill's Redis Streams pub/sub (github.com/ThreeDotsLabs/watermill-redisstream), for coach's non-AWS / self-hosted deployment target.
sqs
Package sqs implements the internal/coachapi/queue.TaskQueue port (ADR-006, "Provider configuration" -> aws-sqs) on top of Amazon SQS.
Package sqs implements the internal/coachapi/queue.TaskQueue port (ADR-006, "Provider configuration" -> aws-sqs) on top of Amazon SQS.
store
memory
Package memory is the in-process coachapi.WorkerJobStore used for local development and tests.
Package memory is the in-process coachapi.WorkerJobStore used for local development and tests.
postgres
Package postgres is the durable coachapi.WorkerJobStore backed by Postgres; its schema is internal/coachapi/migrations.
Package postgres is the durable coachapi.WorkerJobStore backed by Postgres; its schema is internal/coachapi/migrations.

Jump to

Keyboard shortcuts

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