gapreport

package
v0.0.2 Latest Latest
Warning

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

Go to latest
Published: Sep 12, 2026 License: AGPL-3.0 Imports: 11 Imported by: 0

Documentation

Overview

Package gapreport stores capability-gap reports: durable records the assistant writes when a provider service let a user down — either no tool existed for what they needed, or a tool ran and handed back something thin, misleading, or unactionable — so the provider's own team, not the consumer project the conversation happened in, can act on it. Reports are keyed by providerProject (see Report.ProviderProject), resolved from the composed capability document's spec.reportingProject, never from the conversation's project. This is an append-only log: unlike internal/memory, reports are written once via the report_capability_gap capability tool and never edited by the model.

Index

Constants

View Source
const MaxCapabilityKeyLen = 63

MaxCapabilityKeyLen caps the capability key in bytes. 63 is the DNS label bound, which is what a key must fit anyway: it is the name of a CapabilityGap object in the API.

View Source
const MaxCapabilityLen = 200

MaxCapabilityLen caps the short capability description in bytes.

View Source
const MaxEvidenceTextLen = 500

MaxEvidenceTextLen caps Evidence.Observed and Evidence.ContradictedBy in bytes. Evidence is a quoted fragment of tool output, not the whole response, and the bound limits how much can cross the project boundary at once.

View Source
const MaxEvidenceToolLen = 200

MaxEvidenceToolLen caps Evidence.Tool in bytes — it holds a tool name, not prose.

View Source
const MaxReportsPerProject = 500

MaxReportsPerProject caps how many reports a single provider project accumulates.

View Source
const MaxSummaryLen = 1000

MaxSummaryLen caps the summary in bytes.

Variables

View Source
var ErrCapabilityKeyTooLong = errors.New("gapreport: capability key exceeds MaxCapabilityKeyLen")

ErrCapabilityKeyTooLong is returned by Insert when capabilityKey exceeds MaxCapabilityKeyLen.

View Source
var ErrCapabilityTooLong = errors.New("gapreport: capability exceeds MaxCapabilityLen")

ErrCapabilityTooLong is returned by Insert when capability exceeds MaxCapabilityLen.

View Source
var ErrEvidenceTooLong = errors.New("gapreport: evidence field exceeds its maximum length")

ErrEvidenceTooLong is returned by Insert when an evidence field exceeds its bound (MaxEvidenceToolLen or MaxEvidenceTextLen).

View Source
var ErrInvalidCapabilityKey = errors.New("gapreport: capability key must be lowercase alphanumeric words joined by single dashes")

ErrInvalidCapabilityKey is returned by Insert for a key outside the [CapabilityKey] grammar. A key nobody types the same way twice cannot be recognized and reused, so junk is rejected rather than stored.

View Source
var ErrProjectFull = errors.New("gapreport: provider project exceeds MaxReportsPerProject")

ErrProjectFull is returned by Insert when a provider project already holds MaxReportsPerProject reports. Losing a gap report silently would defeat the point of the feature, so this rejects instead of evicting — the calling tool surfaces the error to the model.

View Source
var ErrSummaryTooLong = errors.New("gapreport: summary exceeds MaxSummaryLen")

ErrSummaryTooLong is returned by Insert when summary exceeds MaxSummaryLen.

View Source
var ErrUnknownKind = errors.New("gapreport: unknown kind")

ErrUnknownKind is returned by Insert (via ParseKind) for a kind outside Kinds. A missing kind has an obvious default; an unrecognized one would put a value in the provider's feed that no reader can interpret.

Kinds lists every valid Kind, in the order the report_capability_gap tool schema presents them.

Functions

func NeedsEvidence

func NeedsEvidence(k Kind) bool

NeedsEvidence reports whether a kind's report is only actionable with evidence. MissingCapability has no tool output to quote; every other kind is an accusation about output that exists, and without the quote the receiving team has nothing to check. Evidence is still not *enforced* — see Store.Insert.

func ValidateCapabilityKey

func ValidateCapabilityKey(key string) error

ValidateCapabilityKey checks a key against the grammar and MaxCapabilityKeyLen. The empty string is valid: a key is optional, and every row written before keys existed has none.

Types

type Aggregate

type Aggregate struct {
	// Key is the group's identity, and the name of the CapabilityGap object the
	// API projects from it: CapabilityKey when there is one, otherwise the
	// single report's own ID — see [Store].Aggregate.
	Key string
	// CapabilityKey is the model-coined key, empty for a group of one
	// keyless (pre-key, or filed-without-one) report.
	CapabilityKey string
	// ServiceName is the provider service the gap belongs to. Keys are
	// per-service: the same key filed against two services is two gaps.
	ServiceName string
	// Capability and Kind are taken from the most recent occurrence — the
	// freshest description of a gap that has been re-filed several times.
	Capability string
	Kind       Kind
	// Conversations counts DISTINCT ContextIDs, which is literally "how many
	// conversations hit this". Robust to one conversation filing twice.
	Conversations int
	// Occurrences counts reports, which can exceed Conversations.
	Occurrences int
	FirstSeen   time.Time
	LastSeen    time.Time
}

