Toolbelt glue absorption — map: what the toolbelt absorbs from duplicated consumer glue #1

Open
opened 2026-08-06 16:44:30 +00:00 by ginjiruu · 0 comments
Owner

Destination

A decision/implementation spec for what the toolbelt absorbs from the duplicated consumer glue — delivered as a docs/plans/ collection, one plan per ticket. Consumers (go-template, ttrpg) stay out of scope until the map is done. The map ends when every ticket is closed and the plans are written; reshaping the example/ app onto the absorbed surfaces is deliberately deferred to a separate post-map clean rewrite where the challenges are reassessed against every surface at once.

Notes

  • Domain: toolbelt platform glue; absorbed surfaces stay app-agnostic and read against the CONTEXT.md glossary (composition root, authorizer, Feature, Field).
  • Boundary criterion: "the example proves it." A glue surface qualifies for absorption only when the example/ app hand-wrote it AND consumers duplicate it.
  • Deliverable shape: one plan doc per ticket under docs/plans/, written at resolution and linked from the ticket. The map ends when every ticket is closed; the plans are the deliverable, not example code. No toolbelt or example code lands with a ticket — all implementation (toolbelt API changes, dead-helper deletions, shim shrink, example reshape) is deferred to a separate post-map clean rewrite of example/ where the absorbed surfaces land together and challenges are reassessed.
  • Skills per ticket: HITL → /grilling + /domain-modeling (use /prototype when a concrete API shape helps); AFK → /research subagent on a throwaway research/<name> branch.
  • Footguns: ListObjects type:id folds into the authz-absorption ticket; sqlc types:many was resolved as a research ticket (see Decisions).
  • Known duplicated surfaces (survey, 2026-08-06): authn (1,658 lines ×2), config reexport (~200), cobra bootstrap (~650–1,000), authz wrapper (~140–160), embed shims (69), feature/web (63), test framework (186), tools/download (138).

