Title
internal/config: TOML schema, defaults, validation
Summary
Implement configuration loading exactly per DESIGN.md §5: TOML file at the
XDG path, zero-config defaults, tilde expansion, strict validation with all
errors reported at once, and unknown-key warnings.
Context
Every subcommand starts by loading config; worklog init (27) writes the
starter file; doctor (28) re-uses validation. This issue introduces the
project's single third-party dependency (github.com/BurntSushi/toml,
ADR-001).
Scope
internal/config/config.go, defaults.go, validate.go, tests, and
testdata/*.toml fixtures. Adds the toml dependency to go.mod.
Detailed Requirements
- Structs mirroring DESIGN §5's TOML exactly (
General, Output,
Sources, SourcesZsh, SourcesGit, SourcesClaudeCode,
SourcesCodex, LLM, Redaction, Standup), field tags matching the
TOML key names shown there (toml:"history_file" etc.).
Default() *Config returns the documented defaults verbatim (language
"ja", git root ~/dev, llm.enabled false (opt-in), llm endpoint
http://127.0.0.1:11434/v1, timeout_seconds 120, max_input_chars
24000, max_output_chars 4000, temperature 0.2, redaction "on",
standup.previous_workday true, sources enabled: all four, max_depth
3, exclude_dirs per DESIGN §5).
DefaultPath() string — $XDG_CONFIG_HOME/worklog/config.toml when
XDG_CONFIG_HOME is set and absolute, else ~/.config/worklog/config.toml.
Load(path string) (*Config, []string, error):
path == "" → DefaultPath(); missing file → defaults, no error.
- Decode with
toml.DecodeFile using md.Undecoded() to produce one
warning string per unknown key (typo detection), returned as the second
value — never an error.
- After decode, merge onto defaults (decode into a copy of
Default() so
absent keys keep defaults).
ExpandPath(p string) string — ~ and ~/ expansion via
os.UserHomeDir; no environment-variable expansion (predictability rule,
DESIGN §5). Applied by accessor methods (c.ZshHistoryFile(),
c.GitRoots(), c.ClaudeProjectsDir(), c.CodexSessionsDir(),
c.CodexSessionIndex(), c.CodexArchivedDir(), c.OutputDirectory())
rather than mutating stored values (round-trip fidelity for init).
c.ZshHistoryFile() implements the resolution order: configured value →
$HISTFILE (absolute paths only) → ~/.zsh_history.
Validate() []error collecting ALL violations, per DESIGN §5 rules:
language ∈ {ja,en}; timezone loads; every regex in
sources.zsh.exclude_patterns, redaction.extra_patterns,
redaction.allowlist compiles as RE2; sources.enabled values ∈ the four
known ids (unknown id = error, not warning); llm.enabled && model=="" →
error text mentioning worklog doctor (reachable only via an explicit
config file since the default is enabled=false — zero-config always
validates); endpoint URL: scheme http/https + non-empty host (port
optional); static loopback pre-check: if the host is an IP literal it
must be loopback (hostnames are checked at dial time, DESIGN §5/§11);
numeric bounds exactly as listed (timeout_seconds 1–600,
max_input_chars 1000–1000000, max_output_chars 100–100000,
temperature 0–2, max_depth 1–6); redaction.mode ∈ {on,off}.
Starter() string returns the commented starter TOML used by
worklog init (content = DESIGN §5 block with explanatory comments;
[llm] section ships enabled = false with the comment
# set enabled = true and model = "..." after running worklog doctor).
Keep it in a const; issue 27 writes it to disk.
Acceptance Criteria
Validation
go test -race -cover ./internal/config/ in PR; go mod tidy diff empty;
boundary script green.
Dependencies
Non-goals
Writing the config file (27), flag handling (07), dial-time loopback
enforcement (23).
Design References
docs/DESIGN.md §5, §11.1
docs/decisions/ADR-001-go-and-dependency-policy.md
Source of truth: docs/issues/05-config-package.md (PR #1, branch docs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies: docs/ISSUE_PLAN.md (this is issue 05 of 33).
Title
internal/config: TOML schema, defaults, validation
Summary
Implement configuration loading exactly per DESIGN.md §5: TOML file at the
XDG path, zero-config defaults, tilde expansion, strict validation with all
errors reported at once, and unknown-key warnings.
Context
Every subcommand starts by loading config;
worklog init(27) writes thestarter file;
doctor(28) re-uses validation. This issue introduces theproject's single third-party dependency (
github.com/BurntSushi/toml,ADR-001).
Scope
internal/config/config.go,defaults.go,validate.go, tests, andtestdata/*.tomlfixtures. Adds the toml dependency togo.mod.Detailed Requirements
General,Output,Sources,SourcesZsh,SourcesGit,SourcesClaudeCode,SourcesCodex,LLM,Redaction,Standup), field tags matching theTOML key names shown there (
toml:"history_file"etc.).Default() *Configreturns the documented defaults verbatim (language"ja", git root
~/dev,llm.enabledfalse (opt-in), llm endpointhttp://127.0.0.1:11434/v1,timeout_seconds120,max_input_chars24000,
max_output_chars4000,temperature0.2, redaction "on",standup.previous_workdaytrue, sources enabled: all four,max_depth3, exclude_dirs per DESIGN §5).
DefaultPath() string—$XDG_CONFIG_HOME/worklog/config.tomlwhenXDG_CONFIG_HOMEis set and absolute, else~/.config/worklog/config.toml.Load(path string) (*Config, []string, error):path == ""→DefaultPath(); missing file → defaults, no error.toml.DecodeFileusingmd.Undecoded()to produce onewarning string per unknown key (typo detection), returned as the second
value — never an error.
Default()soabsent keys keep defaults).
ExpandPath(p string) string—~and~/expansion viaos.UserHomeDir; no environment-variable expansion (predictability rule,DESIGN §5). Applied by accessor methods (
c.ZshHistoryFile(),c.GitRoots(),c.ClaudeProjectsDir(),c.CodexSessionsDir(),c.CodexSessionIndex(),c.CodexArchivedDir(),c.OutputDirectory())rather than mutating stored values (round-trip fidelity for
init).c.ZshHistoryFile()implements the resolution order: configured value →$HISTFILE(absolute paths only) →~/.zsh_history.Validate() []errorcollecting ALL violations, per DESIGN §5 rules:language ∈ {ja,en}; timezone loads; every regex in
sources.zsh.exclude_patterns,redaction.extra_patterns,redaction.allowlistcompiles as RE2;sources.enabledvalues ∈ the fourknown ids (unknown id = error, not warning);
llm.enabled && model==""→error text mentioning
worklog doctor(reachable only via an explicitconfig file since the default is
enabled=false— zero-config alwaysvalidates); endpoint URL: scheme http/https + non-empty host (port
optional); static loopback pre-check: if the host is an IP literal it
must be loopback (hostnames are checked at dial time, DESIGN §5/§11);
numeric bounds exactly as listed (
timeout_seconds1–600,max_input_chars1000–1000000,max_output_chars100–100000,temperature0–2,max_depth1–6);redaction.mode∈ {on,off}.Starter() stringreturns the commented starter TOML used byworklog init(content = DESIGN §5 block with explanatory comments;[llm]section shipsenabled = falsewith the comment# set enabled = true and model = "..." after running worklog doctor).Keep it in a
const; issue 27 writes it to disk.Acceptance Criteria
Load("")with no file present returns defaults with nowarnings/errors.
the exact dotted key); each validation rule violated once (one fixture
or table case per rule) with
Validate()returning ALL errorstogether, not just the first.
ZshHistoryFile()order proven with tests manipulatingHISTFILE(set-absolute, set-relative→ignored, unset).
Starter()round-trips: decodes with zero unknownkeys and equals
Default()after load.go.modisgithub.com/BurntSushi/toml.Validation
go test -race -cover ./internal/config/in PR;go mod tidydiff empty;boundary script green.
Dependencies
Non-goals
Writing the config file (27), flag handling (07), dial-time loopback
enforcement (23).
Design References
docs/DESIGN.md§5, §11.1docs/decisions/ADR-001-go-and-dependency-policy.mdSource of truth:
docs/issues/05-config-package.md(PR #1, branchdocs/v1-design). If this issue and the repo docs disagree, the docs win. Execution order and dependencies:docs/ISSUE_PLAN.md(this is issue 05 of 33).