Skip to content

Comments

refactor: split internal packages by domain#58

Merged
WZ merged 1 commit intomainfrom
codex/reorg-phase2-domain-config
Feb 19, 2026
Merged

refactor: split internal packages by domain#58
WZ merged 1 commit intomainfrom
codex/reorg-phase2-domain-config

Conversation

@WZ
Copy link
Owner

@WZ WZ commented Feb 19, 2026

Summary

  • split internal/reportbot into domain-focused packages:
    • internal/app
    • internal/config
    • internal/domain
    • internal/storage/sqlite
    • internal/integrations/slack
    • internal/integrations/github
    • internal/integrations/gitlab
    • internal/integrations/llm
    • internal/report
    • internal/fetch
    • internal/nudge
    • internal/httpx
  • keep runtime behavior and test behavior unchanged while rewiring imports/types across packages
  • update docs that referenced old flat paths (README.md, CLAUDE.md, architecture docs)

Notes

  • this is intentionally a structural refactor only (no feature changes)
  • compatibility wrappers were added where needed to avoid behavioral drift during package extraction

Validation

  • /tmp/go1.23.6 test ./...

Copilot AI review requested due to automatic review settings February 19, 2026 10:05
@WZ WZ merged commit b578e41 into main Feb 19, 2026
5 checks passed
@WZ WZ deleted the codex/reorg-phase2-domain-config branch February 19, 2026 10:09
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR refactors the monolithic internal/reportbot package into 11 domain-focused packages to improve code organization and maintainability. The refactor uses a consistent deps.go pattern across packages to manage cross-package dependencies through type aliases and wrapper functions.

