Absorb the config reexport layer into toolbelt/config #3

Closed
opened 2026-08-06 16:45:51 +00:00 by ginjiruu · 2 comments
Owner

Question

Should toolbelt/config ship a standard app-config bootstrap so apps stop hand-writing the reexport layer?

Consumers and the example all wrap toolbelt/config with a reexport layer: reexports.go (~68-76 lines re-exporting Init/ReadField*/WriteField/Save/BuildLogger/PrintConfig/Validate*/Fields()/Register), consts.go (17-21 lines, byte-identical incl. a stale GO_TEMPLATE prefix in ttrpg), validate.go (58-65 lines, KCL schema validation), fields_meta.go (47 lines: ResolveDBPath/ResolveTupleFilePath). This passes the "example proves it" boundary.

Decisions this ticket resolves:

  1. What exactly moves into toolbelt/config — the reexport shim, the universal-field set, the KCL validation entry?
  2. The divergence: go-template is KCL-driven (schema.kkcl-generate), ttrpg hand-writes fields_app.go and has no schema. What does a shared bootstrap assume about that?
  3. Does BuildLogger move here (it's re-exported in both) or stay in the composition-root ticket?

Resolve with /grilling + /domain-modeling. On close, write the plan to docs/plans/ and reshape the relevant example/ surface.

## Question Should `toolbelt/config` ship a standard app-config bootstrap so apps stop hand-writing the reexport layer? Consumers and the example all wrap `toolbelt/config` with a reexport layer: `reexports.go` (~68-76 lines re-exporting `Init`/`ReadField*`/`WriteField`/`Save`/`BuildLogger`/`PrintConfig`/`Validate*`/`Fields()`/`Register`), `consts.go` (17-21 lines, byte-identical incl. a stale `GO_TEMPLATE` prefix in ttrpg), `validate.go` (58-65 lines, KCL schema validation), `fields_meta.go` (47 lines: `ResolveDBPath`/`ResolveTupleFilePath`). This passes the "example proves it" boundary. Decisions this ticket resolves: 1. What exactly moves into `toolbelt/config` — the reexport shim, the universal-field set, the KCL validation entry? 2. The divergence: go-template is KCL-driven (`schema.k` → `kcl-generate`), ttrpg hand-writes `fields_app.go` and has no schema. What does a shared bootstrap assume about that? 3. Does `BuildLogger` move here (it's re-exported in both) or stay in the composition-root ticket? Resolve with `/grilling` + `/domain-modeling`. On close, write the plan to `docs/plans/` and reshape the relevant `example/` surface.
Author
Owner

Resolution (grilling + domain-modeling; full plan: docs/plans/config-bootstrap.md)

Decisions:

  1. Value bootstrap. config.New(Options{...}) returns an object; the reexport shim is deleted. (toolbelt/config goes instance-owned: the Fields/ConfigEnvPrefix/AppName/AppVersion globals die; Init/ReadField*/WriteField/Save/PrintConfig/Validate become methods; Register/Fields() wrappers die.)
  2. KCL is the single source of truth. Every app declares config in schema.k; kcl-generate emits the field registry and the bootstrap (var App = config.New(...) with AppName/AppVersion/EnvPrefix/SchemaPath/field list + the _ ".../config/kcl" blank import). internal/config becomes 100% generated; consts.go/validate.go/reexports.go disappear.
  3. BuildLogger moves to toolbelt/app and is removed from toolbelt/config (composition-root concern; every app already has its own BuildLogger(logLevel, logFormat)).
  4. cfg.Validate() does KCL schema validation (the app's validate.go filter/reflection boilerplate is deleted).
  5. Completion data surface: cfg.Fields() exposes Field.ValidValues for toolbelt/cmd (ticket #9) cobra completion.
  6. Service-owned defaults graduated to new ticket #12 ("Per-package KCL config fragments"): each service ships a KCL fragment, kcl-generate merges N fragments; this ticket's bootstrap is agnostic to where defaults came from.
  7. ResolveDBPath/ResolveTupleFilePath ruled out of scope — fail the "example proves it" boundary; toolbelt/db.NewDatabase already resolves (filepath.Abs).

Consumer migration (go-template, ttrpg) is a separate later project per the map.

Example reshape (implemented in a later session): delete the three hand-written files, regenerate, mechanically switch call sites to config.App.X, run root + tools tests.

**Resolution** (grilling + domain-modeling; full plan: `docs/plans/config-bootstrap.md`) Decisions: 1. **Value bootstrap.** `config.New(Options{...})` returns an object; the reexport shim is deleted. (`toolbelt/config` goes instance-owned: the `Fields`/`ConfigEnvPrefix`/`AppName`/`AppVersion` globals die; `Init`/`ReadField*`/`WriteField`/`Save`/`PrintConfig`/`Validate` become methods; `Register`/`Fields()` wrappers die.) 2. **KCL is the single source of truth.** Every app declares config in `schema.k`; `kcl-generate` emits the field registry **and** the bootstrap (`var App = config.New(...)` with `AppName`/`AppVersion`/`EnvPrefix`/`SchemaPath`/field list + the `_ ".../config/kcl"` blank import). `internal/config` becomes 100% generated; `consts.go`/`validate.go`/`reexports.go` disappear. 3. **BuildLogger moves to `toolbelt/app`** and is removed from `toolbelt/config` (composition-root concern; every app already has its own `BuildLogger(logLevel, logFormat)`). 4. **`cfg.Validate()`** does KCL schema validation (the app's `validate.go` filter/reflection boilerplate is deleted). 5. **Completion data surface:** `cfg.Fields()` exposes `Field.ValidValues` for `toolbelt/cmd` (ticket #9) cobra completion. 6. **Service-owned defaults graduated** to new ticket #12 ("Per-package KCL config fragments"): each service ships a KCL fragment, `kcl-generate` merges N fragments; this ticket's bootstrap is agnostic to where defaults came from. 7. **`ResolveDBPath`/`ResolveTupleFilePath` ruled out of scope** — fail the "example proves it" boundary; `toolbelt/db.NewDatabase` already resolves (`filepath.Abs`). Consumer migration (go-template, ttrpg) is a separate later project per the map. Example reshape (implemented in a later session): delete the three hand-written files, regenerate, mechanically switch call sites to `config.App.X`, run root + tools tests.
Author
Owner

Resolved via grilling. Plan: docs/plans/config-bootstrap.md. Service-owned defaults graduated to #12. See resolution comment.

Resolved via grilling. Plan: docs/plans/config-bootstrap.md. Service-owned defaults graduated to #12. See resolution comment.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Reference
ginjiruu/toolbelt#3
No description provided.