Documentation
¶
Overview ¶
Package evidence implements the local evidence loop behind `csx run` (goal.md §8.3, contract C14): scan the project, run the user's command, record aggregate observations for PUBLIC packages only, and drain them into anonymous wire batches. Raw logs and paths never enter any uploadable structure; PRIVATE and UNKNOWN packages never enter the observations table at all.
Index ¶
- Constants
- Variables
- func PrepareWantedForUpload(ctx context.Context, payload string, ...) ([]byte, error)
- func QueueSearchHit(ctx context.Context, db *localdb.DB, ident *identity.Identity, ...)
- func QueueWanted(ctx context.Context, db *localdb.DB, ident *identity.Identity, ...)
- func Scan(ctx context.Context, dir string, checker *registry.Checker) (*scanner.ScanResult, error)
- type Batcher
- type CommandOutput
- type EvidenceGap
- type FailureAnalysis
- type FailureEvent
- type PublicnessCache
- type Recorder
- func (r *Recorder) RecordCommandOutput(ctx context.Context, dir string, res *scanner.ScanResult, ...) error
- func (r *Recorder) RecordRun(ctx context.Context, dir string, res *scanner.ScanResult, ...) error
- func (r *Recorder) RecordTerminatedRun(ctx context.Context, dir string, res *scanner.ScanResult, ...) error
- type SearchHit
- type StageEvidence
- type WantedReport
Constants ¶
const ( StageEvidenceStructuredTermination = domain.FailureStageStructuredTermination StageEvidenceResolveDiagnostic = domain.FailureStageResolveDiagnostic StageEvidenceCompilerDiagnostic = domain.FailureStageCompilerDiagnostic StageEvidenceTestRunnerDiagnostic = domain.FailureStageTestRunnerDiagnostic StageEvidenceBuildAggregate = domain.FailureStageBuildAggregate StageEvidenceUnclassifiedDiagnostic = domain.FailureStageUnclassifiedDiagnostic )
const ( EvidenceGapDiagnosticMissing = domain.FailureDiagnosticMissing EvidenceGapStageUnknown = domain.FailureStageUnknown )
const SearchHitQueueKind = "search-hit"
SearchHitQueueKind is the upload queue kind for a search that found something. It is deliberately its own kind: a wanted ask is a package the network should learn, and a hit is a count.
const WantedCandidateQueueKind = "wanted_candidate"
WantedCandidateQueueKind is deliberately not the legacy "wanted" kind. Older daemons know how to POST "wanted" rows without a client-side publicness recheck. Giving unverified local candidates a new kind means an upgraded MCP process can safely share a home with an older daemon: the older process leaves the unfamiliar row alone instead of transmitting it.
Variables ¶
var ErrWantedPublicnessUnconfirmed = errors.New("wanted package publicness is unconfirmed")
ErrWantedPublicnessUnconfirmed means a candidate was syntactically safe to keep locally, but no package in it was confirmed public. The queue drainer treats this as retryable because a registry outage and a private/nonexistent coordinate are intentionally indistinguishable at this privacy boundary.
Functions ¶
func PrepareWantedForUpload ¶ added in v0.1.5
func PrepareWantedForUpload(ctx context.Context, payload string, isPublic func(context.Context, domain.PURL) bool) ([]byte, error)
PrepareWantedForUpload applies the last, fail-closed privacy gate at the point where a queued candidate is about to leave the machine. It decodes into the fixed wire type and marshals it again, so even a hand-edited queue row cannot smuggle an unknown query/path/log field through the daemon.
isPublic must check the exact package version. A nil checker or a false result never transmits that coordinate. The server repeats this check on receipt; neither side treats PURL syntax as proof of public existence.
func QueueSearchHit ¶ added in v0.1.37
func QueueSearchHit(ctx context.Context, db *localdb.DB, ident *identity.Identity, cfg *config.Config, hit SearchHit)
QueueSearchHit records that a search found something, for upload.
The network can see the demand it cannot satisfy and is blind to the demand it does. A miss uploads a Wanted ask, so misses arrive — 648 of them in the first week. A hit wrote a local row that never left the machine, so the server had recorded five hits in its lifetime while 130 distinct samples were adopted, and a sample can only be adopted after a hit surfaced it.
Every rate the product is steered by needs this as its denominator: samples shown per search, applied per shown. Without it the numerator arrives alone.
Community mode only, like every upload, and counts only.
func QueueWanted ¶ added in v0.1.4
func QueueWanted(ctx context.Context, db *localdb.DB, ident *identity.Identity, cfg *config.Config, req domain.SearchRequest)
QueueWanted records that the network had no answer, so the question can be counted and eventually answered by someone.
It goes through the upload queue rather than straight to the network: the queue retries, works offline, and a search must never wait on a report. It lives here rather than in the daemon because AGENTS ARRIVE OVER MCP — the daemon's HTTP search is not the path that matters, and wiring the signal only there meant the one caller who actually asks questions was the one caller whose misses were thrown away.
THE QUESTION IS NEVER SENT. A typed question carries project names, file paths, sometimes an error string with a hostname in it, and goal.md §8.5 keeps all of that on the machine. The local candidate contains only package coordinates and an unambiguous symbol; the daemon still must confirm the exact release on its public registry before any of it is eligible to travel.
func Scan ¶
Scan detects the ecosystems present in dir and runs the scan orchestrator (contract C14 step 1). A nil checker skips the publicness pass — packages stay UNKNOWN and are excluded from evidence. This wrapper lives here rather than in internal/scanner because both adapters and internal/registry import internal/scanner.
Types ¶
type Batcher ¶
Batcher drains pending local observation aggregates into anonymous wire batches (contract C14 step 6). Batches carry only the fields of domain.ObservationBatch — rotating IDs, hashes, and counts; never paths, project names, or raw logs.
func (*Batcher) Drain ¶
Drain builds wire batches from every pending observation aggregate and marks the drained rows uploaded in the same step (read and mark back-to-back, no I/O in between — the localdb contract: rows carry FULL epoch counts and any later increment flips a row back to pending, so a full-count re-send delta-merges safely server-side). Ownership of the returned batches passes to the caller.
func (*Batcher) Upload ¶
func (b *Batcher) Upload(ctx context.Context, httpClient *http.Client, serverURL string) (int, error)
Upload drains pending observations and POSTs them to {serverURL}/v1/evidence/batches as {"batches":[...]}. Community mode only: any other mode is a silent no-op. On a non-2xx response or a transport error the affected rows are restored to pending so no evidence is lost while the server is unreachable (goal.md §25.F). It returns how many batches the server accepted.
The upload is chunked to the server's documented per-request limit and repeats until the queue is empty. Posting a whole drain in one request meant any backlog larger than that limit was rejected wholesale and retried forever — the exact shape of a first sync after scanning a machine's worth of projects.
type CommandOutput ¶ added in v0.1.42
type CommandOutput struct {
Stdout string
Stderr string
StdoutTruncated bool
StderrTruncated bool
Termination domain.FailureTermination
}
CommandOutput is the local copy of the command's two output streams. They stay separate so a recommendation can never replace or masquerade as the command's own diagnostics.
StdoutTruncated / StderrTruncated are load-bearing: false is a claim that the tail IS the whole stream, and callers (MCP `run_observed_command` among them) show the capture unqualified when it is false. A false negative here presents a clipped log as a complete one.
func Run ¶
func Run(ctx context.Context, argv []string, dir string) (exitCode int, output CommandOutput, err error)
Run spawns argv[0] with the remaining args in dir, inheriting stdio so the wrapped command behaves exactly as if run directly. stdout and stderr are additionally teed into separate bounded in-memory tails (capture only; the caller sanitizes stderr before anything is stored in an uploadable row). stderr is also appended raw to $CSX_HOME/logs/last-run.log, which never leaves the machine.
The child's exit code passes through: a non-zero child exit is NOT an error here (exitCode carries it, err stays nil). err is reserved for failures to run at all (command not found, ...).
func (CommandOutput) FailureDiagnostics ¶ added in v0.1.45
func (o CommandOutput) FailureDiagnostics() string
FailureDiagnostics is the stream a failed command should be diagnosed and fingerprinted from.
It was stderr, unconditionally, and half the toolchains an agent runs do not report there. tsc writes its diagnostics to stdout and exits non-zero with an empty stderr; so do go test, pytest, and most npm scripts. Sanitizing that empty stream still produced a fingerprint — the hash of a blank template — so a broken typecheck was RECORDED under, and SEARCHED for by, the hash of nothing at all. Nothing has ever failed that way, so the fingerprint matched nothing, and the free-text query left over was the word "fingerprint" and 64 hex digits. The engine answered it with whatever was nearest, which is how a failing TypeScript build came back with a Dart sample about package:crypto, graded COMPATIBLE, over a stdout the tool was holding the whole time.
The rule, in order:
- The stream naming an error code wins, stderr first. A code is what a stored failure is keyed by, and a runner's own boilerplate ("npm error Lifecycle script failed") names none while the compiler underneath it names TS2352.
- With no code anywhere, stderr if it said anything — so every command that already reported there keeps the exact text, and the exact fingerprint, it had before.
- Otherwise stdout.
A command that printed nothing at all diagnoses nothing, and says so by returning "". That is not a gap to paper over with a hash: an empty answer is what stops a question nobody can answer from being asked.
type EvidenceGap ¶ added in v0.1.46
type EvidenceGap = domain.FailureEvidenceGap
EvidenceGap explains which part of a failure event could not be established.
type FailureAnalysis ¶ added in v0.1.46
type FailureAnalysis struct {
OuterCommand string
OuterStage domain.Stage
Events []FailureEvent
}
FailureAnalysis separates the user's outer workflow intent from each actual failing stage. OuterCommand is privacy-bounded to a known public tool plus a known subcommand; project paths and arbitrary script names are never copied.
func AnalyzeFailure ¶ added in v0.1.46
func AnalyzeFailure(profile scanner.CommandProfile, argv []string, output CommandOutput) FailureAnalysis
AnalyzeFailure classifies actual failing stages from structured termination and bounded stdout/stderr. CommandProfile is retained only as outer intent and as a conservative tool hint after a real test-runner marker is found.
type FailureEvent ¶ added in v0.1.46
type FailureEvent struct {
Stage domain.Stage
Toolchain string
DiagnosticCode string
Diagnostic string
StageEvidence StageEvidence
EvidenceGap EvidenceGap
}
FailureEvent is one independently evidenced failure inside an outer command. Diagnostic remains local until the sanitizer converts it to public evidence.
type PublicnessCache ¶
PublicnessCache adapts localdb's packages table to registry.Cache so publicness verdicts persist across runs (24h TTL enforced by the checker via the stored checked_at stamp).
func (PublicnessCache) GetPublicness ¶
GetPublicness looks up a cached verdict by canonical purl string.
func (PublicnessCache) SetPublicness ¶
func (c PublicnessCache) SetPublicness(ctx context.Context, purl, status string) error
SetPublicness stores a registry check outcome.
type Recorder ¶
Recorder turns one wrapped command run into local aggregate observations (contract C14 steps 4–5). Only PUBLIC packages ever reach the observations table; PRIVATE and UNKNOWN packages are recorded in the local packages inventory only, which is never uploaded.
func (*Recorder) RecordCommandOutput ¶ added in v0.1.46
func (r *Recorder) RecordCommandOutput(ctx context.Context, dir string, res *scanner.ScanResult, profile scanner.CommandProfile, argv []string, exitCode int, output CommandOutput) error
RecordCommandOutput analyzes the actual failure markers before recording. One outer execution may therefore append multiple independent failure rows.
func (*Recorder) RecordRun ¶
func (r *Recorder) RecordRun(ctx context.Context, dir string, res *scanner.ScanResult, profile scanner.CommandProfile, exitCode int, failureTail string) error
RecordRun records the outcome of one `csx run` execution.
- Every scanned package is upserted into the local packages table (local diagnostics; never uploaded).
- Known commands record, per PUBLIC package, one package-level observation (symbol "") and one observation per public symbol usage, at the classified stage with PASS/FAIL from exitCode. On FAIL the failure tail — the stream that carried the diagnosis, see CommandOutput.FailureDiagnostics — is sanitized (stage-aware) and only the resulting fingerprint + error code are attached, never the raw text.
- Unknown commands (profile.Known == false) prove nothing beyond "these public packages are in use": they record only USED/PASS package rows.
- Symbol sightings are stored with the rotating project bucket derived from the absolute project path (HMAC, irreversible).
func (*Recorder) RecordTerminatedRun ¶ added in v0.1.45
func (r *Recorder) RecordTerminatedRun(ctx context.Context, dir string, res *scanner.ScanResult, profile scanner.CommandProfile, term domain.FailureTermination, failureTail string) error
RecordTerminatedRun records a caller-observed structured termination, including exit, timeout, signal, and process-start failure. Callers pass the structured state; this method never guesses one from missing fields.
type SearchHit ¶ added in v0.1.37
type SearchHit struct {
// Grade is the match grade of the top result, as a bucket.
Grade domain.MatchGrade
// ResultsShown is how many results the caller was handed.
ResultsShown int
// OfferID ties this hit to the adoption that may follow it. It is an
// opaque local capability with nothing derivable in it, and it is the
// only thing that can turn "applied" into "applied out of how many
// shown" — the adoptions table has a numerator and no denominator.
OfferID string
// SampleID is a content address of an already-published sample: public
// by construction, and what makes "which answers get used" answerable.
SampleID string
}
SearchHit is what one successful search is allowed to say out loud.
It carries no question. Not the query, not the packages, not the symbols, not the environment — those are the caller's and stay on the caller's machine, which is the rule the local hits table has always followed and the reason hits were never uploaded at all.
type StageEvidence ¶ added in v0.1.46
type StageEvidence = domain.FailureStageEvidence
StageEvidence names the concrete evidence used to override command intent. It is intentionally a small public vocabulary rather than raw parser prose.
type WantedReport ¶ added in v0.1.5
type WantedReport struct {
SchemaVersion int `json:"schemaVersion"`
Epoch string `json:"epoch"`
AnonID string `json:"anonId"`
Packages []string `json:"packages"`
Symbols []string `json:"symbols,omitempty"`
// OS is the platform the miss happened on, optional: a report without
// one is a question about the package rather than about a platform. It
// is what lets the work queue hand a Windows ask to a Windows verifier
// instead of closing it with a Linux pass.
OS string `json:"os,omitempty"`
}
WantedReport is the complete privacy-reduced wire shape. It contains no prose query, project path, dependency-tree context, hostname, or raw log. Candidates use the same shape locally, but are not eligible for upload until PrepareWantedForUpload confirms and rewrites their package list.