Record public facade boundary
This commit is contained in:
61
docs/adr/0006-public-facade.md
Normal file
61
docs/adr/0006-public-facade.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# ADR 0006: Keep the public facade root-owned
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Accepted. This records a design decision, not implementation completion.
|
||||||
|
|
||||||
|
## Date
|
||||||
|
|
||||||
|
2026-09-12
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The root `configkit` package currently combines the supported consumer API with
|
||||||
|
the loading, decoding, inspection, and rendering machinery. That makes ordinary
|
||||||
|
consumer navigation harder and risks exposing implementation details through
|
||||||
|
future public declarations. Moving files alone would not preserve the identity,
|
||||||
|
method sets, callbacks, ownership rules, or reflection-sensitive behavior of
|
||||||
|
the root's public types.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
Keep `configkit` as the sole supported consumer import path and keep all public
|
||||||
|
named types defined there. When lifecycle machinery is extracted, it will live
|
||||||
|
in a cohesive `internal/engine`; the root facade may hold private engine state
|
||||||
|
and forward deliberately selected methods. The dependency direction is root to
|
||||||
|
engine and its supporting internal packages. Internal production packages do
|
||||||
|
not import the root package.
|
||||||
|
|
||||||
|
The facade will use root-owned wrappers and explicit private conversions. It
|
||||||
|
will not use public aliases to engine types, anonymous embedding that promotes
|
||||||
|
engine methods, public engine types in signatures or fields, or public bridge
|
||||||
|
methods. Existing document and YAML boundaries remain intact.
|
||||||
|
|
||||||
|
The migration may use a temporary internal candidate facade while the unchanged
|
||||||
|
root remains the only production entry point. That duplication is a bounded
|
||||||
|
relocation mechanism, not a second supported package, feature path, or runtime
|
||||||
|
selection mechanism. The final cutover removes the temporary candidate and old
|
||||||
|
root implementation together.
|
||||||
|
|
||||||
|
The [architecture policy](../policy/architecture.md#public-api-and-dependency-direction)
|
||||||
|
owns the binding boundary. The [public-facade roadmap](../roadmap/public.md)
|
||||||
|
owns the intended end state and its implementation plan owns progress.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- Alias root types to engine types: reduces forwarding, but makes the engine's
|
||||||
|
exported method set and type ownership part of the consumer API.
|
||||||
|
- Keep all machinery in the root package: avoids migration work, but leaves the
|
||||||
|
public boundary difficult to navigate and easy to expand accidentally.
|
||||||
|
- Introduce a generic adapter or registration framework: could reduce some
|
||||||
|
handwritten conversions, but adds machinery and obscures the ownership rules
|
||||||
|
this boundary needs to preserve.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
The refactor carries explicit forwarding and conversion work, especially for
|
||||||
|
callbacks, detached records, source ownership, frozen inspection, and the
|
||||||
|
exact-type secret bridge. Public compatibility must be verified by behavior and
|
||||||
|
external-package evidence rather than inferred from package names. Internal
|
||||||
|
exports remain implementation surfaces, and the completed architecture will
|
||||||
|
have one engine and one root facade.
|
||||||
@@ -30,6 +30,7 @@ Accepted decisions:
|
|||||||
- [Preserve presence and record application resolution](adr/0003-preserve-presence-and-resolution-history.md)
|
- [Preserve presence and record application resolution](adr/0003-preserve-presence-and-resolution-history.md)
|
||||||
- [Secret references and explicit bootstrap](adr/0004-secret-references-and-explicit-bootstrap.md)
|
- [Secret references and explicit bootstrap](adr/0004-secret-references-and-explicit-bootstrap.md)
|
||||||
- [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.md)
|
- [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.md)
|
||||||
|
- [Keep the public facade root-owned](adr/0006-public-facade.md)
|
||||||
|
|
||||||
For cross-cutting changes, follow every applicable row. Completed implementation
|
For cross-cutting changes, follow every applicable row. Completed implementation
|
||||||
and remediation plans are preserved in Git history at `cb5b435`; use current
|
and remediation plans are preserved in Git history at `cb5b435`; use current
|
||||||
@@ -47,9 +48,10 @@ records are preserved in Git history at `244104f`. The
|
|||||||
[configuration contract](config.md#application-schema-versions) owns current behavior.
|
[configuration contract](config.md#application-schema-versions) owns current behavior.
|
||||||
|
|
||||||
The completed [source-origins during resolution record](roadmap/source-origins-during-resolution.md)
|
The completed [source-origins during resolution record](roadmap/source-origins-during-resolution.md)
|
||||||
links to the current contract and evidence. Its staged implementation handoffs remain
|
links to the current contract and evidence. Its completed implementation handoffs
|
||||||
in [implementation.md](roadmap/implementation.md) until release preparation retires
|
are preserved at `28e7fc5:docs/roadmap/implementation.md`; inspect them with
|
||||||
completed roadmap material.
|
`git show 28e7fc5:docs/roadmap/implementation.md` until release preparation
|
||||||
|
retires the completed roadmap material.
|
||||||
|
|
||||||
## Baseline validation
|
## Baseline validation
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,18 @@ Do not expose parser-specific nodes, errors, or extension hooks through the
|
|||||||
public API. Parser integration belongs behind a private boundary, as recorded
|
public API. Parser integration belongs behind a private boundary, as recorded
|
||||||
in [ADR 0002](../adr/0002-own-yaml-semantics.md).
|
in [ADR 0002](../adr/0002-own-yaml-semantics.md).
|
||||||
|
|
||||||
|
The root `configkit` package is the sole supported consumer import path. Public
|
||||||
|
named types stay root-owned and may hold private delegated implementation state;
|
||||||
|
their exported signatures, fields, and method sets must not expose internal
|
||||||
|
types or acquire internal methods through anonymous embedding. Root production
|
||||||
|
code may depend inward on a cohesive engine and its supporting internal packages,
|
||||||
|
but internal production packages must not import the root. Explicit forwarding
|
||||||
|
and local conversion are acceptable where they preserve type identity,
|
||||||
|
ownership, callback lifetime, and inspection behavior. The accepted
|
||||||
|
root-wrapper boundary and its migration rationale are recorded in
|
||||||
|
[ADR 0006](../adr/0006-public-facade.md); its implementation remains tracked by
|
||||||
|
the public-facade roadmap.
|
||||||
|
|
||||||
Separate input access, document processing, composition, decoding, metadata, and
|
Separate input access, document processing, composition, decoding, metadata, and
|
||||||
rendering responsibilities without prescribing an interface or package for each.
|
rendering responsibilities without prescribing an interface or package for each.
|
||||||
Dependencies must preserve those boundaries; package splits and abstractions
|
Dependencies must preserve those boundaries; package splits and abstractions
|
||||||
|
|||||||
@@ -181,6 +181,35 @@ distinguish decision from implementation, and documentation links pass.
|
|||||||
Run `go test . -run 'LocalDocumentationLinks|DependencyImportBoundary|Example|Facade'`
|
Run `go test . -run 'LocalDocumentationLinks|DependencyImportBoundary|Example|Facade'`
|
||||||
and the everyday check. No engine migration in this stage.
|
and the everyday check. No engine migration in this stage.
|
||||||
|
|
||||||
|
### Completion
|
||||||
|
|
||||||
|
Status: complete (2026-09-12).
|
||||||
|
|
||||||
|
Baseline behavior was verified at `28e7fc5f690b36c60e4d03586afcff00fb1dee90`;
|
||||||
|
this plan was introduced by `eb7de8e40ab944f022a2b6984f4d4e0086f1484d`. The
|
||||||
|
complete root surface was inventoried with `go doc -all .`: lifecycle and generic
|
||||||
|
entry points; root value/input types (`Path`, `Source`, `Secret`); callback and
|
||||||
|
option records; metadata, provenance, diagnostics, history, and rendering
|
||||||
|
records; typed errors and bootstrap reports; and the scalar enum families. The
|
||||||
|
inventory also covered receiver methods, generic instantiations, and relevant
|
||||||
|
interfaces (`error`, `fmt.Formatter`, `fmt.Stringer`, `json.Marshaler`, and
|
||||||
|
`io.WriterTo`). Existing behavioral tests remain the source of detailed API
|
||||||
|
semantics; `facade_compatibility_test.go` supplies compact external-package
|
||||||
|
coverage for the migration boundary.
|
||||||
|
|
||||||
|
Added ADR 0006 and the architecture rule for root-owned public wrappers and
|
||||||
|
inward-only implementation dependencies. They describe the accepted target, not
|
||||||
|
an already-completed extraction. Repaired the development guide, source-origins
|
||||||
|
record, and remediation record to identify the old provenance plan by its Git
|
||||||
|
revision instead of linking to this plan. No engine or lifecycle migration was
|
||||||
|
introduced.
|
||||||
|
|
||||||
|
Validation on Linux with Go 1.26.7 passed: the focused documentation,
|
||||||
|
import-boundary, executable-example, and facade checks, followed by
|
||||||
|
`./scripts/check.sh`. There were no deviations. The next handoff copies the
|
||||||
|
unchanged root implementation into the bounded internal relocation candidate;
|
||||||
|
it must retain the documented public behavior while proving engine independence.
|
||||||
|
|
||||||
## Stage 2 — Establish the internal relocation candidate
|
## Stage 2 — Establish the internal relocation candidate
|
||||||
|
|
||||||
Status: not started.
|
Status: not started.
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ displaced/default paths that the view cannot query. This adds allocation and cop
|
|||||||
work proportional to retained history even for a resolver that never calls the
|
work proportional to retained history even for a resolver that never calls the
|
||||||
new methods. No wall-clock or memory threshold was measured or is asserted here.
|
new methods. No wall-clock or memory threshold was measured or is asserted here.
|
||||||
|
|
||||||
The Stage 5 completion record in [implementation.md](implementation.md) also states
|
The historical completion record at
|
||||||
that the view "does not retain history"; the constructor contradicts that claim.
|
`28e7fc5:docs/roadmap/implementation.md` also states that the view "does not
|
||||||
|
retain history"; the constructor contradicts that claim.
|
||||||
|
|
||||||
### Correction
|
### Correction
|
||||||
|
|
||||||
|
|||||||
@@ -38,11 +38,13 @@ confinement check.
|
|||||||
|
|
||||||
## Evidence and handoff
|
## Evidence and handoff
|
||||||
|
|
||||||
The completed staged record in [implementation.md](implementation.md) maps all
|
The completed staged record is preserved at
|
||||||
twelve acceptance criteria to focused lifecycle, provenance, ownership, consumer,
|
`28e7fc5:docs/roadmap/implementation.md`; retrieve it with
|
||||||
explanation, and secret tests. The final implementation commit is expected to run
|
`git show 28e7fc5:docs/roadmap/implementation.md`. It maps all twelve acceptance
|
||||||
the source checker and the documented two-toolchain gate before any release
|
criteria to focused lifecycle, provenance, ownership, consumer, explanation, and
|
||||||
candidate is selected.
|
secret tests. The final implementation commit is expected to run the source
|
||||||
|
checker and the documented two-toolchain gate before any release candidate is
|
||||||
|
selected.
|
||||||
|
|
||||||
Publication remains a maintainer decision. Release preparation must choose the
|
Publication remains a maintainer decision. Release preparation must choose the
|
||||||
version, write a version-matched note, retire this completed roadmap with link
|
version, write a version-matched note, retire this completed roadmap with link
|
||||||
|
|||||||
85
facade_compatibility_test.go
Normal file
85
facade_compatibility_test.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package configkit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ error = (*configkit.LoadError)(nil)
|
||||||
|
_ fmt.Formatter = configkit.Secret{}
|
||||||
|
_ fmt.Stringer = configkit.Path{}
|
||||||
|
_ json.Marshaler = configkit.Secret{}
|
||||||
|
_ io.WriterTo = (*configkit.SecretsReport)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
type facadeCompatibilityConfig struct {
|
||||||
|
Value string `yaml:"value"`
|
||||||
|
Derived string `yaml:"derived"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFacadeCompatibility(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const packagePath = "gitea.maximumdirect.net/eric/ConfigKit"
|
||||||
|
for _, candidate := range []struct {
|
||||||
|
name string
|
||||||
|
type_ reflect.Type
|
||||||
|
}{
|
||||||
|
{"Path", reflect.TypeFor[configkit.Path]()},
|
||||||
|
{"Source", reflect.TypeFor[configkit.Source]()},
|
||||||
|
{"Secret", reflect.TypeFor[configkit.Secret]()},
|
||||||
|
{"Definition", reflect.TypeFor[configkit.Definition[facadeCompatibilityConfig]]()},
|
||||||
|
{"Options", reflect.TypeFor[configkit.Options[facadeCompatibilityConfig]]()},
|
||||||
|
{"Result", reflect.TypeFor[configkit.Result[facadeCompatibilityConfig]]()},
|
||||||
|
} {
|
||||||
|
if candidate.type_.PkgPath() != packagePath {
|
||||||
|
t.Errorf("%s belongs to %q, want %q", candidate.name, candidate.type_.PkgPath(), packagePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
constructed := configkit.Options[facadeCompatibilityConfig]{
|
||||||
|
Definition: configkit.Definition[facadeCompatibilityConfig]{},
|
||||||
|
Filesystem: &configkit.FilesystemContext{BaseDir: "configs", Root: "configs"},
|
||||||
|
Overrides: []configkit.Override{{
|
||||||
|
Path: configkit.Keys("value"),
|
||||||
|
Value: "override",
|
||||||
|
Flag: "--value",
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
if got := constructed.Overrides[0].Path.String(); got != "/value" {
|
||||||
|
t.Fatalf("constructed override path = %q, want /value", got)
|
||||||
|
}
|
||||||
|
publicResult := configkit.Result[facadeCompatibilityConfig]{Config: facadeCompatibilityConfig{Value: "constructed"}}
|
||||||
|
if publicResult.Config.Value != "constructed" {
|
||||||
|
t.Fatal("constructed result lost its public configuration")
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := configkit.Load(configkit.Bytes("facade.yml", []byte("value: input\n")), configkit.Options[facadeCompatibilityConfig]{
|
||||||
|
Definition: configkit.Definition[facadeCompatibilityConfig]{
|
||||||
|
Resolve: func(configuration *facadeCompatibilityConfig, resolution *configkit.Resolution) error {
|
||||||
|
configkit.Default(resolution, configkit.Keys("derived"), &configuration.Derived, configuration.Value+" default", "derive value")
|
||||||
|
configkit.Normalize(resolution, configkit.Keys("value"), &configuration.Value, "normalized "+configuration.Value, "normalize value")
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Load() error = %v", err)
|
||||||
|
}
|
||||||
|
if result.Config != (facadeCompatibilityConfig{Value: "normalized input", Derived: "input default"}) {
|
||||||
|
t.Fatalf("Load() Config = %#v", result.Config)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = configkit.Load[facadeCompatibilityConfig](configkit.Bytes("invalid.yml", []byte("value: [\n")))
|
||||||
|
var loadError *configkit.LoadError
|
||||||
|
if !errors.As(err, &loadError) {
|
||||||
|
t.Fatalf("Load() error = %T, want *configkit.LoadError", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user