Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
60 commits
Select commit Hold shift + click to select a range
3ea1650
refactor: drop 11 deprecation-stub files
Jun 28, 2026
3f61a31
refactor(auth): inline CreateAuth; drop dead manager+providers subpkg
Jun 28, 2026
e8c958e
refactor(daemon): drop dead delta compat shims
Jun 28, 2026
17c9c97
refactor(state): drop transitively-dead helpers and test wrappers
Jun 28, 2026
8209b36
refactor(hugo/models): drop V2/V3 typed-transformer design
Jun 28, 2026
644687b
refactor(hugo/editlink): consolidate detector chain into resolver.go
Jun 28, 2026
1ff5ae7
fix(build): route docbuilder build through BuildService
Jun 28, 2026
641d604
refactor(build/validation): depend on local interface, not *hugo.Gene…
Jun 28, 2026
d76fb3a
refactor(state): consolidate three state-interface hierarchies
Jun 28, 2026
e5dca93
refactor(hugo/models): define per-stage narrow interfaces
Jun 28, 2026
a25238a
chore: add var _ compile-time assertions for major interfaces
Jun 28, 2026
b390f92
fix(build): preserve original log field semantics in CLI success log
Jun 29, 2026
23416cb
refactor(hugo/stages): drop no-op pipeline stages
Jun 29, 2026
edad679
chore: delete long-tail dead exports across 14 packages
Jun 29, 2026
cf1971a
refactor(forge): runner consumes BuildEnqueuer port, not queue.BuildJ…
Jun 29, 2026
056296b
refactor(preview): depend on preview/runtime, not daemon directly
Jun 29, 2026
a484403
refactor(hugo): trim dead links wrapper + inline mergeParams
Jun 29, 2026
7c1c153
refactor(state): drop dead Manager type
Jun 29, 2026
6596c20
refactor(state): split ServiceAdapter by capability
Jun 29, 2026
aada8bd
refactor(daemon): defer god-package carve to dedicated refactor
Jun 29, 2026
6f03a5f
refactor(build/queue): home BuildServiceAdapter in queue package
Jun 29, 2026
008a4a4
refactor(foundation,build/queue): complete A1 + fix T21 build break
Jun 29, 2026
3b4b23e
refactor(hugo): rename errors sub-package to hugoerrors (M1)
Jun 29, 2026
f5d909e
refactor(forge,build): drop dead DiscoveryEnqueuerAdapter (H2)
Jun 29, 2026
af8fbd9
refactor(metrics): delete dead metrics package (H1)
Jun 29, 2026
bdf65bd
refactor(daemon,httpserver): drop dead interface, field, methods (M12…
Jun 29, 2026
84df2f8
refactor(state): drop dead BuildStore/ScheduleStore/StatisticsStore (…
Jun 29, 2026
6f7bd64
refactor(state): collapse ServiceAdapter and per-store interfaces (H4…
Jun 29, 2026
9b90ac7
refactor(retry): promote retry.Policy to first-class executor (H7)
Jun 29, 2026
1d42116
refactor(httpserver): split Runtime into Status + optional Triggers/M…
Jun 29, 2026
66700bc
refactor(errors): standardize 8 packages on foundation/errors + forbi…
Jun 29, 2026
b40fe1a
refactor(forge,hugo): centralize titleCase, edit URL, and forge-type …
Jun 29, 2026
b94975b
refactor(docmodel,urlutil): centralize markdown + URL predicates (M10…
Jun 29, 2026
b64940b
refactor(build,frontmatterops,delta): hashutil+UpsertFields+BuildServ…
Jun 29, 2026
90a95c5
refactor(errors): classify internal/preview/local_preview.go errors (…
Jun 29, 2026
cc50470
refactor(errors): classify internal/state/json_store.go errors (H6 fo…
Jun 29, 2026
79dfbd2
refactor(errors): classify internal/linkverify/extractor.go errors (H…
Jun 29, 2026
e0f113e
refactor(errors): classify internal/forge/{discoveryrunner,helpers} e…
Jun 29, 2026
10cd72f
refactor(errors): classify internal/foundation/normalization/normaliz…
Jun 29, 2026
8154dec
refactor(errors): classify internal/markdown/edits.go errors (H6 foll…
Jun 29, 2026
72319b1
refactor(errors): classify internal/auth/auth.go errors (H6 follow-up)
Jun 29, 2026
6993431
fix(lint): remove unintended 'internal/.*\.go$' exclusion added by mi…
Jun 29, 2026
371d97f
refactor(errors): classify internal/daemon/{event_emitter,scheduler,d…
Jun 29, 2026
0e60027
refactor(errors): classify internal/git/{hash,remote_cache} errors (H…
Jun 29, 2026
f8c12e2
refactor(errors): classify internal/templates/*.go errors (H6 follow-up)
Jun 29, 2026
80cdfbb
refactor(errors): classify internal/server/httpserver/{http_server,ht…
Jun 29, 2026
8115ae9
refactor(errors): classify internal/{versioning,workspace} errors (H6…
Jun 29, 2026
00d71f2
refactor(errors): classify internal/docs/taxonomy_snippets.go errors …
Jun 29, 2026
e7c86c5
refactor(errors): classify internal/doctemplate/app errors (H6 follow…
Jun 29, 2026
48555b8
chore(lint): prune H6 exclusion entries for already-migrated packages
Jun 29, 2026
36849c9
refactor(errors): classify internal/lint/*.go errors (H6 follow-up)
Jun 29, 2026
9dbf78c
refactor(errors): classify cmd/docbuilder/commands/* errors (H6 follo…
Jun 29, 2026
4ecb69b
refactor(errors): classify internal/hugo/{categories_menu,...,structu…
Jun 29, 2026
8660327
refactor(errors): classify internal/doctemplate/service/template_serv…
Jun 29, 2026
76ce7fa
fix(daemon): initialize state manager before build service
Jun 29, 2026
b815e20
fix(daemon): assign forgeManager to daemon.forgeManager
Jun 30, 2026
2790835
refactor(daemon): extract per-subsystem constructors from NewDaemonWi…
Jun 30, 2026
af86236
refactor(daemon): extract per-subsystem start/stop helpers from Start…
Jun 30, 2026
c49a6b3
refactor(daemon): carve lifecycle sub-package
Jun 30, 2026
6f03ab6
fix(lint): migrate .golangci.yml to v2 schema
Jun 30, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,14 @@ linters:
- usestdlibvars
- whitespace
settings:
dupl:
# 200 covers the 57-line render/output_path parallel template
# parsing as well as the test patterns in cmd/ (each repeated
# setup block in cli_integration_test.go and
# template_integration_test.go registers as ~20 lines, fitting
# under 200). Raise further if the linter starts flagging new
# template helpers down the line.
threshold: 200
tagalign:
sort: true
strict: false
Expand Down Expand Up @@ -93,6 +101,41 @@ linters:
local-variables-are-used: false
generated-is-used: false

# Pass H6 of cleanup migration. forbidigo is configured (above) to
# ban fmt.Errorf in production code. As packages migrate to
# foundation/errors they are removed from the exclusion list below.
# Remove a line whenever you finish migrating a package.
#
# Migrated (not excluded, must pass the rule):
# internal/state/, internal/linkverify/,
# internal/templates/sequence.go, internal/forge/enhanced_mock.go,
# internal/hugo/indexes.go, internal/build/delta/manager.go,
# internal/config/composite_defaults.go, internal/daemon/daemon.go.
#
# v2 schema note: the v1 keys that used to live under `issues:` have
# moved here. Specifically:
# issues.exclude-dirs-use-default -> removed (option no longer exists)
# issues.exclude-dirs / -files -> linters.exclusions.paths (mirror
# under formatters.exclusions.paths
# so formatters apply the same skips)
# issues.exclude-rules -> linters.exclusions.rules
exclusions:
paths:
- 'internal/hugo/stages_transition_test\.go$'
rules:
# The old //nolint:forbidigo directives in cmd/ now target fmt.Println;
# forbidigo fmt.Errorf never matches in those functions, so the
# comments are unused. Silence the unused-directive complaints from nolintlint.
- linters: [nolintlint]
path: 'cmd/docbuilder/commands/(build|init)\.go$'
- linters: [nolintlint]
path: 'examples/tools/debug_webhook\.go$'
# render.go and output_path.go have parallel template-parsing
# shapes by design; the dupl threshold flags them as 57-line copies.
# Both files were in this state before this cleanup.
- linters: [dupl]
path: 'internal/templates/(render|output_path)\.go$'

formatters:
enable:
# - gci
Expand All @@ -104,4 +147,10 @@ formatters:
sections:
- standard
- default
- prefix(git.home.luguber.info/inful/docbuilder)
- prefix(git.home.luguber.info/inful/docbuilder)
exclusions:
# Mirror the linters-side file skip so formatters leave the
# matching test file unchanged (matches v1's `issues.exclude-files`
# semantics, which applied to linters and formatters alike).
paths:
- 'internal/hugo/stages_transition_test\.go$'
82 changes: 54 additions & 28 deletions cmd/docbuilder/commands/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,12 @@ import (
"os"
"path/filepath"

"git.home.luguber.info/inful/docbuilder/internal/build"
"git.home.luguber.info/inful/docbuilder/internal/config"
"git.home.luguber.info/inful/docbuilder/internal/docs"
derrors "git.home.luguber.info/inful/docbuilder/internal/foundation/errors"
"git.home.luguber.info/inful/docbuilder/internal/hugo"
"git.home.luguber.info/inful/docbuilder/internal/workspace"
)

// BuildCmd implements the 'build' command.
Expand Down Expand Up @@ -45,7 +48,7 @@ func (b *BuildCmd) Run(_ *Global, root *CLI) error {
} else {
_, loadedCfg, err := config.LoadWithResult(root.Config)
if err != nil {
return fmt.Errorf("load config: %w", err)
return derrors.WrapError(err, derrors.CategoryConfig, "load config").Build()
}
cfg = loadedCfg
slog.Info("Loaded config from file", "config", root.Config)
Expand Down Expand Up @@ -85,7 +88,9 @@ func (b *BuildCmd) Run(_ *Global, root *CLI) error {
return RunBuild(cfg, outputDir, b.Incremental, root.Verbose, b.KeepWorkspace)
}

// RunBuild executes the build pipeline using the unified generator pipeline.
// RunBuild executes the build pipeline using build.BuildService.
// This is the canonical CLI entry point; it routes through the same service
// the daemon uses (with a nil skip-evaluator and a noop recorder).
//
//nolint:forbidigo // fmt is used for user-facing messages
func RunBuild(cfg *config.Config, outputDir string, incrementalMode, verbose, keepWorkspace bool) error {
Expand All @@ -107,44 +112,65 @@ func RunBuild(cfg *config.Config, outputDir string, incrementalMode, verbose, ke
"incremental", incrementalMode,
"keep_workspace", keepWorkspace)

// Create workspace manager
wsManager, err := CreateWorkspace(cfg)
if err != nil {
return err
}
if !keepWorkspace {
defer CleanupWorkspace(wsManager)
} else {
slog.Info("Workspace will be preserved for debugging", "path", wsManager.GetPath())
fmt.Printf("Workspace preserved at: %s\n", wsManager.GetPath())
// Build the workspace factory. Persistent managers (KeepWorkspace=true)
// skip cleanup on exit; ephemeral managers (KeepWorkspace=false) are
// removed by BuildService's deferred cleanup.
wsDir := cfg.Build.WorkspaceDir
workspaceFactory := func() *workspace.Manager {
if keepWorkspace {
return workspace.NewPersistentManager(wsDir, "working")
}
return workspace.NewManager(wsDir)
}

// Initialize Generator
generator := hugo.NewGenerator(cfg, outputDir).WithKeepStaging(keepWorkspace)
svc := build.NewBuildService().
WithWorkspaceFactory(workspaceFactory).
WithHugoGeneratorFactory(func(c *config.Config, dir string) build.HugoGenerator {
return hugo.NewGenerator(c, dir).WithKeepStaging(keepWorkspace)
}).
WithSkipEvaluatorFactory(func(string) build.SkipEvaluator {
// Skip evaluation requires daemon state; the CLI never has it.
return nil
})

req := build.BuildRequest{
Config: cfg,
OutputDir: outputDir,
Incremental: incrementalMode,
Options: build.BuildOptions{
Verbose: verbose,
KeepWorkspace: keepWorkspace,
SkipIfUnchanged: false,
},
}

// Run the unified pipeline
ctx := context.Background()
report, err := generator.GenerateFullSite(ctx, cfg.Repositories, wsManager.GetPath())
result, err := svc.Run(context.Background(), req)
if err != nil {
slog.Error("Build pipeline failed", "error", err)
// Show workspace location on error for debugging
if keepWorkspace {
fmt.Printf("\nError occurred. Workspace preserved at: %s\n", wsManager.GetPath())
fmt.Printf("\nError occurred. Workspace preserved (check above log).\n")
fmt.Printf("Hugo staging directory: %s_stage\n", outputDir)
}
return err
}

if report.FailedRepositories > 0 {
if result.Report != nil && result.Report.FailedRepositories > 0 {
slog.Warn("Some repositories were skipped due to errors",
"skipped", report.FailedRepositories,
"skipped", result.Report.FailedRepositories,
"total", len(cfg.Repositories))
}

// Preserve the original log shape: "pages" = rendered HTML pages (not
// markdown files). The CLI used to read report.RenderedPages directly;
// BuildService exposes that via BuildResult.Report.RenderedPages.
renderedPages := 0
if result.Report != nil {
renderedPages = result.Report.RenderedPages
}
slog.Info("Build completed successfully",
"output", outputDir,
"pages", report.RenderedPages,
"skipped_repos", report.FailedRepositories)
"pages", renderedPages,
"skipped_repos", result.RepositoriesSkipped)

fmt.Println("Build completed successfully")
return nil
Expand Down Expand Up @@ -196,12 +222,12 @@ func (b *BuildCmd) runLocalBuild(cfg *config.Config, outputDir string, verbose,
// Resolve absolute path to docs directory
docsPath, err := filepath.Abs(b.DocsDir)
if err != nil {
return fmt.Errorf("resolve docs dir: %w", err)
return derrors.WrapError(err, derrors.CategoryFileSystem, "resolve docs dir").Build()
}

// Verify docs directory exists
if st, statErr := os.Stat(docsPath); statErr != nil || !st.IsDir() {
return fmt.Errorf("docs dir not found or not a directory: %s (use -d to specify a different path)", docsPath)
return derrors.NewError(derrors.CategoryNotFound, "docs dir not found or not a directory (use -d to specify a different path)").WithContext("path", docsPath).Build()
}

slog.Info("Building from local directory",
Expand All @@ -217,12 +243,12 @@ func (b *BuildCmd) runLocalBuild(cfg *config.Config, outputDir string, verbose,
slog.Info("Discovering documentation files")
docFiles, discErr := discovery.DiscoverDocs(repoPaths)
if discErr != nil {
return fmt.Errorf("discovery failed: %w", discErr)
return derrors.WrapError(discErr, derrors.CategoryInternal, "discovery failed").Build()
}

if len(docFiles) == 0 {
slog.Warn("No documentation files found in directory", "dir", docsPath)
return fmt.Errorf("no documentation files found in %s", docsPath)
return derrors.NewError(derrors.CategoryNotFound, "no documentation files found").WithContext("path", docsPath).Build()
}

slog.Info("Documentation discovered", "files", len(docFiles))
Expand All @@ -239,7 +265,7 @@ func (b *BuildCmd) runLocalBuild(cfg *config.Config, outputDir string, verbose,
if keepWorkspace {
fmt.Printf("\nError occurred. Hugo staging directory: %s_stage\n", outputDir)
}
return fmt.Errorf("site generation failed: %w", err)
return derrors.WrapError(err, derrors.CategoryInternal, "site generation failed").Build()
}

slog.Info("Hugo site generated successfully",
Expand Down
5 changes: 3 additions & 2 deletions cmd/docbuilder/commands/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (

"git.home.luguber.info/inful/docbuilder/internal/config"
"git.home.luguber.info/inful/docbuilder/internal/forge"
derrors "git.home.luguber.info/inful/docbuilder/internal/foundation/errors"
"git.home.luguber.info/inful/docbuilder/internal/git"
"git.home.luguber.info/inful/docbuilder/internal/workspace"
)
Expand Down Expand Up @@ -83,7 +84,7 @@ func LoadEnvFile() error {
for _, p := range envPaths {
if _, err := os.Stat(p); err == nil {
if err := godotenv.Load(p); err != nil {
return fmt.Errorf("failed loading %s: %w", p, err)
return derrors.WrapError(err, derrors.CategoryFileSystem, "failed loading").WithContext("path", p).Build()
}
return nil
}
Expand Down Expand Up @@ -215,7 +216,7 @@ func ApplyAutoDiscovery(ctx context.Context, cfg *config.Config) error {
if len(cfg.Repositories) == 0 && len(cfg.Forges) > 0 {
repos, err := AutoDiscoverRepositories(ctx, cfg)
if err != nil {
return fmt.Errorf("auto-discovery failed: %w", err)
return derrors.WrapError(err, derrors.CategoryInternal, "auto-discovery failed").Build()
}
cfg.Repositories = repos
}
Expand Down
10 changes: 5 additions & 5 deletions cmd/docbuilder/commands/daemon.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@ package commands

import (
"context"
"fmt"
"log/slog"
"os/signal"
"syscall"
"time"

"git.home.luguber.info/inful/docbuilder/internal/config"
"git.home.luguber.info/inful/docbuilder/internal/daemon"
derrors "git.home.luguber.info/inful/docbuilder/internal/foundation/errors"
)

// DaemonCmd implements the 'daemon' command.
Expand All @@ -25,7 +25,7 @@ func (d *DaemonCmd) Run(_ *Global, root *CLI) error {

result, cfg, err := config.LoadWithResult(root.Config)
if err != nil {
return fmt.Errorf("load config: %w", err)
return derrors.WrapError(err, derrors.CategoryConfig, "load config").Build()
}
// Print any normalization warnings
for _, w := range result.Warnings {
Expand All @@ -44,7 +44,7 @@ func RunDaemon(cfg *config.Config, dataDir, configPath string) error {
// Create and start the daemon with config file watching
d, err := daemon.NewDaemonWithConfigFile(cfg, configPath)
if err != nil {
return fmt.Errorf("failed to create daemon: %w", err)
return derrors.WrapError(err, derrors.CategoryInternal, "failed to create daemon").Build()
}

// Start daemon in a goroutine
Expand All @@ -59,7 +59,7 @@ func RunDaemon(cfg *config.Config, dataDir, configPath string) error {
select {
case err := <-errChan:
if err != nil {
return fmt.Errorf("daemon error: %w", err)
return derrors.WrapError(err, derrors.CategoryInternal, "daemon error").Build()
}
case <-ctx.Done():
slog.Info("Shutdown signal received, stopping daemon...")
Expand All @@ -70,7 +70,7 @@ func RunDaemon(cfg *config.Config, dataDir, configPath string) error {
defer stopCancel()

if err := d.Stop(stopCtx); err != nil {
return fmt.Errorf("failed to stop daemon: %w", err)
return derrors.WrapError(err, derrors.CategoryInternal, "failed to stop daemon").Build()
}

slog.Info("Daemon stopped successfully")
Expand Down
16 changes: 8 additions & 8 deletions cmd/docbuilder/commands/discover.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ package commands

import (
"context"
"fmt"
"log/slog"

"git.home.luguber.info/inful/docbuilder/internal/config"
"git.home.luguber.info/inful/docbuilder/internal/docs"
derrors "git.home.luguber.info/inful/docbuilder/internal/foundation/errors"
"git.home.luguber.info/inful/docbuilder/internal/git"
)

Expand All @@ -15,27 +15,27 @@ type DiscoverCmd struct {
Repository string `short:"r" help:"Specific repository to discover (optional)"`
}

func (d *DiscoverCmd) Run(_ *Global, root *CLI) error {
func (d *DiscoverCmd) Run(ctx context.Context, _ *Global, root *CLI) error {
// Load .env file if it exists (before config)
if err := LoadEnvFile(); err == nil && root.Verbose {
slog.Info("Loaded environment variables from .env file")
}

result, cfg, err := config.LoadWithResult(root.Config)
if err != nil {
return fmt.Errorf("load config: %w", err)
return derrors.WrapError(err, derrors.CategoryConfig, "load config").Build()
}
// Print any normalization warnings
for _, w := range result.Warnings {
slog.Warn(w)
}
if err := ApplyAutoDiscovery(context.Background(), cfg); err != nil {
if err := ApplyAutoDiscovery(ctx, cfg); err != nil {
return err
}
return RunDiscover(cfg, d.Repository)
return RunDiscover(ctx, cfg, d.Repository)
}

func RunDiscover(cfg *config.Config, specificRepo string) error {
func RunDiscover(ctx context.Context, cfg *config.Config, specificRepo string) error {
slog.Info("Starting documentation discovery", "repositories", len(cfg.Repositories))

// Create workspace manager
Expand All @@ -62,7 +62,7 @@ func RunDiscover(cfg *config.Config, specificRepo string) error {
}
}
if len(reposToProcess) == 0 {
return fmt.Errorf("repository '%s' not found in configuration", specificRepo)
return derrors.NewError(derrors.CategoryConfig, "repository not found in configuration").WithContext("repository", specificRepo).Build()
}
} else {
reposToProcess = cfg.Repositories
Expand All @@ -75,7 +75,7 @@ func RunDiscover(cfg *config.Config, specificRepo string) error {
slog.Info("Cloning repository", "name", repo.Name, "url", repo.URL)

var result git.CloneResult
result, err = gitClient.CloneRepoWithMetadata(*repo)
result, err = gitClient.CloneRepoWithMetadata(ctx, *repo)
if err != nil {
slog.Error("Failed to clone repository", "name", repo.Name, "error", err)
return err
Expand Down
Loading
Loading