commitdigest

package
v0.0.19 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 28, 2026 License: MIT Imports: 25 Imported by: 0

Documentation

Index

Constants

View Source
const DefaultSystemPrompt = `You are a changelog curator for a technical software project. You write for engineers familiar with the codebase — assume domain vocabulary is known. Do not re-explain packages, frameworks, or acronyms.

Inputs: the current chunk's commits (subjects, bodies, stats), computed metrics (hotspots, reverts, optional follow-up signals, optional boundary crossings), and — when present — prior-window summaries.

Use commit bodies for motivation. Do not paraphrase subjects verbatim.

Title: "# Changelog" followed on the next line by an italic date range derived from commit dates, formatted as *YYYY-MM-DD – YYYY-MM-DD* (a single date if the range collapses to one day).

After the title, write the sections below in order. Emit only sections that have material; omit empty sections silently. Do not write placeholder "None detected" blocks.

Narrative (no heading) — open with 2–4 paragraphs grouping commits by theme. State what changed and the constraint or motivation behind it. Cite short hashes inline in parentheses using the first 8 characters, e.g. (a1b2c3d4). Use verbs of fact. Do not use adjectives of scale like "comprehensive", "extensive", "major", "robust", "seamless", "significant".

### Decisions & Deletions — bullets for durable architectural events: ADR transitions, package/file removals, reverts (from metrics), public API breaks, new top-level modules, dependency bumps. Cite hashes.

### Hotspots — only files with 3 or more commits this chunk. One line per file naming the reason for churn (iteration on an in-flight spec, bug-fix cycle, refactor sweep). If multiple non-generated hotspots share the same reason, consolidate them into a single bullet listing the paths instead of repeating the reason. Collapse all hotspots tagged [generated] in the metrics into one trailing bullet of the form: "generated artifacts regenerated (N files)" — do not enumerate their paths.

### Follow-ups & Risks — surface the individual WIP/fixup/squash/hotfix commits provided under "Follow-up signals" in the metrics, along with reverts, unresolved TODOs, or short-lived code visible in commit bodies. Never invent an aggregate ratio ("N of M commits are WIP"); cite only the specific commits the metrics feed names. If no follow-up signals appear in the metrics, the section has no material — skip it entirely.

### Continuity — exactly one line, only when prior summaries are supplied: what thread this chunk closes, continues, or reverts.

Rules:
- Hard cap: 250 words excluding hash citations.
- Fold any boundary-crossing flags into the relevant Narrative paragraph; do not create a dedicated section for them.
- Never echo commit subjects verbatim — paraphrase into prose.
- If a Thread Registry is supplied in the system context, every themed claim in the Narrative must cite a registered thread id in backticks (e.g. ` + "`fffi2-codegen`" + `). Never invent thread ids that are not in the registry. If this chunk's activity genuinely falls outside every registered thread, say so plainly ("outside registered threads") rather than fabricating a new one. The Continuity line is the natural home for thread citations when multiple threads are advanced.
`
View Source
const FlatCommitChangeStructure = "repoName String, chunkIndex Int32, " +
	"commitHash String, commitShortHash String, commitDate String, " +
	"commitAuthor String, commitAuthorEmail String, commitSubject String, " +
	"subjectType String, path String, adds Int32, dels Int32"

FlatCommitChangeStructure is the ClickHouse --structure argument matching the JSON tags on FlatCommitChange. Kept here so mine-trends stays in sync with the struct.

View Source
const ThreadSynthesisSystemPrompt = `` /* 1713-byte string literal not displayed */

Variables

View Source
var ErrCursorHashNotFound = errors.New("cursor hash not found in repository")
View Source
var ErrNoCommits = errors.New("git repository has no commits")
View Source
var ErrNotAGitRepo = errors.New("not a git repository")
View Source
var PackageProps = packageprops.Props{
	WASMWASI:         packageprops.WASMBlocked,
	WASMJS:           packageprops.WASMBlocked,
	WASMFreestanding: packageprops.WASMBlocked,
}

