Skip to content

refactor(capabilities): inline datamodel structs into parent packages#21

Merged
alanshaw merged 1 commit into
mainfrom
frrist/inline-capability-datamodel
May 18, 2026
Merged

refactor(capabilities): inline datamodel structs into parent packages#21
alanshaw merged 1 commit into
mainfrom
frrist/inline-capability-datamodel

Conversation

@frrist
Copy link
Copy Markdown
Member

@frrist frrist commented May 16, 2026

Each capability exposed its wire structs through a parent-package alias to a <cap>/datamodel/ subpackage:

// capabilities/blob/add.go
type AddArguments = bdm.AddArgumentsModel

The split solved a real problem: bindcap.Arguments requires CBOR codec methods, those methods are what cbor-gen produces, and the parent package couldn't compile against itself before codegen ran. Putting the wire structs in a separate package broke that bootstrap cleanly.

The indirection has accumulated costs worth addressing now:

  • The aliases are pure renames (Foo = otherpkg.FooModel), which we've been trying to avoid elsewhere in the codebase.

  • The Model suffix leaks back through struct field declarations. args.Metadata reads as Metadata at the call site, but hover-types and error messages still surface MetadataModel because the field is declared with the suffixed name. A recent piri refactor surfaced a confusing type mismatch where the alias chain took a while to untangle.

  • Each capability duplicates the same datamodel/ boilerplate (a gen/ subfolder, generator entry point, generated codecs).

This change inlines each <cap>/datamodel/ into its parent and drops the rename aliases. The codegen bootstrap is preserved with a build tag: binding files carry //go:build !codegen and the generator runs with -tags codegen, so during codegen only the wire types compile (no bindcap dependency). The generator post-processes its output to re-inject the tag, keeping subsequent runs stale-safe.

The top-level capabilities/datamodel.UnitModel becomes capabilities.Unit. Per-op OK aliases that resolved to UnitModel are kept as within-package aliases (e.g. type AddOK = capabilities.Unit) to preserve operation-named labels at type-parameter call sites like bindexec.Response[*blob.AddOK].

Additionally, capabilities.MustNew wraps bindcap.New and panics on construction error. The previous var Foo, _ = bindcap.New(...) pattern silently dropped errors that, for hardcoded commands and options, can only ever be programmer bugs — failing at package init is strictly safer than discovering it later via nil dereference.

External impact: only the sprue repo imports an inner datamodel package directly (two test files using
access/datamodel.CapabilityRequestModel). Those need to update alongside the libforge bump — change the import path and drop the Model suffix.

Each capability exposed its wire structs through a parent-package alias
to a `<cap>/datamodel/` subpackage:

    // capabilities/blob/add.go
    type AddArguments = bdm.AddArgumentsModel

The split solved a real problem: `bindcap.Arguments` requires CBOR codec
methods, those methods are what `cbor-gen` produces, and the parent
package couldn't compile against itself before codegen ran. Putting the
wire structs in a separate package broke that bootstrap cleanly.

The indirection has accumulated costs worth addressing now:

  - The aliases are pure renames (Foo = otherpkg.FooModel), which we've
    been trying to avoid elsewhere in the codebase.

  - The `Model` suffix leaks back through struct field declarations.
    `args.Metadata` reads as `Metadata` at the call site, but hover-types
    and error messages still surface `MetadataModel` because the field
    is declared with the suffixed name. A recent piri refactor surfaced
    a confusing type mismatch where the alias chain took a while to
    untangle.

  - Each capability duplicates the same `datamodel/` boilerplate (a
    `gen/` subfolder, generator entry point, generated codecs).

This change inlines each `<cap>/datamodel/` into its parent and drops
the rename aliases. The codegen bootstrap is preserved with a build
tag: binding files carry `//go:build !codegen` and the generator runs
with `-tags codegen`, so during codegen only the wire types compile
(no bindcap dependency). The generator post-processes its output to
re-inject the tag, keeping subsequent runs stale-safe.

The top-level `capabilities/datamodel.UnitModel` becomes
`capabilities.Unit`. Per-op `OK` aliases that resolved to `UnitModel`
are kept as within-package aliases (e.g. `type AddOK = capabilities.Unit`)
to preserve operation-named labels at type-parameter call sites like
`bindexec.Response[*blob.AddOK]`.

Additionally, `capabilities.MustNew` wraps `bindcap.New` and panics on
construction error. The previous `var Foo, _ = bindcap.New(...)` pattern
silently dropped errors that, for hardcoded commands and options, can
only ever be programmer bugs — failing at package init is strictly
safer than discovering it later via nil dereference.

External impact: only the sprue repo imports an inner datamodel package
directly (two test files using
`access/datamodel.CapabilityRequestModel`). Those need to update
alongside the libforge bump — change the import path and drop the
`Model` suffix.

The pdp/ tree gets the same treatment in a follow-up branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@frrist frrist requested a review from alanshaw May 16, 2026 00:04
@frrist frrist self-assigned this May 16, 2026
@alanshaw alanshaw merged commit e5cc3a0 into main May 18, 2026
4 checks passed
@alanshaw alanshaw deleted the frrist/inline-capability-datamodel branch May 18, 2026 09:40
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.

2 participants