Aggregate is one distinct capability gap: every report sharing a Report.CapabilityKey for one service, collapsed into one entry with a count of how many conversations hit it. The occurrence rows behind it stay listable through Store.List, carrying the per-occurrence evidence.

It deliberately carries NO consumer identity — not ConsumerProject, not ContextID, not a per-customer breakdown. "How many conversations" is the prioritisation signal; "which of your customers" is a cross-tenant profile prioritisation does not need.

type Evidence

type Evidence struct {
	// Tool is the tool whose output is at fault, e.g. "workloads_list".
	Tool string
	// Observed is what that tool returned, e.g. "actionability: transient".
	Observed string
	// ContradictedBy is the fact that makes Observed wrong, thin, or
	// impossible to act on, e.g. "instance unchanged for 9d".
	ContradictedBy string
}

Evidence quotes the tool output a non-MissingCapability report is about. Its fields hold TOOL OUTPUT and OBJECT STATE only — never text from the user's message; the report_capability_gap tool description draws that line.

func (Evidence) IsZero

func (e Evidence) IsZero() bool

IsZero reports whether no evidence was supplied at all.

type InsertParams

type InsertParams struct {
	ProviderProject string
	ServiceName     string
	ConsumerProject string
	ContextID       string
	// CapabilityKey is optional but is what makes de-duplication work; see
	// [Report].CapabilityKey. Validated against [ValidateCapabilityKey].
	CapabilityKey string
	Capability    string
	Summary       string
	// Kind is optional; empty means KindMissingCapability.
	Kind Kind
	// Evidence is optional. Expected for every kind but MissingCapability,
	// though not required — see [Store].Insert.
	Evidence Evidence
}

InsertParams is the input to Store.Insert. A struct rather than positional arguments because providerProject, consumerProject, and contextID are all strings: a swap compiles fine and silently files into the wrong team's project.

type Kind

type Kind string

Kind classifies what kind of shortfall a report describes. A gap is not only an absent tool: a tool that answers with too little, misleadingly, or unactionably is a defect the provider's team cannot see from their side.

const (
	// KindMissingCapability: no tool covered what the user needed. The
	// zero value and the default for any report that does not say otherwise.
	KindMissingCapability Kind = "MissingCapability"
	// KindInsufficientDetail: a tool answered, but omitted a field the
	// answer needed to be actionable.
	KindInsufficientDetail Kind = "InsufficientDetail"
	// KindMisleadingOutput: a tool answered, and its output pointed at a
	// wrong conclusion.
	KindMisleadingOutput Kind = "MisleadingOutput"
	// KindUnactionableGuidance: a tool told the user to do something they
	// cannot do.
	KindUnactionableGuidance Kind = "UnactionableGuidance"
)

func ParseKind

func ParseKind(s string) (Kind, error)

ParseKind maps tool input to a Kind. The empty string is KindMissingCapability, so a provider that sets no kind and every row stored before kinds existed keep meaning exactly what they already meant.

type MemoryStore

type MemoryStore struct {
	// contains filtered or unexported fields
}

MemoryStore is an in-process Store. Reports live for the lifetime of the service process; PostgresStore is the durable equivalent behind the same interface.

func NewMemoryStore

func NewMemoryStore() *MemoryStore

NewMemoryStore returns an empty in-memory store.

func (*MemoryStore) Aggregate

func (s *MemoryStore) Aggregate(_ context.Context, providerProject string) ([]Aggregate, error)

Aggregate implements Store.

func (*MemoryStore) CapabilityKeys

func (s *MemoryStore) CapabilityKeys(ctx context.Context, providerProject, serviceName string, limit int) ([]string, error)

CapabilityKeys implements Store.

func (*MemoryStore) Insert

func (s *MemoryStore) Insert(_ context.Context, params InsertParams) (Report, error)

Insert implements Store.

func (*MemoryStore) List

func (s *MemoryStore) List(_ context.Context, providerProject string) ([]Report, error)

List implements Store.

type PostgresStore

type PostgresStore struct {
	// contains filtered or unexported fields
}

PostgresStore is a durable Store on PostgreSQL. Safe for concurrent use. Construct with NewPostgresStore, release with Close.

func NewPostgresStore

func NewPostgresStore(ctx context.Context, databaseURL string, logger *slog.Logger) (*PostgresStore, error)