PackageProps records this package's curated properties (ADR-0080). Seeded by `wasmsurvey props generate`; curate by hand, then `wasmsurvey props verify`.

Functions

func BlameOwners

func BlameOwners(ctx context.Context, repoDir string, filePath string, coAuthorCache map[string][]string) (owners []string, err error)

BlameOwners returns the set of author and co-author emails for a file, derived from the current git blame snapshot.

func ExtractSubjectType

func ExtractSubjectType(subject string) (kind string)

ExtractSubjectType returns a normalized kind for a commit subject. Fixup / squash / revert prefixes and WIP-as-word take precedence over Conventional Commits `type:` extraction because they are stronger signals of intent. Returns "other" when nothing matches.

func NewCliCommand

func NewCliCommand() *cli.Command

func RenderChunkPrompt

func RenderChunkPrompt(repoName string, commits []CommitEntry, metrics DigestMetrics, windowContext string, systemPrompt string, threadRegistry string) (system string, user string)

func RenderCommitEntry

func RenderCommitEntry(commit CommitEntry) (rendered string)

func RenderMetricsSection

func RenderMetricsSection(metrics DigestMetrics) (rendered string)

func RenderRepoHeader

func RenderRepoHeader(repoName string) (rendered string)

func RenderThreadRegistry

func RenderThreadRegistry(threads []Thread) (rendered string)

RenderThreadRegistry formats a thread registry for injection into the system prompt as a bulleted markdown block. One line per thread, anchored on the id the LLM is expected to cite verbatim.

func SaveCursors

func SaveCursors(dir string, cursors CursorMap) (err error)

SaveCursors writes cursors.json atomically via temp file + rename so a crashed write never leaves a partial file (resume state would be corrupted otherwise).

func ValidateCursorHash

func ValidateCursorHash(ctx context.Context, repoDir string, hash string) (err error)

ValidateCursorHash confirms the hash still resolves to a commit in repoDir. Returns ErrCursorHashNotFound if git rev-parse fails (history was rewritten).

func WriteDigest

func WriteDigest(w io.Writer, digests []RepoDigest) (err error)

func WriteFlattenedJSONEachRow

func WriteFlattenedJSONEachRow(w io.Writer, repos []RepoChunks) (err error)

WriteFlattenedJSONEachRow streams one JSON object per line to w. Preferred for large inputs — avoids materialising the full flat slice in memory.

Types

type BoundaryCrossing

type BoundaryCrossing struct {
	File       string   `json:"file"`
	CommitHash string   `json:"commitHash"`
	Author     string   `json:"author"`
	Owners     []string `json:"owners"`
}

func DetectBoundaryCrossings

func DetectBoundaryCrossings(ctx context.Context, repoDir string, commits []CommitEntry) (crossings []BoundaryCrossing, err error)

DetectBoundaryCrossings identifies commits that modify files whose blame authors (and co-authors) do not include the commit author.

type Chunk

type Chunk struct {
	Index      int32
	Commits    []CommitEntry
	TokenCount int64
	Metrics    DigestMetrics
}

type ChunkResult

type ChunkResult struct {
	Index      int32         `json:"index"`
	TokenCount int64         `json:"tokenCount"`
	Summary    string        `json:"summary,omitempty"`
	Metrics    DigestMetrics `json:"metrics"`
	Commits    []CommitEntry `json:"commits"`
}

type CommitCursor

type CommitCursor struct {
	LastCommitHash string `json:"lastCommitHash"`
	LastCommitDate string `json:"lastCommitDate"`
	LastChunkIndex int32  `json:"lastChunkIndex"`
	UpdatedAt      string `json:"updatedAt"`
}

func NewCursorForChunk

func NewCursorForChunk(chunkIndex int32, commits []CommitEntry) (cursor CommitCursor, ok bool)

NewCursorForChunk builds a cursor pointing at the newest commit in chunk. Commits are assumed oldest-first (as produced by git log --reverse in CollectDigest). Returns ok=false for empty chunks.

