Files
ConfigKit/docs/roadmap/public.md

188 lines
10 KiB
Markdown

# Public facade and internal implementation
Status: agreed direction; not implemented. This roadmap defines the desired end
state and user intent. The [implementation plan](implementation.md) owns the
ordered work, concrete adapter design, validation commands, and handoffs.
## Intent
Make ConfigKit easier for humans and coding agents to navigate by separating its
consumer-facing API from its implementation machinery. Consumers should find a
readable, documented public surface in the root `configkit` package; maintainers
should find the loading and inspection machinery under `internal/`.
Long-term maintainability is the primary objective. The public API should remain
stable while internal implementation can evolve without becoming a consumer
commitment. An internal implementation change must not expand or alter the public
API merely because the facade exposes the same underlying type. Repository
aesthetics and easier navigation are useful secondary benefits, not the reason
to minimize public-package code.
This is an organizational and API-boundary refactor, not a new configuration
feature or security boundary. Unexported root-package details are already private
under ordinary Go access rules. The concern is accidentally exposing internal
types or exported methods that consumers can depend on even without documentation.
Wrappers provide structural separation; they do not make internal changes that
alter observable public behavior automatically compatible.
The [architecture policy](../policy/architecture.md) and
[implementation discipline](../../AGENTS.md#implementation-discipline) govern the
design. Existing [configuration contracts](../config.md) and exported Go
declaration comments remain authoritative. The user-approved wrapper boundary is
a contributor design choice, not a strengthening of consumer behavior contracts.
## Current architecture
The root currently owns both public declarations and most private implementation:
source acquisition, imports, composition and overlays, reflection descriptors,
decoding, resolution and validation coordination, provenance, history, secret
handling, and rendering. The [component overview](../internal/overview.md) owns
the detailed inventory.
Two established internal boundaries already exist: `internal/document` owns the
parser-independent tree, and `internal/yaml` owns upstream YAML integration.
[ADR 0002](../adr/0002-own-yaml-semantics.md) remains applicable.
Public types are not merely passive records. For example, `Result[T]` combines
its public `Config` field with private inspection snapshots; `Resolution` holds
callback state; `Path` holds semantic segments; and `Secret` holds private
reference and resolved-value state. Moving implementation requires deliberate
type ownership, not just file relocation.
## Target architecture
### Public root package
The root remains the single supported consumer import path. It contains:
- package and API reference documentation;
- public-owned type declarations, including wrappers and ordinary public records;
- public constants, constructors, entry points, and lightweight delegation;
- public contract tests and executable API examples where appropriate.
Substantial source processing, decoding, lifecycle bookkeeping, history replay,
and rendering mechanics live internally. Small value operations or adapters may
remain in the root when moving them would introduce disproportionate machinery.
Public API documentation remains beside public declarations and must be useful
through ordinary Go documentation and editor navigation.
### Public-owned types and private delegation
The agreed approach is root-defined public types, with private delegation to
internal implementation where needed. Public wrappers expose deliberately chosen
methods; internal state is held privately rather than exported or embedded in a
way that promotes internal methods into the public surface. Public signatures
and fields do not expose internal plumbing types. Adding an internal exported
method must not add a public method automatically.
Simple public records and small value operations can remain directly implemented
in the root. There is no requirement to wrap every value or move every method.
Aliases to internally owned implementation types are not the facade strategy:
they would couple the public method set to the implementation this work is meant
to separate.
Straightforward forwarding code is an acceptable maintenance cost. Adapters must
avoid competing mutable state and unnecessary conversions, particularly around
`Result[T].Config`, frozen inspection data, callback lifetime, source ownership,
and reflection-sensitive secret handling. These connections are private
implementation details, not public bridge APIs or a generic registration framework.
### Internal engine and supporting packages
A cohesive internal engine owns the loading lifecycle and the closely coupled
schema, decoding, resolution, provenance, and inspection implementation. The
selected package is `internal/engine`.
The existing document and YAML packages retain their responsibilities. Additional
packages are appropriate only where a cohesive responsibility and practical
dependency boundary justify them. There is no requirement for one package per
phase, file category, or component in the current inventory.
Dependencies run from the facade into internal implementation and from the
engine into its supporting packages. Internal packages do not import the public
root. Public types remain root-owned; internal collaborators use internally owned
state and narrow implementation connections. Internal code does not gain direct
access to private fields of public types across packages.
The engine may initially remain fairly large. A smaller root alone does not
justify a network of adapters, generic provider interfaces, or a framework.
Internal exports needed between packages are implementation surfaces, not new
supported consumer APIs. Existing parser isolation remains intact.
## Compatibility and scope
The target is a behavior-preserving refactor requiring no consumer migration:
- Preserve the module/import path, exported names, function and method
signatures, generic use, public fields, and supported construction patterns.
- Preserve loading order, source ownership, defaults, composition and overlay
behavior, resolution and validation semantics, and structured diagnostics.
- Preserve callback lifetime, fixed entry provenance, detached query results,
final inspection snapshots, and independence across loads.
- Preserve secret recognition, credential timing, explicit access, redaction,
and opt-in bootstrap behavior.
- Preserve the supported Go and platform baseline and the existing dependency
isolation; this work does not require a toolchain or third-party dependency
change.
Type restructuring can affect more than compilation. The implementation must
assess method sets, interface satisfaction, zero values, copying and equality
where supported, error assertions, reflection-based special-type recognition,
and public documentation. Keeping public named types in the root preserves their
defining package identity, but does not by itself prove compatibility of their
state or behavior. Observable effects must be evaluated and reported, not silently
declared compatible or newly guaranteed forever.
No new input restrictions, resource limits, lifecycle restrictions, security
guarantees, configuration semantics, or public extension hooks are in scope.
Do not expose plumbing publicly merely to solve an import cycle. A discovered
need to change an established contract requires a separate explicit decision.
Version selection and publication remain governed by the
[release policy](../policy/release.md), not by this roadmap.
## Completion criteria
Completion means the root is a legible public surface, substantive plumbing is
internally owned, consumers require no migration, and representative
load-to-inspection workflows still pass through the facade. Public exported
members are deliberate declarations, not incidental exposure of engine methods
or state. The completed architecture has one engine, no parallel implementation,
and no competing mutable configuration state. Small operations deliberately
retained in the root are justified by maintainability, not a numerical size gate.
Public behavior and maintained consumer examples exercise the root facade;
mechanism tests live with their implementation owners. Test-only access does not
expand the public API. Repository checks preserve parser isolation and the public
boundary. Validation follows the [testing policy](../policy/testing.md);
release-candidate validation remains separate under the
[release procedure](../release.md).
Tests should protect behavior and intentional dependency boundaries, not exact
private layouts, forwarding call sequences, or an arbitrary package count.
Implementation completion must be backed by evidence; this proposal is not
evidence that the target architecture already exists. Completed roadmap material
is retired during release preparation under the documentation policy.
## Documentation ownership in the target architecture
Under the [documentation policy](../policy/documentation.md), architecture policy
owns the public-wrapper and inward-dependency rules, preserving proportionality.
An ADR records the accepted decision and why an alias-first facade was rejected:
it reduces forwarding but couples public methods to internal implementation.
The additional wrapper code is justified by long-term API independence.
The component overview describes actual facade, engine, supporting packages, and
state ownership. Development navigation locates the tests and mechanisms at their
new owners. Internal documentation explains the private adapters. Exported API
reference comments remain in the public package; consumer guidance identifies
the supported root import without requiring knowledge of the engine.
Configuration semantics remain in the existing contract, not in package-layout
documentation. Internal layouts do not become public stability guarantees.
Release documentation describes the behavior-preserving refactor without
replacing canonical API or architecture documentation.
The alias-versus-wrapper decision is resolved by the user's explicit selection
of wrappers. An implementation convenience is not authority to change that
decision or an established consumer contract.