NewPostgresStore connects to databaseURL (a postgres:// URL), verifies the connection, and applies the schema. It fails fast on an unreachable or unwilling database.

func (*PostgresStore) Aggregate

func (s *PostgresStore) Aggregate(ctx context.Context, providerProject string) ([]Aggregate, error)

Aggregate implements Store.

func (*PostgresStore) CapabilityKeys

func (s *PostgresStore) CapabilityKeys(ctx context.Context, providerProject, serviceName string, limit int) ([]string, error)

CapabilityKeys implements Store. Most-hit first, so a cap can only ever drop the keys least likely to be re-filed.

func (*PostgresStore) Close

func (s *PostgresStore) Close()

Close releases the connection pool.

func (*PostgresStore) Insert

func (s *PostgresStore) Insert(ctx context.Context, params InsertParams) (Report, error)

Insert implements Store. The project report-count bound is enforced inside the same transaction as the write, so concurrent inserts cannot race past MaxReportsPerProject.

func (*PostgresStore) List

func (s *PostgresStore) List(ctx context.Context, providerProject string) ([]Report, error)

List implements Store.

type Report

type Report struct {
	ID string
	// ProviderProject is the write key: the provider's own project
	// (spec.reportingProject on the capability document), where the
	// provider's team reviews reports.
	ProviderProject string
	// ServiceName is the provider service the gap belongs to (spec.serviceName).
	ServiceName string
	// ConsumerProject is the project the conversation happened in — provenance only.
	ConsumerProject string
	// ContextID is the conversation the gap arose in — provenance only.
	ContextID string
	// CapabilityKey names the gap in a form two conversations can agree on,
	// e.g. "workload-metrics". Capability is prose no two writings of the same
	// gap match; the key is what makes occurrences group. Empty on rows written
	// before keys existed, and on any report filed without one.
	//
	// It lives per occurrence and grouping happens at read time, so merging two
	// synonym keys stays one UPDATE and the next Aggregate re-derives every
	// count — the reason this is not an upsert onto a counter.
	CapabilityKey string
	// Capability is a short description of the capability at fault, e.g.
	// "list pipelines for StreamCo".
	Capability string
	// Summary is what the user was trying to do.
	Summary string
	// Kind classifies the shortfall. Always set on a report returned by a
	// [Store]; an unset stored value reads back as KindMissingCapability.
	Kind Kind
	// Evidence quotes the offending tool output. Zero for most
	// MissingCapability reports, which have nothing to quote.
	Evidence  Evidence
	CreatedAt time.Time
}

Report is one capability-gap report, attributed to the provider whose service fell short — not the consumer project the conversation ran in, which is carried only as provenance.

type Store

type Store interface {
	// List returns a provider project's reports, newest first. An unknown
	// project yields nil, nil. This is the occurrence view: one row per
	// report filed, each with its own evidence.
	List(ctx context.Context, providerProject string) ([]Report, error)
	// Aggregate returns a provider project's distinct gaps, one entry per
	// (service, capability key), most-hit first — ordered by Conversations
	// descending, then LastSeen descending, then Key ascending so the result
	// is deterministic. An unknown project yields nil, nil.
	//
	// Reports with no capability key are NOT merged with each other: each is
	// its own single-occurrence entry keyed by its report ID. Free prose is
	// exactly what cannot establish that two are the same gap, so merging them
	// would be a guess presented as a count. They still appear, unmerged, so
	// nothing already filed drops out of the provider's view.
	Aggregate(ctx context.Context, providerProject string) ([]Aggregate, error)
	// CapabilityKeys returns the keys already filed against one service in
	// one provider project, most-hit first (same ordering as Aggregate),
	// capped at limit (<= 0 means no cap). Reports with no key contribute
	// nothing.
	//
	// It returns bare keys and nothing else, on purpose: the result is injected
	// into a prompt running in SOME OTHER consumer's conversation. A key is a
	// bounded slug naming the provider's own capability; the report prose
	// around it is not, and has no business crossing into another tenant's
	// turn. This signature is where that boundary is enforced.
	CapabilityKeys(ctx context.Context, providerProject, serviceName string, limit int) ([]string, error)
	// Insert records a new report, assigning ID and CreatedAt, and defaulting
	// an empty Kind to KindMissingCapability. It returns ErrCapabilityTooLong,
	// ErrSummaryTooLong, ErrCapabilityKeyTooLong, ErrInvalidCapabilityKey,
	// ErrEvidenceTooLong, ErrUnknownKind, or ErrProjectFull; the store is left
	// unchanged.
	//
	// A malformed capability key is REJECTED because the caller can fix it on
	// the spot and retry. An omitted key is fine — it just does not group.
	//
	// A kind that [NeedsEvidence] with no evidence is ACCEPTED, not rejected:
	// rejecting drops the signal entirely and pushes the caller toward
	// relabelling it MissingCapability, and a wrong classification is worse for
	// the reader than a thin one. The nudge belongs in the tool result.
	Insert(ctx context.Context, params InsertParams) (Report, error)
}

Store persists and lists capability-gap reports. Implementations must be safe for concurrent use.

Jump to

Keyboard shortcuts

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