type CommitEntry

type CommitEntry struct {
	Hash        string      `json:"hash"`
	Author      string      `json:"author"`
	Date        string      `json:"date"`
	Subject     string      `json:"subject"`
	Body        string      `json:"body,omitempty"`
	Stat        string      `json:"stat,omitempty"`
	StatEntries []StatEntry `json:"statEntries,omitempty"`
}

type CursorMap

type CursorMap map[string]CommitCursor

func LoadCursors

func LoadCursors(dir string) (cursors CursorMap, err error)

LoadCursors reads cursors.json from dir. Returns an empty map if dir is empty or the file does not exist.

type DigestChunker

type DigestChunker struct {
	TokenBudget    int64
	ReservedTokens int64
	Counter        TokenCounterI
}

func (*DigestChunker) ChunkAll

func (inst *DigestChunker) ChunkAll(digest RepoDigest, overheadTokens int64) (chunks []Chunk, err error)

ChunkAll splits all commits into chunks using a fixed overhead. Use ChunkNext for iterative chunking where overhead changes between chunks.

func (*DigestChunker) ChunkNext

func (inst *DigestChunker) ChunkNext(commits []CommitEntry, index int32, overheadTokens int64, repoName string) (chunk Chunk, consumed int)

ChunkNext returns the next chunk from the front of commits, given the current overhead (system prompt + window context + repo header tokens). Returns the chunk and how many commits were consumed.

type DigestMetrics

type DigestMetrics struct {
	BoundaryCrossings []BoundaryCrossing `json:"boundaryCrossings,omitempty"`
	IterationHotspots []IterationHotspot `json:"iterationHotspots,omitempty"`
	Reverts           []RevertSignal     `json:"reverts,omitempty"`
	FollowUps         []FollowUpSignal   `json:"followUps,omitempty"`
	TotalCommits      int32              `json:"totalCommits"`
	UniqueAuthors     int32              `json:"uniqueAuthors"`
	// FollowUpIsAuthorBaseline is true when at least half the chunk's commits
	// carry a follow-up signal. In that regime WIP/fixup prefixes are the
	// author's routine commit cadence, not a risk signal — the renderer
	// suppresses the follow-up bullet so the summarizer does not report a
	// meaningless aggregate like "N of M commits are WIP".
	FollowUpIsAuthorBaseline bool `json:"followUpIsAuthorBaseline,omitempty"`
}

func ComputeMetrics

func ComputeMetrics(commits []CommitEntry, config MetricsConfig) (metrics DigestMetrics)

type FlatCommitChange

type FlatCommitChange struct {
	RepoName          string `json:"repoName"`
	ChunkIndex        int32  `json:"chunkIndex"`
	CommitHash        string `json:"commitHash"`
	CommitShortHash   string `json:"commitShortHash"`
	CommitDate        string `json:"commitDate"`
	CommitAuthor      string `json:"commitAuthor"`
	CommitAuthorEmail string `json:"commitAuthorEmail"`
	CommitSubject     string `json:"commitSubject"`
	SubjectType       string `json:"subjectType"`
	Path              string `json:"path"`
	Adds              int32  `json:"adds"`
	Dels              int32  `json:"dels"`
}

FlatCommitChange is one row in the JSONEachRow facts table consumed by trend-mining SQL. One row per file-change per commit; commits that touched no files (empty merges, typo-only commits) emit a single row with Path=="" as a sentinel so commit-level aggregations (countDistinct(commitHash)) still see them. Trend-mining queries that want file-level joins should filter `path <> ”`.

func FlattenRepoChunks

func FlattenRepoChunks(repos []RepoChunks) (rows []FlatCommitChange)

FlattenRepoChunks materialises all file-changes across all chunks as a flat slice. Convenient for tests; production code should prefer the streaming WriteFlattenedJSONEachRow.

type FollowUpSignal

