Skip to content

feat(dead): Go language voice earns the dead verdict#121

Merged
luuuc merged 4 commits into
mainfrom
feat/dead-code-go-voice
Jun 2, 2026
Merged

feat(dead): Go language voice earns the dead verdict#121
luuuc merged 4 commits into
mainfrom
feat/dead-code-go-voice

Conversation

@luuuc
Copy link
Copy Markdown
Owner

@luuuc luuuc commented Jun 2, 2026

Problem

Every Go symbol Sense reported was possibly_dead with reason core_no_language_voice — the honest default for any stack with no registered voice. That surrenders the one language where a dead verdict can be made airtight: Go is statically typed and compiled, its visibility is structural (capitalized = exported), and the toolchain already ships an oracle for exactly this class (staticcheck's U1000, unused unexported symbols). An agent asking "what is safe to delete in this Go package?" deserved a precise answer, not a wall of possibly_dead.

Summary

Add a Go language voice so Go becomes the second language whose symbols can earn the dead verdict, with the verdict validated against staticcheck U1000 as a compiler-grade oracle.

Changes

Harvest seam (internal/extract, internal/scan)

  • The Go extractor harvests, in a single tree walk per file, its broad mention set (the per-language soundness gate), reflective dispatch targets (reflect.MethodByName / FieldByName args, tagged struct-field names), and cgo //export names.
  • New extract.CgoExportEmitter; the scan persists //export names to a flat cgo_exports meta key.
  • Blank declarations (const _, var _) are no longer emitted as symbols (a blank is never an unused-symbol finding).

Go voice (internal/dead)

  • goVoice earns dead only for an unexported func/method/type with no caller, no mention, and no invisible-reach idiom. Everything else stays possibly_dead with an exact reason: go_init, go_interface (indexed + stdlib interface methods), go_cgo, go_generated, go_exported, go_const.
  • Facts.InterfaceMethodNames and Facts.CgoExportNames are populated; the shared core_reflection hint is now language-neutral.
  • init/Init are no longer treated as constructors — the voice owns init (go_init) so it surfaces with an accurate "runtime-invoked" hint.
  • testdata/ fixtures are excluded from candidacy, matching the Go toolchain's universe so fixtures are never reported as removable.

staticcheck oracle (internal/dead/eval)

  • A runner that scans a repo, runs staticcheck -checks U1000, and asserts Sense's dead set is a subset (zero false dead). Matches on (file, bare-name) to survive line/column drift; injects its command runner so all exit shapes test without the binary installed.

Breaking Changes

⚠️ Dead-code output now reports Go symbols with Go-specific reasons and can mark Go symbols dead. Previously every Go symbol was possibly_dead / core_no_language_voice. No wire-schema change.

Test Plan

  • Two-sided gate proven end to end (golden): an unexported helper earns dead; an init and an interface method stay possibly_dead with exact reasons.
  • Per-reason unit tests for every go_* code; planted false-dead is detected by the oracle gate.
  • Binding gate on real repos (gin, sense): zero false dead, validated against staticcheck U1000 (sense: 2 dead, 100% recall; gin: 0).
  • go test ./... passes; make ci green (0 lint issues).
  • sense binary builds cleanly.

luuuc added 4 commits June 2, 2026 09:52
Add the scan-time harvest seam the Go dead-code voice stands on. The Go
extractor now streams its own broad mention set (the per-language soundness
gate), reflective dispatch targets (reflect.MethodByName / FieldByName string
args and tagged struct-field names), and cgo //export function names, all in a
single tree walk per file. A new extract.CgoExportEmitter plus the scan
collector/harness persist //export names to a flat cgo_exports sense_meta key.

- golang: HarvestsMentions() opts the language into harvested_langs, so its
  symbols stop failing closed at the per-language soundness gate
- golang: blank const/var declarations (`const _`, `var _`) are no longer
  emitted as symbols, so a blank is never a dead candidate
- scan: cgo_exports written per full scan, unioned like the other name sets
Go becomes the second language whose symbols can earn `dead`. The goVoice earns
`dead` only for an unexported func/method/type with no caller, no mention, and no
invisible-reach idiom; everything else stays possibly_dead with an exact reason:
go_init, go_interface (indexed or stdlib interface methods), go_cgo, go_generated,
go_exported (staticcheck U1000 flags only unexported, so exported stays open), and
go_const (an iota anchor must not be cut). Soundness is structural: Go visibility
is capitalized-means-exported, already recorded on every symbol.

- dead: register goVoice; populate Facts.InterfaceMethodNames and
  Facts.CgoExportNames; the core_reflection hint is now language-neutral
- dead: init/Init are no longer treated as constructors — the Go voice owns init
  (go_init) so it surfaces with an accurate "runtime-invoked" hint
- dead: testdata/ fixtures are excluded from candidacy, matching the Go
  toolchain's universe so fixtures are never reported as removable
- smoke fixture + golden prove the two-sided gate end to end (a dead helper
  earns dead; an init and an interface method stay possibly_dead)
Add a compiler-grade trust gate for the Go voice: scan a repo with Sense, run
`staticcheck -checks U1000`, and assert Sense's `dead` set is a subset (zero
false dead). staticcheck knows the full type system and build tags Sense lacks,
so the binding invariant is Sense `dead` (Go) ⊆ U1000, not equality. The runner
matches on (file, bare-name) to survive line/column drift between the tools and
injects its command runner so all exit shapes test deterministically without the
binary installed; the live end-to-end check skips cleanly when it is absent.
Record the Go dead-code voice under Unreleased: the earned-dead candidate, every
possibly_dead reason, the harvest seam, the staticcheck U1000 oracle, and the
testdata/ exclusion. Update the existing dead-code breaking-change note from
"only Ruby ships a voice" to "Ruby and Go".
@luuuc luuuc self-assigned this Jun 2, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Jun 2, 2026

Codecov Report

❌ Patch coverage is 94.38596% with 16 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/dead/dead.go 82.35% 3 Missing and 3 partials ⚠️
internal/dead/eval/staticcheck.go 90.00% 3 Missing and 3 partials ⚠️
internal/extract/golang/harvest.go 96.07% 2 Missing and 2 partials ⚠️

📢 Thoughts on this report? Let us know!

@luuuc luuuc merged commit 18e4370 into main Jun 2, 2026
6 checks passed
@luuuc luuuc deleted the feat/dead-code-go-voice branch June 2, 2026 10:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant