diff --git a/internal/receipt/markdown.go b/internal/receipt/markdown.go
index f18a7ca..81884bb 100644
--- a/internal/receipt/markdown.go
+++ b/internal/receipt/markdown.go
@@ -91,7 +91,7 @@ func WriteMarkdown(w io.Writer, r schema.Receipt) {
if cov.SuppressedMentions > 0 {
fmt.Fprintf(w, "%s\n", mentionNote(cov.SuppressedMentions))
}
- fmt.Fprintf(w, "\nschema %s · exit gate: refuted claims block; the remainder informs, never fails\n", r.SchemaVersion)
+ fmt.Fprintf(w, "\nschema %s%s · exit gate: refuted claims block; the remainder informs, never fails\n", r.SchemaVersion, toolNote(r))
}
// mdCell makes text safe inside a markdown table cell.
diff --git a/internal/receipt/receipt.go b/internal/receipt/receipt.go
index 729e5ae..4bb83f8 100644
--- a/internal/receipt/receipt.go
+++ b/internal/receipt/receipt.go
@@ -11,7 +11,9 @@ import (
"path"
"path/filepath"
"regexp"
+ "runtime/debug"
"strings"
+ "sync"
"github.com/joshft/correctful/internal/gitdiff"
"github.com/joshft/correctful/schema"
@@ -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
@@ -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 {
@@ -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)
@@ -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".
diff --git a/internal/receipt/receipt_test.go b/internal/receipt/receipt_test.go
index 661ec69..1eef2f8 100644
--- a/internal/receipt/receipt_test.go
+++ b/internal/receipt/receipt_test.go
@@ -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)
+ }
+}
diff --git a/schema/schema.go b/schema/schema.go
index 771a3e1..4df49a8 100644
--- a/schema/schema.go
+++ b/schema/schema.go
@@ -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.
@@ -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"