type FollowUpSignal struct {
	Hash    string `json:"hash"`
	Subject string `json:"subject"`
	Kind    string `json:"kind"`
}

FollowUpSignal flags a commit whose subject indicates unfinished work: wip/fixup/squash/hotfix language. Kind is one of "wip", "fixup", "squash", "hotfix". Surfaced so the summarizer can list them under Follow-ups & Risks.

type IterationHotspot

type IterationHotspot struct {
	Path        string `json:"path"`
	CommitCount int32  `json:"commitCount"`
	IsGenerated bool   `json:"isGenerated,omitempty"`
}

type MetricsConfig

type MetricsConfig struct {
	HotspotTopN int32
}

type RepoChunks

type RepoChunks struct {
	RepoName string        `json:"repoName"`
	RepoPath string        `json:"repoPath"`
	Chunks   []ChunkResult `json:"chunks"`
}

RepoChunks is the intermediate format emitted by extract and consumed by summarize.

type RepoDigest

type RepoDigest struct {
	RepoPath string
	RepoName string
	Commits  []CommitEntry
}

func CollectDigest

func CollectDigest(ctx context.Context, repoPath string, since string, author string, noStat bool, fromHash string) (digest RepoDigest, err error)

CollectDigest runs git log in repoPath. If fromHash is non-empty, commits after that hash up to HEAD are returned (since is ignored); this is how seamless resume works. Otherwise commits matching since/author are returned.

type RevertSignal

type RevertSignal struct {
	Hash    string `json:"hash"`
	Subject string `json:"subject"`
}

RevertSignal flags a commit whose subject indicates it reverts earlier work. Surfaced so the summarizer can list them under Decisions & Deletions.

type SlidingWindow

type SlidingWindow struct {
	MaxSummaries int32
	Summaries    []string
	Dir          string
	// contains filtered or unexported fields
}

func (*SlidingWindow) ContextTokenCount

func (inst *SlidingWindow) ContextTokenCount(counter TokenCounterI) (count int64)

func (*SlidingWindow) LoadFromDir

func (inst *SlidingWindow) LoadFromDir() (err error)

func (*SlidingWindow) Persist

func (inst *SlidingWindow) Persist(chunkIndex int32, commits []CommitEntry) (err error)

func (*SlidingWindow) Push

func (inst *SlidingWindow) Push(summary string)

func (*SlidingWindow) RenderContext

func (inst *SlidingWindow) RenderContext() (context string)

type StatEntry

type StatEntry struct {
	Path string `json:"path"`
	Adds int32  `json:"adds"`
	Dels int32  `json:"dels"`
}

StatEntry is the structured counterpart to the human-readable Stat string: per-file add/delete line counts from `git diff-tree --numstat`. Consumed by trend-mining SQL so queries do not need to parse the freeform Stat format. Adds/Dels are 0 for binary files (git reports "-" for those).

type Thread

type Thread struct {
	ID                  string     `json:"id"`
	Title               string     `json:"title"`
	Span                ThreadSpan `json:"span"`
	Summary             string     `json:"summary"`
	ComplexityDirection string     `json:"complexityDirection"`
	PathPrefixes        []string   `json:"pathPrefixes"`
	AnchorCommits       []string   `json:"anchorCommits"`
}

Thread is one durable narrative arc identified over a chunk window. The registry is written by synthesize-threads and consumed by summarize via --thread-registry; chunk summaries anchor their themes to these ids.

type ThreadSpan

type ThreadSpan struct {
	Start string `json:"start"`
	End   string `json:"end"`
}

type TiktokenCounter

type TiktokenCounter struct {
	Encoding             string
	CorrectionMultiplier float64
	// contains filtered or unexported fields
}

func (*TiktokenCounter) CountTokens

func (inst *TiktokenCounter) CountTokens(text string) (count int64)

func (*TiktokenCounter) Init

func (inst *TiktokenCounter) Init() (err error)

type TokenCounterI

type TokenCounterI interface {
	CountTokens(text string) (count int64)
}

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL