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
- Variables
- func MarshalTaskPayload(jobID string) ([]byte, error)
- func WithPrincipal(ctx context.Context, p Principal) context.Context
- type APIError
- type ClaimLease
- type Completion
- type CreateJobRequest
- type CreateJobResponse
- type Diagnostic
- type ErrorEnvelope
- type Finding
- type FindingSource
- type Job
- type JobDiagnostic
- type JobFinding
- type JobKind
- type JobStatus
- type JobStatusResponse
- type JobStore
- type Principal
- type RepoBaselineScanParams
- type Report
- type ReportSummary
- type ReportVersions
- type WorkerJobStore
Constants ¶
const ( ErrorCodeUnauthenticated = "unauthenticated" 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.
const ReportVersion1 = "1"
ReportVersion1 is the frozen groundwork-era report_version value.
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 ¶
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.
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".
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
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.
Types ¶
type ClaimLease ¶ added in v0.2.0
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 ¶
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 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>.
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 ¶
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.
Source Files
¶
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. |