[object Object]

← back to Cli Printing Press

feat(regenmerge): add TEMPLATED-BODY-DRIFT verdict to catch in-place body edits (#468)

abde446ebe376def6f2380d9c400a2132da3e07b · 2026-05-01 15:49:52 -0700 · Trevin Chow

Adds a 6th classification verdict that catches the failure mode the
original plan acknowledged as a known limitation: pub modifies the body
of an existing templated function in-place (calls a hand-written helper,
adds a header-injection loop, etc.) without changing the function's
top-level decl name. Decl-set comparison says TEMPLATED-CLEAN, take-fresh
overwrites the file, the integration is silently destroyed.

Surfaced by sweep dogfood:
- cal-com (#175): pub's client.go do() called requiredHeadersForPath()
  inside the templated function, integrating with calcom_versions.go's
  per-endpoint API-version map. Take-fresh dropped the call; v2 endpoints
  silently degraded.
- pagliacci-pizza (#174): pub's client.go New() chained
  surf.Builder().Impersonate().Chrome() for cloud-bypass HTTP. Take-fresh
  reverted to the stdlib http.Client. Currently works against pagliacci
  but the integration was hand-written for a reason.

Mechanism: per file classified as TEMPLATED-CLEAN by the existing
decl-set check, parse pub and fresh function bodies and collect the
call-target identifier set per function. Any pub function whose body
calls identifiers fresh's same-named function doesn't is flagged as
TEMPLATED-BODY-DRIFT and surfaced for human review instead of auto-
overwritten.

Conservative by design — fires only when both files define a function
with the same canonical name. AddCommand args skip recursion (handled
separately by the lost-registration restoration path).

Tests: 3 regression cases pinning the cal-com shape (helper call dropped),
the pagliacci shape (chained .Method().Method() drop), the
AddCommand-args false-positive guard, and the no-drift baseline.

Reuse cleanup applied: extracted canonicalFuncName(*ast.FuncDecl) helper
shared between extractDecls and bodyCallsByFunc. AddCommand-skip lifted
to a package-level driftSkipSelectors set so future selector skips
don't require touching the AST walker.

Verification:
- go test ./... — 2625 pass (4 new)
- go vet, golangci-lint clean
- Human report renders body_drift per function with the lost call list

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files touched

Diff

commit abde446ebe376def6f2380d9c400a2132da3e07b
Author: Trevin Chow <trevin@trevinchow.com>
Date:   Fri May 1 15:49:52 2026 -0700

    feat(regenmerge): add TEMPLATED-BODY-DRIFT verdict to catch in-place body edits (#468)
    
    Adds a 6th classification verdict that catches the failure mode the
    original plan acknowledged as a known limitation: pub modifies the body
    of an existing templated function in-place (calls a hand-written helper,
    adds a header-injection loop, etc.) without changing the function's
    top-level decl name. Decl-set comparison says TEMPLATED-CLEAN, take-fresh
    overwrites the file, the integration is silently destroyed.
    
    Surfaced by sweep dogfood:
    - cal-com (#175): pub's client.go do() called requiredHeadersForPath()
      inside the templated function, integrating with calcom_versions.go's
      per-endpoint API-version map. Take-fresh dropped the call; v2 endpoints
      silently degraded.
    - pagliacci-pizza (#174): pub's client.go New() chained
      surf.Builder().Impersonate().Chrome() for cloud-bypass HTTP. Take-fresh
      reverted to the stdlib http.Client. Currently works against pagliacci
      but the integration was hand-written for a reason.
    
    Mechanism: per file classified as TEMPLATED-CLEAN by the existing
    decl-set check, parse pub and fresh function bodies and collect the
    call-target identifier set per function. Any pub function whose body
    calls identifiers fresh's same-named function doesn't is flagged as
    TEMPLATED-BODY-DRIFT and surfaced for human review instead of auto-
    overwritten.
    
    Conservative by design — fires only when both files define a function
    with the same canonical name. AddCommand args skip recursion (handled
    separately by the lost-registration restoration path).
    
    Tests: 3 regression cases pinning the cal-com shape (helper call dropped),
    the pagliacci shape (chained .Method().Method() drop), the
    AddCommand-args false-positive guard, and the no-drift baseline.
    
    Reuse cleanup applied: extracted canonicalFuncName(*ast.FuncDecl) helper
    shared between extractDecls and bodyCallsByFunc. AddCommand-skip lifted
    to a package-level driftSkipSelectors set so future selector skips
    don't require touching the AST walker.
    
    Verification:
    - go test ./... — 2625 pass (4 new)
    - go vet, golangci-lint clean
    - Human report renders body_drift per function with the lost call list
    
    Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---
 internal/cli/regen_merge.go                     |  11 +-
 internal/pipeline/regenmerge/body_drift.go      | 101 +++++++++++++
 internal/pipeline/regenmerge/body_drift_test.go | 193 ++++++++++++++++++++++++
 internal/pipeline/regenmerge/classify.go        |  62 +++++---
 internal/pipeline/regenmerge/regenmerge.go      |  28 +++-
 5 files changed, 369 insertions(+), 26 deletions(-)

diff --git a/internal/cli/regen_merge.go b/internal/cli/regen_merge.go
index 508d2216..9f8666d7 100644
--- a/internal/cli/regen_merge.go
+++ b/internal/cli/regen_merge.go
@@ -113,6 +113,7 @@ func printHumanRegenReport(w io.Writer, report *regenmerge.MergeReport, applied
 		regenmerge.VerdictPublishedOnlyTemplated,
 		regenmerge.VerdictNovel,
 		regenmerge.VerdictTemplatedWithAdditions,
+		regenmerge.VerdictTemplatedBodyDrift,
 		regenmerge.VerdictNovelCollision,
 	} {
 		fmt.Fprintf(w, "  %-26s %d\n", v, counts[v])
@@ -122,7 +123,10 @@ func printHumanRegenReport(w io.Writer, report *regenmerge.MergeReport, applied
 	// Files needing human review.
 	var needsReview []regenmerge.FileClassification
 	for _, fc := range report.Files {
-		if fc.Verdict == regenmerge.VerdictTemplatedWithAdditions || fc.Verdict == regenmerge.VerdictNovelCollision {
+		switch fc.Verdict {
+		case regenmerge.VerdictTemplatedWithAdditions,
+			regenmerge.VerdictTemplatedBodyDrift,
+			regenmerge.VerdictNovelCollision:
 			needsReview = append(needsReview, fc)
 		}
 	}
@@ -138,6 +142,11 @@ func printHumanRegenReport(w io.Writer, report *regenmerge.MergeReport, applied
 					fmt.Fprintf(w, "    in_fresh_not_published: %v\n", fc.DeclSetDelta.InFreshNotPublished)
 				}
 			}
+			if fc.BodyDrift != nil {
+				for fn, calls := range fc.BodyDrift.Functions {
+					fmt.Fprintf(w, "    body_drift in %s: %v\n", fn, calls)
+				}
+			}
 		}
 		fmt.Fprintln(w)
 	}
diff --git a/internal/pipeline/regenmerge/body_drift.go b/internal/pipeline/regenmerge/body_drift.go
new file mode 100644
index 00000000..9e0656c9
--- /dev/null
+++ b/internal/pipeline/regenmerge/body_drift.go
@@ -0,0 +1,101 @@
+package regenmerge
+
+import (
+	"go/ast"
+	"go/parser"
+	"go/token"
+	"sort"
+)
+
+// detectBodyDrift parses pub and fresh files, walks each function's body,
+// and returns the per-function set of call-target identifiers pub uses but
+// fresh's same-named function doesn't. Returns nil if there's no drift.
+// Conservative — fires only when both files define a function with the
+// same canonical name.
+func detectBodyDrift(pubPath, freshPath string) *BodyDrift {
+	pubFns := bodyCallsByFunc(pubPath)
+	if pubFns == nil {
+		return nil
+	}
+	freshFns := bodyCallsByFunc(freshPath)
+	if freshFns == nil {
+		return nil
+	}
+
+	driftFns := map[string][]string{}
+	for fn, pubCalls := range pubFns {
+		freshCalls, ok := freshFns[fn]
+		if !ok {
+			// Function exists only in pub. The decl-set check would have
+			// flagged that already (TEMPLATED-WITH-ADDITIONS), so we
+			// shouldn't be here. Skip.
+			continue
+		}
+		var diff []string
+		for call := range pubCalls {
+			if _, present := freshCalls[call]; !present {
+				diff = append(diff, call)
+			}
+		}
+		if len(diff) > 0 {
+			sort.Strings(diff)
+			driftFns[fn] = diff
+		}
+	}
+	if len(driftFns) == 0 {
+		return nil
+	}
+	return &BodyDrift{Functions: driftFns}
+}
+
+// driftSkipSelectors lists method-name selectors whose argument expressions
+// the body-drift walker should NOT recurse into. AddCommand is handled by
+// the lost-registration restoration path — re-flagging its arg calls here
+// would duplicate that signal and trip false positives on every CLI with
+// novel commands.
+var driftSkipSelectors = map[string]struct{}{
+	"AddCommand": {},
+}
+
+// bodyCallsByFunc parses filename and returns a map from canonical function
+// name to the set of call-target identifiers in that function's body. For
+// `foo()` the target is "foo"; for `pkg.Bar()` or `obj.Bar()` it's "Bar"
+// (the selector name — receivers and import aliases differ across files
+// but the function identity is the load-bearing signal). Returns nil if
+// the file fails to parse.
+func bodyCallsByFunc(filename string) map[string]map[string]struct{} {
+	fset := token.NewFileSet()
+	file, err := parser.ParseFile(fset, filename, nil, parser.SkipObjectResolution)
+	if err != nil {
+		return nil
+	}
+	out := make(map[string]map[string]struct{}, len(file.Decls))
+	for _, d := range file.Decls {
+		fn, ok := d.(*ast.FuncDecl)
+		if !ok || fn.Body == nil {
+			continue
+		}
+		name := canonicalFuncName(fn)
+		calls := map[string]struct{}{}
+		ast.Inspect(fn.Body, func(n ast.Node) bool {
+			ce, ok := n.(*ast.CallExpr)
+			if !ok {
+				return true
+			}
+			switch fnExpr := ce.Fun.(type) {
+			case *ast.Ident:
+				calls[fnExpr.Name] = struct{}{}
+			case *ast.SelectorExpr:
+				if fnExpr.Sel != nil {
+					calls[fnExpr.Sel.Name] = struct{}{}
+					if _, skip := driftSkipSelectors[fnExpr.Sel.Name]; skip {
+						return false
+					}
+				}
+			}
+			return true
+		})
+		out[name] = calls
+	}
+	return out
+}
diff --git a/internal/pipeline/regenmerge/body_drift_test.go b/internal/pipeline/regenmerge/body_drift_test.go
new file mode 100644
index 00000000..2874e6ff
--- /dev/null
+++ b/internal/pipeline/regenmerge/body_drift_test.go
@@ -0,0 +1,193 @@
+package regenmerge
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+// TestBodyDriftCatchesCalComShape pins the cal-com regression that motivated
+// this probe. Pub's client.go calls a hand-written helper
+// (requiredHeadersForPath) inside the templated do() function. Decl-set
+// comparison says TEMPLATED-CLEAN because both files have the same set of
+// top-level decls. Without the body-drift probe, take-fresh-wholesale would
+// silently drop the helper integration and break per-endpoint API
+// versioning at runtime.
+func TestBodyDriftCatchesCalComShape(t *testing.T) {
+	t.Parallel()
+
+	pubCLI := `package client
+
+func do(path string) {
+	authHeader := getAuth()
+	setHeader("Authorization", authHeader)
+	for k, v := range requiredHeadersForPath(path) {
+		setHeader(k, v)
+	}
+}
+
+func getAuth() string  { return "" }
+func setHeader(k, v string) {}
+`
+	freshCLI := `package client
+
+func do(path string) {
+	authHeader := getAuth()
+	setHeader("Authorization", authHeader)
+}
+
+func getAuth() string  { return "" }
+func setHeader(k, v string) {}
+`
+	pubDir, freshDir := buildSyntheticFixture(t,
+		map[string]string{"internal/client/client.go": pubCLI},
+		map[string]string{"internal/client/client.go": freshCLI})
+
+	report, err := Classify(pubDir, freshDir, Options{Force: true})
+	require.NoError(t, err)
+
+	verdicts := verdictMap(report)
+	assert.Equal(t, VerdictTemplatedBodyDrift, verdicts["internal/client/client.go"],
+		"pub's body calls a helper fresh's body doesn't — must flag drift")
+
+	var fc *FileClassification
+	for i := range report.Files {
+		if report.Files[i].Path == "internal/client/client.go" {
+			fc = &report.Files[i]
+			break
+		}
+	}
+	require.NotNil(t, fc.BodyDrift)
+	assert.Contains(t, fc.BodyDrift.Functions, "do",
+		"do() is the function with drift")
+	assert.Contains(t, fc.BodyDrift.Functions["do"], "requiredHeadersForPath",
+		"requiredHeadersForPath is the dropped call target")
+}
+
+// TestBodyDriftIgnoresAddCommandArgs pins the false-positive guard: the
+// lost-registration mechanism handles pub-only AddCommand calls separately,
+// so body-drift must not re-flag them. Without this filter, every CLI with
+// novel commands would trip body-drift (pub's Execute calls
+// newFooCmd/newBarCmd that fresh's Execute doesn't).
+func TestBodyDriftIgnoresAddCommandArgs(t *testing.T) {
+	t.Parallel()
+
+	pubCLI := `package cli
+
+func Execute() {
+	rootCmd := newRoot()
+	rootCmd.AddCommand(newFooCmd())
+	rootCmd.AddCommand(newBarCmd())
+}
+
+func newRoot() *struct{}     { return nil }
+func newFooCmd() *struct{}   { return nil }
+func newBarCmd() *struct{}   { return nil }
+`
+	freshCLI := `package cli
+
+func Execute() {
+	rootCmd := newRoot()
+}
+
+func newRoot() *struct{}     { return nil }
+`
+	pubDir, freshDir := buildSyntheticFixture(t,
+		map[string]string{"internal/cli/root.go": pubCLI},
+		map[string]string{"internal/cli/root.go": freshCLI})
+
+	report, err := Classify(pubDir, freshDir, Options{Force: true})
+	require.NoError(t, err)
+
+	// Decl-set comparison flags this as TEMPLATED-WITH-ADDITIONS because
+	// pub has newFooCmd / newBarCmd that fresh doesn't. The drift probe
+	// shouldn't re-flag the AddCommand args separately. (The decl-set
+	// mismatch is the load-bearing signal here, not body drift.)
+	verdicts := verdictMap(report)
+	got := verdicts["internal/cli/root.go"]
+	assert.NotEqual(t, VerdictTemplatedBodyDrift, got,
+		"AddCommand argument calls must not trigger body drift; lost-registration handles them")
+}
+
+// TestBodyDriftCatchesPagliacciShape pins the second regression that
+// motivated this probe. Pagliacci-pizza's pub client.go used a Chrome-
+// impersonating HTTP client (surf library) inside New() — without it the
+// CLI risks Cloudflare blocks. Decl-set comparison says TEMPLATED-CLEAN;
+// take-fresh-wholesale dropped the integration silently.
+func TestBodyDriftCatchesPagliacciShape(t *testing.T) {
+	t.Parallel()
+
+	pubCLI := `package client
+
+func New() *Client {
+	builder := surf.Builder().Impersonate().Chrome()
+	return &Client{client: builder.Build()}
+}
+
+type Client struct {
+	client interface{}
+}
+`
+	freshCLI := `package client
+
+func New() *Client {
+	return &Client{client: defaultHTTPClient()}
+}
+
+func defaultHTTPClient() interface{} { return nil }
+
+type Client struct {
+	client interface{}
+}
+`
+	pubDir, freshDir := buildSyntheticFixture(t,
+		map[string]string{"internal/client/client.go": pubCLI},
+		map[string]string{"internal/client/client.go": freshCLI})
+
+	report, err := Classify(pubDir, freshDir, Options{Force: true})
+	require.NoError(t, err)
+
+	verdicts := verdictMap(report)
+	assert.Equal(t, VerdictTemplatedBodyDrift, verdicts["internal/client/client.go"],
+		"chained .Impersonate().Chrome().Build() in pub's New() must flag drift")
+
+	var fc *FileClassification
+	for i := range report.Files {
+		if report.Files[i].Path == "internal/client/client.go" {
+			fc = &report.Files[i]
+			break
+		}
+	}
+	require.NotNil(t, fc.BodyDrift)
+	driftCalls := fc.BodyDrift.Functions["New"]
+	for _, want := range []string{"Builder", "Impersonate", "Chrome"} {
+		assert.Contains(t, driftCalls, want,
+			"surf-library call %q must surface in drift report", want)
+	}
+}
+
+// TestBodyDriftClean verifies the no-drift case: same function bodies in
+// both files (modulo comments and whitespace) classify as TEMPLATED-CLEAN.
+func TestBodyDriftClean(t *testing.T) {
+	t.Parallel()
+
+	src := `package cli
+
+func helper(x int) int {
+	return doStuff(x) + 1
+}
+
+func doStuff(x int) int { return x * 2 }
+`
+	pubDir, freshDir := buildSyntheticFixture(t,
+		map[string]string{"internal/cli/helpers.go": src},
+		map[string]string{"internal/cli/helpers.go": src})
+
+	report, err := Classify(pubDir, freshDir, Options{Force: true})
+	require.NoError(t, err)
+
+	verdicts := verdictMap(report)
+	assert.Equal(t, VerdictTemplatedClean, verdicts["internal/cli/helpers.go"],
+		"identical bodies must stay TEMPLATED-CLEAN")
+}
diff --git a/internal/pipeline/regenmerge/classify.go b/internal/pipeline/regenmerge/classify.go
index 7d963a32..3fecc105 100644
--- a/internal/pipeline/regenmerge/classify.go
+++ b/internal/pipeline/regenmerge/classify.go
@@ -62,14 +62,7 @@ func extractDecls(filename string) (declSet, error) {
 	for _, d := range file.Decls {
 		switch decl := d.(type) {
 		case *ast.FuncDecl:
-			name := decl.Name.Name
-			if decl.Recv != nil && len(decl.Recv.List) > 0 {
-				recv := receiverTypeName(decl.Recv.List[0].Type)
-				if recv != "" {
-					name = "(" + recv + ")." + name
-				}
-			}
-			decls.add(name)
+			decls.add(canonicalFuncName(decl))
 		case *ast.GenDecl:
 			for _, spec := range decl.Specs {
 				switch s := spec.(type) {
@@ -114,6 +107,19 @@ func receiverTypeName(expr ast.Expr) string {
 	return ""
 }
 
+// canonicalFuncName returns the qualified name used as the dedup key for
+// function/method decls: bare `Name` for top-level funcs, `(*Type).Name`
+// or `(Type).Name` for methods.
+func canonicalFuncName(fn *ast.FuncDecl) string {
+	name := fn.Name.Name
+	if fn.Recv != nil && len(fn.Recv.List) > 0 {
+		if recv := receiverTypeName(fn.Recv.List[0].Type); recv != "" {
+			name = "(" + recv + ")." + name
+		}
+	}
+	return name
+}
+
 // hasTemplatedMarker reports whether one of the file's first few lines
 // contains the "Generated by CLI Printing Press" marker. Scans the head of
 // the file rather than a fixed byte slab so a long line-1 SPDX/license
@@ -219,7 +225,7 @@ func classifyFiles(publishedDir, freshDir string) ([]FileClassification, error)
 			freshPath := filepath.Join(freshDir, rel)
 			pubMarker := hasTemplatedMarker(pubPath)
 			freshMarker := hasTemplatedMarker(freshPath)
-			fc = decideBothPresent(rel, pubDecls, freshDecls, pubMarker, freshMarker, freshGlobalDecls)
+			fc = decideBothPresent(rel, pubPath, freshPath, pubDecls, freshDecls, pubMarker, freshMarker, freshGlobalDecls)
 		}
 		out = append(out, fc)
 	}
@@ -236,7 +242,7 @@ func stringSet(s []string) map[string]struct{} {
 
 // decideBothPresent runs the in-both branch of the classification decision
 // tree.
-func decideBothPresent(rel string, pub, fresh declSet, pubMarker, freshMarker bool, freshGlobal declSet) FileClassification {
+func decideBothPresent(rel, pubPath, freshPath string, pub, fresh declSet, pubMarker, freshMarker bool, freshGlobal declSet) FileClassification {
 	fc := FileClassification{Path: rel}
 
 	pubExtras := pub.minus(fresh)
@@ -244,21 +250,31 @@ func decideBothPresent(rel string, pub, fresh declSet, pubMarker, freshMarker bo
 	templated := pubMarker || freshMarker || isStrictSubset(pub, fresh)
 
 	if templated {
-		if len(pubExtras) == 0 {
-			fc.Verdict = VerdictTemplatedClean
-			return fc
-		}
-		// Cross-file decl search: are all "extras" in fresh's global decl
-		// set? If so, the generator moved them to a different file —
-		// safe to overwrite.
-		allMoved := true
-		for _, name := range pubExtras {
-			if _, ok := freshGlobal[name]; !ok {
-				allMoved = false
-				break
+		cleanByDeclSet := len(pubExtras) == 0
+		if !cleanByDeclSet {
+			// Cross-file decl search: are all "extras" in fresh's global
+			// decl set? If so, the generator moved them to a different
+			// file — decl-set is clean.
+			allMoved := true
+			for _, name := range pubExtras {
+				if _, ok := freshGlobal[name]; !ok {
+					allMoved = false
+					break
+				}
 			}
+			cleanByDeclSet = allMoved
 		}
-		if allMoved {
+		if cleanByDeclSet {
+			// Decl-set looks clean. One more check: do pub's function
+			// bodies call identifiers fresh's same-function bodies don't?
+			// Catches in-place body modifications that decl-set comparison
+			// misses (e.g., pub adds a call to a hand-written helper
+			// inside an existing templated function).
+			if drift := detectBodyDrift(pubPath, freshPath); drift != nil {
+				fc.Verdict = VerdictTemplatedBodyDrift
+				fc.BodyDrift = drift
+				return fc
+			}
 			fc.Verdict = VerdictTemplatedClean
 			return fc
 		}
diff --git a/internal/pipeline/regenmerge/regenmerge.go b/internal/pipeline/regenmerge/regenmerge.go
index dd7a0ab9..fa030dbb 100644
--- a/internal/pipeline/regenmerge/regenmerge.go
+++ b/internal/pipeline/regenmerge/regenmerge.go
@@ -23,8 +23,8 @@ const (
 	VerdictNewTemplateEmission Verdict = "NEW-TEMPLATE-EMISSION"
 
 	// VerdictTemplatedClean marks a file present in both trees where
-	// published's decl-set is a subset of fresh's. Safe to overwrite with
-	// fresh.
+	// published's decl-set is a subset of fresh's AND function bodies
+	// match. Safe to overwrite with fresh.
 	VerdictTemplatedClean Verdict = "TEMPLATED-CLEAN"
 
 	// VerdictTemplatedWithAdditions marks a file present in both trees where
@@ -33,6 +33,14 @@ const (
 	// overwrite — surface for human review.
 	VerdictTemplatedWithAdditions Verdict = "TEMPLATED-WITH-ADDITIONS"
 
+	// VerdictTemplatedBodyDrift marks a file whose decl-set matches
+	// fresh's but whose function bodies call identifiers fresh's same-named
+	// bodies don't. Catches in-place body modifications the decl-set
+	// comparison can't see — e.g., pub's client.go adds a call to a
+	// hand-written helper inside an existing templated function. Preserve
+	// published; surface for human review.
+	VerdictTemplatedBodyDrift Verdict = "TEMPLATED-BODY-DRIFT"
+
 	// VerdictPublishedOnlyTemplated marks a file present only in published
 	// that carries the templated marker. Fresh dropped emitting it. Stale
 	// template emission; user can decide to delete.
@@ -62,6 +70,22 @@ type FileClassification struct {
 	// NOVEL-COLLISION verdicts. Lists the top-level declaration names that
 	// differ between published and fresh.
 	DeclSetDelta *DeclSetDelta `json:"decl_set_delta,omitempty"`
+
+	// BodyDrift is populated for TEMPLATED-BODY-DRIFT verdict. Lists
+	// per-function the call-target identifiers pub references that don't
+	// appear anywhere in fresh's tree.
+	BodyDrift *BodyDrift `json:"body_drift,omitempty"`
+}
+
+// BodyDrift records function-body call-target differences between published
+// and fresh for a templated file with matching decl-sets. Each entry names
+// a function that exists in both files but whose published-side body
+// references identifiers fresh's tree doesn't have — a load-bearing signal
+// that pub hand-edited the body to integrate with custom helpers.
+type BodyDrift struct {
+	// Functions maps the qualified function name (bare or "(*Type).Method")
+	// to the list of pub-only call-target identifiers in its body.
+	Functions map[string][]string `json:"functions,omitempty"`
 }
 
 // DeclSetDelta names the declarations that differ between published and fresh

← 08b88654 chore(main): release 3.2.1 (#466)  ·  back to Cli Printing Press  ·  feat(generator): owner precedence chain — preserve attributi d1a8a467 →