Decisions so far

  • Research: can the cobra bootstrap boilerplate be reduced without losing cobra API power? — Absorb is viable (~86% of package cmd); a toolbelt/cmd parent-command factory preserves the raw cobra API. Graduated into a design ticket.
  • Research: sqlc-gen-zombiezen types:many — fixable or workaround? — Not a plugin bug: types:many is not a sqlc annotation; use sqlc.slice('ids'); no fork, no workaround; the only affected query is consumer-side (go-template).
  • Absorb the authz wrapper — and settle the ListObjects type:id footgun — New toolbelt/authz policy package; type:id strip in Scope; ScopeFunc + ListRelations + Enforce absorbed; user refs codegen-owned; no write interface (rides embeddedfga.Server); narrow Backend interface. Deliverable (plan + example reshape) taken to a new session.
  • Absorb the config reexport layer into toolbelt/config — Value bootstrap: config.New(Options{...}) instance-owned object (globals Fields/ConfigEnvPrefix/AppName/AppVersion deleted; funcs become methods); KCL single source of truth, kcl-generate emits registry + bootstrap (var App = config.New(...)), internal/config 100% generated; BuildLogger moves to toolbelt/app; cfg.Validate() absorbs KCL validation; cfg.Fields() exposes completion data for toolbelt/cmd. Plan: docs/plans/config-bootstrap.md; example reshape + consumer migration deferred (example reshape in a later session). Service-owned defaults graduated to #12.
  • Absorb the composition-root Config/BuildLogger bootstrap into toolbelt/apptoolbelt/app owns the whole bootstrap: New(ctx, cfg *config.Config, opts) reads platform fields internally and builds the logger (exported BuildLogger(level, format)); the startup summary moves into New; Component.Config becomes the *config.Config object. Features self-populate at Registerauthn.New(opts) loses its six OIDC fields + logger arg and reads c.Config/c.Logger; app.go shrinks to NewKanbanApp(ctx, config.App) + feature wiring. Flat Config struct, NewConfig(), and all five BuildLogger copies die. Plan: docs/plans/app-bootstrap.md; example reshape deferred with the config ticket. Field ownership coordinates with #12.
  • Per-package KCL config fragments — service-owned defaults (db, embeddednats, embeddedfga) — Landed as commit f3cbcd0 (sub-tickets #15–#18 under parent spec #14): each service package ships its own schema.k fragment (db/, embeddednats/, embeddedfga/, app/); kcl-generate resolves fragments via kcl.mod (shared kclmod package; -universal flag removed) instead of string-merging a universal schema; apps compose by native import under canonical keys (schema Config(app.Config)); Go const defaults dropped (embeddednats.SetupNATS takes the store dir positionally); ttrpg consumer break recorded. Lessons: directive correlation became file-aware for multi-file merges; kcl.Run must receive path deps as explicit external packages.
  • Design toolbelt/cmd — the shared cobra bootstrap surface — Option B parent-command factory targeting the #3/#4 instance shape: NewRootCommand(cfg, RootOptions{Short, Long, PreInit}) (Use/Version from cfg; validation structural via generated cfg.Validate(), no callback), AddConfigCommand(cfg, root) full group (KCL-aware by default: blank-import config/kcl, .k gate; no save callback), Pager(), Serve(cfg, ServeOptions{Options app.Options}) (logger build/sync + startup summary absorbed into app.New per plan #4), embeddedoidc.DevSetup with options. Int -v/-vv/-vvv ladder only (ttrpg bool -v not a compat constraint); per-app Execute() stays; factory owns flag storage. Plan: docs/plans/cmd-bootstrap.md; implementation lands with the #3/#4 reshapes. Codegen follow-up graduated to #23.
  • Collapse the byte-identical fga/db embed shims into toolbelt — FGA + DB shims collapse to bare exported embed vars (var ModelFS embed.FS / var MigrationsFS embed.FS); all shim helpers die (ModelFS(), Migrations(), ModDir, EmbeddedModData/EmbeddedModules/ModelFiles, EmbeddedFGAModelFS — usage audit found the latter three dead everywhere). db.NewDatabase wrapper absorbed as a variadic functional-options API in the embeddedfga idiom: appdb.NewDatabase(ctx, dbPath, fsys, opts ...Option) with WithMigrationsDir (default "migrations") + WithLogger. Embed-dir convention codified by option defaults + loud startup failure, not KCL (schemas declare config values, embed dirs are Go-source layout — nothing to validate). Plan: docs/plans/embed-shims.md; no code lands (deferred to the post-map example rewrite).
  • HATEOAS actions pattern — show only actions the user can take — The can_ convention: a relation starting can_ is a capability action (1:1 with an affordance, reads "can "); owner/member are composition roles, never actions. fga-generate emits Actions() + ActionsForUser() (same set annotated {Rel, Allowed, Reason} from one ListRelations batch), dependency-free — no app-side computeActions, no glue. Action carries no Href/Method (renderer's rel→widget map owns routes); permission-only, state conditionals stay in the renderer (declarative state verification graduated to #24). Authz seam closes #2's open point: authz.ListRelations hides raw openfgav1; generated RelationLister stays dependency-free. Read model = per-board KV buckets, actions recompute per render. Board model grows per-action relations, seeded to show granularity. Plan: docs/plans/hateoas-actions.md; implementation deferred to the post-map rewrite.

Not yet specified

  • Whether the sqlc sqlc.slice guidance needs codifying in the toolbelt (docs/example) even though no toolbelt example uses IN today.
  • The sqlc-gen-zombiezen reproducibility gap (unpinned plugin) and two unrelated upstream plugin bugs surfaced by the sqlc research — candidate task, not yet ticketed.
  • Whether the OpenAPI spec (oapi tool) can supply the rel→route half of HATEOAS actions, keyed by convention to can_ relations — the linking convention (which operation ↔ which relation) is unsettled.
  • Any new duplicated glue the resolutions expose inside example/ (graduate on discovery).

Out of scope

  • Consumer migration (go-template, ttrpg adopting the absorbed surfaces) — separate later project.
  • feature/web scaffolding (byte-identical home-page boilerplate) — app-shaped, ruled out.
  • test/framework (byte-identical auth/server test helpers) — app tooling, ruled out.
  • tools/download absorption — the separate exploration resolved keep-per-consumer (fails the example-proves-it boundary; manifest is app-shaped); its real defect is a broken invocation surface, a consumer-side hygiene fix.
  • Swappable backends — external OpenFGA/NATS/DB drivers (issue #10) — a platform architecture effort (microlith → connected services), not absorption of duplicated consumer glue; tracked separately.
  • ttrpg copy-paste hygiene (wrong module name, self-imports, stale GO_TEMPLATE prefix; stale fork of go-template) — a consumer bug, not an absorption decision.
  • Config ResolveDBPath/ResolveTupleFilePath (config reexport ticket) — fails the example-proves-it boundary (not hand-written in the example); toolbelt/db.NewDatabase already resolves paths via filepath.Abs.
  • The example/ clean rewrite (reshaping the app onto all absorbed surfaces) — a separate post-map effort, not part of this map; it is where every ticket's deferred implementation lands together and the challenges are reassessed.
## Destination A decision/implementation spec for what the toolbelt absorbs from the duplicated consumer glue — delivered as a `docs/plans/` collection, one plan per ticket. Consumers (go-template, ttrpg) stay out of scope until the map is done. The map ends when every ticket is closed and the plans are written; reshaping the `example/` app onto the absorbed surfaces is deliberately deferred to a separate post-map clean rewrite where the challenges are reassessed against every surface at once. ## Notes - **Domain:** toolbelt platform glue; absorbed surfaces stay app-agnostic and read against the `CONTEXT.md` glossary (composition root, authorizer, Feature, Field). - **Boundary criterion: "the example proves it."** A glue surface qualifies for absorption only when the `example/` app hand-wrote it AND consumers duplicate it. - **Deliverable shape:** one plan doc per ticket under `docs/plans/`, written at resolution and linked from the ticket. The map ends when every ticket is closed; the plans are the deliverable, not example code. **No toolbelt or example code lands with a ticket** — all implementation (toolbelt API changes, dead-helper deletions, shim shrink, example reshape) is deferred to a separate post-map clean rewrite of `example/` where the absorbed surfaces land together and challenges are reassessed. - **Skills per ticket:** HITL → `/grilling` + `/domain-modeling` (use `/prototype` when a concrete API shape helps); AFK → `/research` subagent on a throwaway `research/<name>` branch. - **Footguns:** `ListObjects` `type:id` folds into the authz-absorption ticket; sqlc `types:many` was resolved as a research ticket (see Decisions). - Known duplicated surfaces (survey, 2026-08-06): authn (1,658 lines ×2), config reexport (~200), cobra bootstrap (~650–1,000), authz wrapper (~140–160), embed shims (69), feature/web (63), test framework (186), tools/download (138). ## Decisions so far - [Research: can the cobra bootstrap boilerplate be reduced without losing cobra API power?](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/6) — Absorb is viable (~86% of `package cmd`); a `toolbelt/cmd` parent-command factory preserves the raw cobra API. Graduated into a design ticket. - [Research: sqlc-gen-zombiezen types:many — fixable or workaround?](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/7) — Not a plugin bug: `types:many` is not a sqlc annotation; use `sqlc.slice('ids')`; no fork, no workaround; the only affected query is consumer-side (go-template). - [Absorb the authz wrapper — and settle the ListObjects type:id footgun](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/2) — New `toolbelt/authz` policy package; `type:id` strip in `Scope`; `ScopeFunc` + `ListRelations` + `Enforce` absorbed; user refs codegen-owned; no write interface (rides `embeddedfga.Server`); narrow `Backend` interface. Deliverable (plan + example reshape) taken to a new session. - [Absorb the config reexport layer into toolbelt/config](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/3) — Value bootstrap: `config.New(Options{...})` instance-owned object (globals `Fields`/`ConfigEnvPrefix`/`AppName`/`AppVersion` deleted; funcs become methods); KCL single source of truth, `kcl-generate` emits registry + bootstrap (`var App = config.New(...)`), `internal/config` 100% generated; `BuildLogger` moves to `toolbelt/app`; `cfg.Validate()` absorbs KCL validation; `cfg.Fields()` exposes completion data for `toolbelt/cmd`. Plan: `docs/plans/config-bootstrap.md`; example reshape + consumer migration deferred (example reshape in a later session). Service-owned defaults graduated to [#12](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/12). - [Absorb the composition-root Config/BuildLogger bootstrap into toolbelt/app](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/4) — `toolbelt/app` owns the whole bootstrap: `New(ctx, cfg *config.Config, opts)` reads platform fields internally and builds the logger (exported `BuildLogger(level, format)`); the startup summary moves into `New`; `Component.Config` becomes the `*config.Config` object. Features self-populate at `Register` — `authn.New(opts)` loses its six OIDC fields + logger arg and reads `c.Config`/`c.Logger`; `app.go` shrinks to `NewKanbanApp(ctx, config.App)` + feature wiring. Flat `Config` struct, `NewConfig()`, and all five `BuildLogger` copies die. Plan: `docs/plans/app-bootstrap.md`; example reshape deferred with the config ticket. Field ownership coordinates with [#12](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/12). - [Per-package KCL config fragments — service-owned defaults (db, embeddednats, embeddedfga)](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/12) — Landed as commit `f3cbcd0` (sub-tickets #15–#18 under parent spec #14): each service package ships its own `schema.k` fragment (`db/`, `embeddednats/`, `embeddedfga/`, `app/`); `kcl-generate` resolves fragments via `kcl.mod` (shared `kclmod` package; `-universal` flag removed) instead of string-merging a universal schema; apps compose by native import under canonical keys (`schema Config(app.Config)`); Go const defaults dropped (`embeddednats.SetupNATS` takes the store dir positionally); ttrpg consumer break recorded. Lessons: directive correlation became file-aware for multi-file merges; `kcl.Run` must receive path deps as explicit external packages. - [Design toolbelt/cmd — the shared cobra bootstrap surface](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/9) — Option B parent-command factory targeting the #3/#4 instance shape: `NewRootCommand(cfg, RootOptions{Short, Long, PreInit})` (Use/Version from cfg; validation structural via generated `cfg.Validate()`, no callback), `AddConfigCommand(cfg, root)` full group (KCL-aware by default: blank-import `config/kcl`, `.k` gate; no save callback), `Pager()`, `Serve(cfg, ServeOptions{Options app.Options})` (logger build/sync + startup summary absorbed into `app.New` per plan #4), `embeddedoidc.DevSetup` with options. Int `-v/-vv/-vvv` ladder only (ttrpg bool `-v` not a compat constraint); per-app `Execute()` stays; factory owns flag storage. Plan: `docs/plans/cmd-bootstrap.md`; implementation lands with the #3/#4 reshapes. Codegen follow-up graduated to [#23](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/23). - [Collapse the byte-identical fga/db embed shims into toolbelt](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/5) — FGA + DB shims collapse to bare exported embed vars (`var ModelFS embed.FS` / `var MigrationsFS embed.FS`); all shim helpers die (`ModelFS()`, `Migrations()`, `ModDir`, `EmbeddedModData`/`EmbeddedModules`/`ModelFiles`, `EmbeddedFGAModelFS` — usage audit found the latter three dead everywhere). `db.NewDatabase` wrapper absorbed as a variadic functional-options API in the `embeddedfga` idiom: `appdb.NewDatabase(ctx, dbPath, fsys, opts ...Option)` with `WithMigrationsDir` (default `"migrations"`) + `WithLogger`. Embed-dir convention codified by option defaults + loud startup failure, not KCL (schemas declare config values, embed dirs are Go-source layout — nothing to validate). Plan: `docs/plans/embed-shims.md`; no code lands (deferred to the post-map example rewrite). - [HATEOAS actions pattern — show only actions the user can take](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/11) — The `can_` convention: a relation starting `can_` is a capability action (1:1 with an affordance, reads "can <verb> <noun>"); `owner`/`member` are composition roles, never actions. `fga-generate` emits `Actions()` + `ActionsForUser()` (same set annotated `{Rel, Allowed, Reason}` from one `ListRelations` batch), dependency-free — no app-side computeActions, no glue. `Action` carries no Href/Method (renderer's rel→widget map owns routes); **permission-only**, state conditionals stay in the renderer (declarative state verification graduated to [#24](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/24)). Authz seam closes #2's open point: `authz.ListRelations` hides raw `openfgav1`; generated `RelationLister` stays dependency-free. Read model = per-board KV buckets, actions recompute per render. Board model grows per-action relations, seeded to show granularity. Plan: `docs/plans/hateoas-actions.md`; implementation deferred to the post-map rewrite. ## Not yet specified - Whether the sqlc `sqlc.slice` guidance needs codifying in the toolbelt (docs/example) even though no toolbelt example uses `IN` today. - The `sqlc-gen-zombiezen` reproducibility gap (unpinned plugin) and two unrelated upstream plugin bugs surfaced by the sqlc research — candidate task, not yet ticketed. - Whether the OpenAPI spec (oapi tool) can supply the rel→route half of HATEOAS actions, keyed by convention to `can_` relations — the linking convention (which operation ↔ which relation) is unsettled. - Any new duplicated glue the resolutions expose inside `example/` (graduate on discovery). ## Out of scope - **Consumer migration** (go-template, ttrpg adopting the absorbed surfaces) — separate later project. - **feature/web** scaffolding (byte-identical home-page boilerplate) — app-shaped, ruled out. - **test/framework** (byte-identical auth/server test helpers) — app tooling, ruled out. - **tools/download absorption** — the separate exploration resolved keep-per-consumer (fails the example-proves-it boundary; manifest is app-shaped); its real defect is a broken invocation surface, a consumer-side hygiene fix. - **Swappable backends — external OpenFGA/NATS/DB drivers** ([issue #10](https://git.animeteamspeak.moe/ginjiruu/toolbelt/issues/10)) — a platform architecture effort (microlith → connected services), not absorption of duplicated consumer glue; tracked separately. - **ttrpg copy-paste hygiene** (wrong module name, self-imports, stale `GO_TEMPLATE` prefix; stale fork of go-template) — a consumer bug, not an absorption decision. - **Config `ResolveDBPath`/`ResolveTupleFilePath`** (config reexport ticket) — fails the example-proves-it boundary (not hand-written in the example); `toolbelt/db.NewDatabase` already resolves paths via `filepath.Abs`. - **The `example/` clean rewrite** (reshaping the app onto all absorbed surfaces) — a separate post-map effort, not part of this map; it is where every ticket's deferred implementation lands together and the challenges are reassessed.
Sign in to join this conversation.
No milestone
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.

Dependencies

No dependencies set.

Reference
ginjiruu/toolbelt#1
No description provided.