Changes:

  • Split internal/reportbot into domain-focused packages (app, config, domain, storage/sqlite, integrations/*, report, fetch, nudge, httpx)
  • Moved core domain types to internal/domain as single source of truth
  • Added deps.go files in each package with type aliases and function wrappers to maintain existing interfaces
  • Updated all documentation (README.md, CLAUDE.md, architecture docs) to reflect new package structure

Reviewed changes

Copilot reviewed 48 out of 48 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
internal/storage/sqlite/db.go Changed package to sqlite, added type aliases for domain types, removed duplicate type definitions
internal/storage/sqlite/db_test.go Changed package declaration to match
internal/report/*.go Changed package to report, added deps.go with wrappers for cross-package dependencies
internal/nudge/*.go Changed package to nudge, added deps.go with duplicate resolveUserIDs implementation
internal/integrations/slack/*.go Changed package to slackbot, added deps.go and test helpers
internal/integrations/github/*.go Changed package to github, added deps.go and unused buggy status.go
internal/integrations/gitlab/*.go Changed package to gitlab, added deps.go
internal/integrations/llm/*.go Changed package to llm, added deps.go, wrappers, and exported types
internal/fetch/*.go Changed package to fetch, added deps.go with duplicate helper functions
internal/httpx/*.go Changed package to httpx, exported ExternalHTTPClient
internal/domain/*.go New package containing all core domain types and business logic
internal/config/config.go Changed package to config, refactored glossary validation, duplicate parseClock
internal/app/app.go Changed package to app, updated imports to new package locations
cmd/reportbot/main.go Updated import path from internal/reportbot to internal/app
docs/*.md, README.md, CLAUDE.md Updated all documentation to reference new package paths
Comments suppressed due to low confidence (1)

internal/domain/models.go:103

  • The parseClock function is duplicated in both internal/config/config.go (lines 307-317) and internal/domain/models.go (lines 93-103) with identical implementations.

This duplication creates a maintainability risk. Since internal/domain/models.go:ReportWeekRange already depends on config.Config, consider removing this duplicate and having internal/domain call the version in internal/config instead, or moving the function to a more appropriate shared location if circular dependency concerns exist.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2 to +26

import "time"

func mapPRStatus(pr GitHubPR) string {
if pr.State == "merged" {
return "done"
}
if pr.State == "open" {
return "in progress"
}
return "done"
}

func prReportedAt(pr GitHubPR, loc *time.Location) time.Time {
if pr.State == "open" && !pr.UpdatedAt.IsZero() {
return pr.UpdatedAt
}
if !pr.MergedAt.IsZero() {
return pr.MergedAt
}
if !pr.CreatedAt.IsZero() {
return pr.CreatedAt
}
return time.Now().In(loc)
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These functions are duplicated and contain bugs compared to the active implementations used elsewhere in the codebase.

Bug 1: mapPRStatus returns "done" as the default case (line 12), but the active implementation in internal/fetch/deps.go:70 returns "in progress" as default. This inconsistency could cause incorrect status reporting if this code were ever used.

Bug 2: prReportedAt returns pr.UpdatedAt directly without timezone conversion (line 17), but all other implementations correctly use .In(loc) for timezone handling.

These functions appear to be unused dead code that was inadvertently created during the package split. They should be removed since the active implementations are in internal/fetch/deps.go (lines 63-87) and are called from internal/fetch/auto_fetch.go:106.

Suggested change
import "time"
func mapPRStatus(pr GitHubPR) string {
if pr.State == "merged" {
return "done"
}
if pr.State == "open" {
return "in progress"
}
return "done"
}
func prReportedAt(pr GitHubPR, loc *time.Location) time.Time {
if pr.State == "open" && !pr.UpdatedAt.IsZero() {
return pr.UpdatedAt
}
if !pr.MergedAt.IsZero() {
return pr.MergedAt
}
if !pr.CreatedAt.IsZero() {
return pr.CreatedAt
}
return time.Now().In(loc)
}

Copilot uses AI. Check for mistakes.
Comment on lines +18 to +64
func resolveUserIDs(api *slack.Client, identifiers []string) ([]string, []string, error) {
var ids []string
var names []string
for _, raw := range identifiers {
val := strings.TrimSpace(raw)
if val == "" {
continue
}
if isLikelySlackID(val) {
ids = append(ids, val)
} else {
names = append(names, val)
}
}
if len(names) == 0 {
return uniqueStrings(ids), nil, nil
}
users, err := api.GetUsers()
if err != nil {
return uniqueStrings(ids), names, err
}
nameToID := make(map[string]string)
for _, user := range users {
addName := func(n string) {
n = strings.ToLower(strings.TrimSpace(n))
if n == "" {
return
}
if _, exists := nameToID[n]; !exists {
nameToID[n] = user.ID
}
}
addName(user.Name)
addName(user.RealName)
addName(user.Profile.DisplayName)
}
var unresolved []string
for _, name := range names {
key := strings.ToLower(strings.TrimSpace(name))
if id, ok := nameToID[key]; ok {
ids = append(ids, id)
} else {
unresolved = append(unresolved, name)
}
}
return uniqueStrings(ids), unresolved, nil
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The resolveUserIDs function in this package differs from the one in internal/integrations/slack/slack_users.go in two important ways:

  1. It calls api.GetUsers() directly (line 35) instead of using the cached getCachedUsers() function, which could lead to excessive API calls.
  2. It lacks logging statements that help with debugging user resolution issues.

The version in internal/integrations/slack/slack_users.go (lines 38-93) uses a 5-minute cache and includes logging for better observability. Consider whether the nudge package should use the same cached implementation for consistency and to avoid rate limiting issues with the Slack API.

Copilot uses AI. Check for mistakes.
Comment on lines +89 to +144
var parenPattern = regexp.MustCompile(`\([^)]*\)|([^)]*)`)

func normalizeNameTokens(s string) []string {
if s == "" {
return nil
}
s = parenPattern.ReplaceAllString(s, " ")
s = strings.ToLower(s)
var b strings.Builder
for _, r := range s {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
} else {
b.WriteRune(' ')
}
}
parts := strings.Fields(b.String())
if len(parts) == 0 {
return nil
}
return parts
}

func nameMatches(teamEntry, candidate string) bool {
teamTokens := normalizeNameTokens(teamEntry)
candTokens := normalizeNameTokens(candidate)
if len(teamTokens) == 0 || len(candTokens) == 0 {
return false
}
if allIn(teamTokens, candTokens) || allIn(candTokens, teamTokens) {
return true
}
return false
}

func allIn(needles, haystack []string) bool {
set := make(map[string]bool, len(haystack))
for _, t := range haystack {
set[t] = true
}
for _, t := range needles {
if !set[t] {
return false
}
}
return true
}

func anyNameMatches(teamEntries []string, candidate string) bool {
for _, entry := range teamEntries {
if nameMatches(entry, candidate) {
return true
}
}
return false
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These helper functions (nameMatches, normalizeNameTokens, allIn, anyNameMatches) and the parenPattern regex are duplicated verbatim from internal/integrations/slack/slack_users.go (lines 126-183).

This duplication creates a maintainability issue: if a bug is found or improvement is needed in the name matching logic, it must be fixed in both places. Consider extracting this shared logic to a common location, such as internal/domain or a new internal/util package, so both the fetch and slack integrations can reuse the same implementation.

Copilot uses AI. Check for mistakes.
Comment on lines +28 to +77
type loadStatus int

const (
templateFromFile loadStatus = iota
templateFirstEver
)

func ReportWeekRange(cfg Config, now time.Time) (time.Time, time.Time) {
return domain.ReportWeekRange(cfg, now)
}

func FridayOfWeek(monday time.Time) time.Time {
return domain.FridayOfWeek(monday)
}

func FetchAndImportMRs(cfg Config, db *sql.DB) (fetch.FetchResult, error) {
return fetch.FetchAndImportMRs(cfg, db)
}

func FormatFetchSummary(result fetch.FetchResult) string {
return fetch.FormatFetchSummary(result)
}

func BuildReportsFromLast(cfg Config, items []WorkItem, reportDate time.Time, corrections []ClassificationCorrection, historicalItems []domain.HistoricalItem) (BuildResult, error) {
return report.BuildReportsFromLast(cfg, items, reportDate, corrections, historicalItems)
}

func WriteEmailDraftFile(body, outputDir string, reportDate time.Time, subjectPrefix string) (string, error) {
return report.WriteEmailDraftFile(body, outputDir, reportDate, subjectPrefix)
}

func WriteReportFile(content, outputDir string, reportDate time.Time, teamName string) (string, error) {
return report.WriteReportFile(content, outputDir, reportDate, teamName)
}

func renderTeamMarkdown(t *report.ReportTemplate) string {
return report.RenderTeamMarkdown(t)
}

func renderBossMarkdown(t *report.ReportTemplate) string {
return report.RenderBossMarkdown(t)
}

func loadTemplateForGeneration(outputDir, teamName string, reportDate time.Time) (*report.ReportTemplate, loadStatus, error) {
t, err := report.LoadTemplateForGeneration(outputDir, teamName, reportDate)
if err != nil {
return nil, templateFromFile, err
}
return t, templateFromFile, nil
}
Copy link

Copilot AI Feb 19, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The templateFirstEver constant is defined but never used in the codebase. The loadTemplateForGeneration wrapper always returns templateFromFile regardless of the actual load status (line 76), and searching the codebase shows templateFirstEver is never referenced.

This is dead code that should be removed, or if the distinction is meaningful, the wrapper should correctly propagate the actual load status from the underlying function.

Copilot uses AI. Check for mistakes.
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