Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion internal/receipt/markdown.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ func WriteMarkdown(w io.Writer, r schema.Receipt) {
if cov.SuppressedMentions > 0 {
fmt.Fprintf(w, "<sub>%s</sub>\n", mentionNote(cov.SuppressedMentions))
}
fmt.Fprintf(w, "\n<sub>schema %s Β· exit gate: refuted claims block; the remainder informs, never fails</sub>\n", r.SchemaVersion)
fmt.Fprintf(w, "\n<sub>schema %s%s Β· exit gate: refuted claims block; the remainder informs, never fails</sub>\n", r.SchemaVersion, toolNote(r))
}

// mdCell makes text safe inside a markdown table cell.
Expand Down
53 changes: 52 additions & 1 deletion internal/receipt/receipt.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"path"
"path/filepath"
"regexp"
"runtime/debug"
"strings"
"sync"

"github.com/joshft/correctful/internal/gitdiff"
"github.com/joshft/correctful/schema"
Expand Down Expand Up @@ -59,6 +61,7 @@ func Assemble(change gitdiff.Change, claims []schema.Claim, evidence [][]schema.

return schema.Receipt{
SchemaVersion: schema.SchemaVersion,
ToolVersion: toolVersion(),
Change: schema.ChangeRef{
// The receipt carries the repository NAME, never its location: a
// receipt is shareable, and external-tool details are scrubbed of
Expand Down Expand Up @@ -86,6 +89,45 @@ func Assemble(change gitdiff.Change, claims []schema.Claim, evidence [][]schema.
}
}

// toolVersion identifies this checker build from its own build info β€” the
// first chain field: two receipts are comparable across time only when the
// harvesters and runners that produced them are known. Module version plus
// short VCS revision when the build is stamped (`go install` from a clone
// stamps both; a dirty tree gains "+dirty"), "unknown" when the build
// carries no identity β€” stated, never guessed.
var toolVersion = sync.OnceValue(func() string {
bi, ok := debug.ReadBuildInfo()
if !ok {
return "unknown"
}
v := bi.Main.Version
rev, dirty := "", false
for _, s := range bi.Settings {
switch s.Key {
case "vcs.revision":
rev = s.Value
case "vcs.modified":
dirty = s.Value == "true"
}
}
if len(rev) > 12 {
rev = rev[:12]
}
if rev != "" && dirty {
rev += "+dirty"
}
switch {
case v != "" && rev != "":
return v + " " + rev
case v != "":
return v
case rev != "":
return rev
default:
return "unknown"
}
})

// anchoringSummary tallies the binding layer's resolution outcomes. Nil when
// no claim carries an anchor β€” a repo without a definition corpus.
func anchoringSummary(claims []schema.Claim) *schema.AnchoringSummary {
Expand Down Expand Up @@ -350,7 +392,7 @@ func WriteJSON(w io.Writer, r schema.Receipt) error {
// every other tool hides.
func WriteText(w io.Writer, r schema.Receipt) {
s := r.Summary
fmt.Fprintf(w, "correctful receipt (schema %s)\n", r.SchemaVersion)
fmt.Fprintf(w, "correctful receipt (schema %s%s)\n", r.SchemaVersion, toolNote(r))
fmt.Fprintf(w, "change: %s...%s%s", r.Change.BaseRef, r.Change.HeadRef, shaNote(r.Change))
if r.Change.Repo != "" {
fmt.Fprintf(w, " [%s]", r.Change.Repo)
Expand Down Expand Up @@ -438,6 +480,15 @@ func mentionNote(n int) string {
return fmt.Sprintf("%d spec-id mention(s) not minted as claims β€” the repo defines no spec-id corpus, so a reference has no possible referent", n)
}

// toolNote renders the producing build beside the schema version β€” shared by
// both renderers so the chain field is visible wherever the receipt is read.
func toolNote(r schema.Receipt) string {
if r.ToolVersion == "" {
return ""
}
return " Β· correctful " + r.ToolVersion
}

// detailOf picks the evidence detail a reader needs: for a refuted claim, the
// FAILING probe's detail β€” a claim with five probes where the fourth failed
// must not display the first probe's "ok".
Expand Down
21 changes: 21 additions & 0 deletions internal/receipt/receipt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -492,3 +492,24 @@ func TestLLMVerifiedRowKeepsProvenance(t *testing.T) {
}
}
}

// TestReceiptCarriesToolVersion: the first chain field. Every receipt names
// the build that produced it β€” "unknown" when the build carries no identity,
// never empty β€” and both renderers show it beside the schema version.
func TestReceiptCarriesToolVersion(t *testing.T) {
claims, evidence := sampleClaims()
r := Assemble(gitdiff.Change{BaseRef: "main", HeadRef: "wip"}, claims, evidence, schema.Coverage{})
if r.ToolVersion == "" {
t.Fatal("tool version empty β€” must be stated, or stated as unknown")
}
var text, md strings.Builder
WriteText(&text, r)
WriteMarkdown(&md, r)
want := "correctful " + r.ToolVersion
if !strings.Contains(text.String(), want) {
t.Errorf("text receipt lacks %q", want)
}
if !strings.Contains(md.String(), want) {
t.Errorf("markdown receipt lacks %q", want)
}
}
16 changes: 12 additions & 4 deletions schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -374,9 +374,17 @@ type Coverage struct {
// Receipt is the per-change output: what was claimed, what was verified, and β€”
// the load-bearing part β€” the honest unverified remainder.
type Receipt struct {
SchemaVersion string `json:"schema_version"`
Change ChangeRef `json:"change"`
Results []ClaimResult `json:"results"`
SchemaVersion string `json:"schema_version"`
// ToolVersion identifies the checker build that produced this receipt β€”
// module version plus VCS revision when the build carries them, so two
// receipts are comparable across time only when the harvesters and
// runners that produced them are known. One field covers every in-tree
// component (they ship in one binary); per-supplier versions arrive with
// the evidence-intake contract. "unknown" when the build carries no
// identity β€” stated, never guessed.
ToolVersion string `json:"tool_version,omitempty"`
Change ChangeRef `json:"change"`
Results []ClaimResult `json:"results"`
// Remainder is the subset of Results with Status == StatusUnverified,
// surfaced explicitly so a reader never has to derive it. This is the
// feature no other tool in the field ships.
Expand All @@ -386,4 +394,4 @@ type Receipt struct {
}

// SchemaVersion is the current version of the receipt schema (the payload).
const SchemaVersion = "0.0.9"
const SchemaVersion = "0.0.10"
Loading