Plan the public facade refactor

This commit is contained in:
2026-09-12 15:16:52 +00:00
parent 28e7fc5f69
commit eb7de8e40a
2 changed files with 517 additions and 321 deletions

View File

@@ -1,379 +1,388 @@
# Source origins during resolution: implementation plan # Public facade: implementation plan
Status: implementation complete (2026-09-12). All stages were completed in order. Status: not started. Baseline: `28e7fc5f690b36c60e4d03586afcff00fb1dee90`.
The [feature roadmap](source-origins-during-resolution.md) owns the target contract The [feature roadmap](public.md) owns the target architecture and user intent.
and consumer intent. This document owns implementation steps, validation, and This plan owns implementation decisions, ordered work, validation, and handoffs.
handoffs. It is written for one gpt-5.6-terra coding agent per stage; no stage Each numbered stage is sized for one gpt-5.6-terra coding-agent assignment;
requires parallel agents or changes to a downstream repository. no stage requires subagents or changes to downstream repositories.
## Required reading and working rules Historical navigation: this filename previously held the completed source-origins
plan, including its Stage 5 completion record. That record is preserved at the
baseline above: `git show 28e7fc5:docs/roadmap/implementation.md`. Existing incoming
links from the source-origins roadmap, remediation record, and development guide
refer to that historical plan, not the stages below. Stage 1 repairs those links.
Before each stage, inspect the worktree and read the preceding stage's completion ## Working rules
record. Follow [development](../development.md) and all policies:
Before each stage read [development](../development.md), the feature roadmap,
the preceding completion records, and all applicable policies:
[architecture](../policy/architecture.md), [documentation](../policy/documentation.md), [architecture](../policy/architecture.md), [documentation](../policy/documentation.md),
[testing](../policy/testing.md), and [release](../policy/release.md). [testing](../policy/testing.md), and [release](../policy/release.md).
Read the roadmap, the configuration contract's resolution and metadata sections, Read the owning configuration/API contracts and implementation before editing.
and the relevant code before editing. Use the code graph for discovery and verify Prefer the code graph for discovery, verifying its results against current code.
current definitions rather than assuming line numbers or private signatures. Preserve unrelated work and the baseline's already-fixed resolution-history copy.
Preserve unrelated work. Keep each stage independently buildable with passing Complete stages in order. Every handoff must build and pass its relevant tests;
tests; do not introduce stub public methods or defer known correctness failures. do not leave placeholder public methods, skipped regressions, or known failures
Run the focused commands listed for the stage and `./scripts/check.sh` from the for a later stage. Run `./scripts/check.sh` at every code-stage handoff, in addition
repository root. Acquire required toolchains/dependencies explicitly before offline to the focused gates below. Keep default tests deterministic and offline. Follow
checks. Document unsupported filesystem fixtures rather than silently omitting them. the development guide for dependency acquisition and supported toolchains.
Use real collaborators and temporary files for provenance. Keep default tests Record completion beneath each stage: changed owners, tests and actual toolchain,
offline, deterministic, and credential-free. Prefer codes, paths, layers, locations, results, deviations, remaining risks, and the next handoff. Update current
and observable snapshots over private call counts or incidental error messages. documentation with actual changes, not promised completion. Commit stage work
Only use a narrow I/O or credential seam where it proves the no-extra-access rule. when authorized; this plan does not authorize pushes, tags, release selection,
or publication. Release notes and native-platform release evidence remain part
of the separate release procedure.
At each handoff, update that stage's status here with changed files, tests/results, ## Fixed implementation decisions
actual toolchains, deviations, and remaining risks. Keep public reference and current
contract documentation synchronized with behavior as it lands. Commit coherent
stage work when authorized, but do not push, tag, select a version, or prepare a
speculative release note as part of this implementation sprint.
## Implementation decisions ### Package and migration strategy
The roadmap's two method signatures are fixed for this sprint: Final production packages are the public root, `internal/engine`, and the existing
`internal/document` and `internal/yaml`. Keep the engine cohesive; do not create
separate packages for every phase. Root imports engine; engine imports its
supporting packages; no internal production package imports root. Only the YAML
adapter imports the upstream YAML dependency.
```go Public named types remain defined in root. Use named private fields for delegated
func (r *Resolution) InputInfo(path Path) (FieldInfo, bool) state, explicit forwarding methods, and ordinary public record declarations.
func (r *Resolution) Source(id string) (SourceInfo, bool) Do not alias public types to engine types, anonymously embed engine types, expose
``` engine types in public signatures/fields, or add public bridge methods.
Internal type aliases used only inside implementation are not public aliases.
The required behavior is a stable entry-state view and detached public return Because private-state references span most root implementation files, use a
records, not a particular internal representation. The recommended implementation bounded relocation candidate rather than attempting a live package split in
is a private `Metadata` value containing effective fields and path shapes, together every stage. Stage 2 copies the existing implementation into `internal/engine`;
with a source-ID index. Reuse `Metadata.Lookup` semantics. A historical view is not Stages 4–7 build the eventual root facade in temporary `internal/facade` (package
needed for this feature, and exposing one is outside this sprint's scope. name `configkit`). Root remains the sole consumer entry point until Stage 8.
The temporary facade is staging space, not a second supported API/package.
Build the view after `metadataFromDecoded` and `applyOverlayMetadata` in This temporary duplication is a migration mechanism only: no feature development,
`resolveLoadInput`, immediately before `runResolution`, only when a resolver exists. runtime selector, fallback engine, or new consumer import is introduced. Baseline
Its path shapes come from `decoded.final`, not the raw document or mutable Go semantics remain fixed; necessary corrections must be reconciled explicitly.
configuration. Separate copies of field/source records are one straightforward way Stage 8 moves the facade to root and removes the old implementation and temporary
to isolate the view; immutable sharing or ownership transfer is equally acceptable directory in the same handoff. There is one production implementation at completion.
when it preserves query stability and detached returns. Source records come from This makes the large textual move mechanical while separating it from the more
`assembledInput.sources`. Avoid aliases that observable consumer operations or demanding ownership and callback work. Do not keep two engines as a final design.
subsequent metadata updates could mutate while the view remains usable.
Missing or duplicate internally generated IDs are implementation-invariant failures
to fix, not new consumer-facing diagnostic categories.
Prefer passing the private view explicitly to `runResolution` and retaining it in ### Type and state ownership
`resolutionState`. Update all private callers/tests deliberately: construct their
entry view from their decoded fixtures and supplied sources where they test these
queries. Every production resolution callback must receive complete provenance.
Private signatures and representation may change when an idiomatic alternative
better serves that requirement; record the reasoning in the stage handoff.
Queries first check callback activity through the existing context guard. Only | Public family | Selected representation and adaptation |
then may invalid paths or unknown IDs return misses. `InputInfo` delegates to the | --- | --- |
entry metadata lookup; `Source` clones the matched source on return. Preserve | `Path` | Root struct with named private `engine.Path` value; constructors and methods delegate. Preserve semantic segment kinds and invalid state by direct private wrapping, never stringify/reparse. |
existing `IsExplicit`, diagnostics, resolution recording, and secret timing. | `Layer`, `ChangeKind`, `Severity`, `Code`, `SecretFileStatus` | Root-defined string types and explicitly typed constants with unchanged values. Convert to engine equivalents at the boundary; engine constants may supply values without making public aliases. |
| `Location`, `SourceInfo`, `Diagnostic`, `Origin`, `FieldInfo`, `Change` | Root-defined records with unchanged exported fields. Explicit private conversions cover nested pointers, slices, and paths. |
| `ValueSnapshot`, `ValueChange`, `SecretReferenceInfo`, `Explanation`, `SecretFileResult` | Root-defined output records. Convert detached engine records; preserve nil/empty distinctions and field semantics. |
| `Source` | Root struct with private `engine.Source` value. Engine owns bytes, constructor validity, and the shared reader claim; wrapper copies must not create new claims or re-read/copy source input. |
| `Metadata` | Root struct with private engine metadata value; explicit query methods convert detached results. Engine retains effective and historical indexes. |
| `Resolution`, `Validation` | Root structs with private pointers to their engine contexts. Engine alone owns activity and audit state; wrappers never recreate that state. |
| `Definition[T]`, `Options[T]`, `FilesystemContext`, `Override`, `VersionRule`, `Version` | Root records retaining all current fields and signatures. Private option/callback conversion into engine records; no extra configuration clone or reflection-based generic record mapper. |
| `Result[T]` | Root struct retaining `Config T`, plus private pointer to a non-generic engine inspection result. Engine load returns configuration and inspection separately; facade stores configuration exactly once. |
| `Secret` | Root-owned opaque scalar retaining value-copy semantics and existing public methods. Private exact-type adapter lets engine read/build it; no public accessor for plumbing. |
| `LoadError`, `SecretsError`, `SecretsReport` | Root structs with private engine backing state and explicit methods. Translate engine typed failures at entry points; never return or unwrap an engine error as the public structured error. |
Queries must not reload input, read files, resolve symlinks, access credentials, The inventory in Stage 1 verifies every baseline exported declaration, not just
or re-project mutable configuration instead of reading entry-state information. these grouped examples. Public type identity stays rooted in `configkit`.
Prefer reusing existing indexes and avoiding unnecessary whole-tree copies. Preserve receivers, generics, supported zero/nil behavior, interface satisfaction,
Per-load source indexing is a recommendation, not a complexity guarantee: a simple error assertions, and supported construction/copy behavior. Private field layout
scan may be appropriate for the existing bounded source collection. Assess costs and incidental formatting are not new compatibility promises.
in context, including path lengths and nested record copies; this feature adds no
performance guarantees, resource caps, or benchmark gates.
## Stage 1 — Factor reusable effective-path indexing Use handwritten, local conversion helpers. For detached outputs, transfer already
detached data where safe instead of deep-copying everything again. Never expose
engine-owned mutable slices/maps through public records. Preserve order and
missing/null distinctions. Do not round-trip records through JSON/YAML or use
`unsafe`, registration globals, or reflection to bypass private fields.
Status: complete (2026-09-12). Dependencies: none. ### Exact-type secret bridge
Scope: `metadata.go`, `load_metadata.go`, and their focused test owners. Introduce an engine-internal `SecretBinding` containing `Type reflect.Type`,
`Read func(reflect.Value) SecretState`, and
`Make func(SecretState) reflect.Value`. `SecretState` carries the existing name,
value, specified, and resolved fields. It is operation data, never inspection
data. The root constructs the binding privately for exactly `reflect.TypeFor[Secret]()`;
Read and Make use ordinary type assertions/construction in the owning package.
1. Prepare reusable effective-path lookup for the entry view. The recommended Supply the binding explicitly to the engine load and schema construction. Store
approach extracts indexing from `finalizeMetadataPaths`; an equivalent design it in the per-operation schema/value-operation context, available to recursive
may reuse existing traversal directly. Keep historical indexing and result decoding, projection, cloning, dynamic-value checks, resolution, and validation.
publication behavior unchanged, and preserve one set of semantic-path rules. Keep the application configuration type `T` unchanged: no mirror configuration
2. Add the private entry-view construction described above using pre-resolution structs and no conversion of arbitrary application objects.
metadata, the decoded final tree, and acquired source records. Share extracted
machinery with result finalization where useful. Do not expose public methods yet.
3. Establish and document internal ownership sufficient for stable queries and
detached returns. Reuse `cloneFieldInfo` and `cloneSourceInfo` where copying is
needed; do not require eager copies or duplicate history solely to match this
plan's recommended representation.
4. Add focused coverage proving the private view recognizes defaulted fields,
missing descendants, root, escaped keys, explicit key/index mismatches, and
contextual canonical sequence indices. Test stability across actual lifecycle
operations and mutation of returned records, not arbitrary mutation of backing
data that the implementation owns as immutable. Reuse existing shape tests for
exhaustive syntax cases rather than duplicating them.
Acceptance: all existing final metadata/history behavior is unchanged; a complete Replace all direct engine assertions/constructions of the old `Secret`, not just
entry view can be constructed before final publication without a historical index. schema recognition. Include decoding, unresolved versus validated clone modes,
No public API or configuration contract changes are claimed by this stage. projection, required/optional references, lookup caching, and validator mutation
audit. Recognition remains exact-type recognition, not acceptance of any type
implementing an interface. Bindings are fixed by the facade, not configurable by
consumers, and perform no environment access themselves. Environment lookup
remains in its existing lifecycle phase and its existing private test seam.
Validation: `go test . -run 'Metadata|Path|Provenance|Historical'`, then the everyday Engine unit tests use a test-local secret type and binding. Root integration
checker. Handoff should identify the helper and constructor signatures for Stage 2. tests exercise the actual public `Secret`. There is no production engine-owned
replacement secret type that consumers must put in their configuration.
Completion record (2026-09-12): extracted `effectivePathIndex(final)` from final ### Callback, option, result, and error bridges
metadata publication and introduced `newResolutionInput(metadata, final, sources)`.
The private view owns cloned metadata/source records, indexes source IDs, and exposes
private detached lookup helpers for the next step; it intentionally has no public API
or load-lifecycle wiring yet. `TestResolutionInputKeepsDetachedEntryMetadata` covers
root, defaults, explicit null, missing descendants, escaped keys, contextual parsed
sequence indices, key/index mismatches, detached records, and stability after real
recorded resolution. `go test . -run 'Metadata|Path|Provenance|Historical'` and
`./scripts/check.sh` passed with Go 1.26.7 on Linux. No deviations or remaining
Stage 1 risks were identified.
## Stage 2 — Wire the callback API and enforce lifetime Keep the engine lifecycle generic in the real `T`. Convert each supplied options
value without selecting/validating options in the facade: engine retains original
validation order and diagnostics, including multiple-options rejection. Preserve
nil callbacks, version pointers, defaults behavior, and override values without
eagerly invoking callbacks or evaluating `any` values.
Status: complete (2026-09-12). Dependencies: Stage 1. Adapt Resolve/Validate/Version.Check by closures wrapping the exact engine context
pointer and passing the same `*T` received from engine. Convert version records and
semantic paths at those boundaries. The secret-classification predicate similarly
wraps the engine path. Do not recover, retry, close, or extend callbacks in the
facade; engine retains the existing lifetime and failure behavior.
Scope: `load_result.go`, `resolution.go`, necessary private callers, reference Generic public Default/Normalize forward to generic engine helper functions with
comments, and resolution/callback ownership tests. the original target pointer and value. Do not implement them as just assignment
plus Record: existing target validation and projected expected-value audit matter.
Nil wrapper handling must preserve current inactive-context behavior.
1. Build and pass the entry view at the existing pre-resolution boundary. Use Refactor engine publication to return a short-lived generic loaded value containing
`input.sources`, which includes sources acquired by import, profile, and CLI `Config T` and an inspection pointer. Inspection owns frozen trees, metadata,
processing; do not reconstruct source identities from labels or field locations. history, sources, diagnostics, and sensitivity; it does not retain `T` or callback
2. Implement both public methods with the existing activity guard and detached bindings. Public `Result[T]` holds `Config` and that pointer, not another complete
returns. Keep lookup state frozen when recording changes or mutating configuration. generic engine result. Result inspection must not reread caller-mutated Config.
3. Add public-load tests for a file-authored scalar, a defaulted field, explicit Existing caller-constructed zero results and nil result receivers retain behavior.
null, a missing path, empty/unknown source IDs, and `NoSource`. Assert exact
`InputInfo.Explicit` agreement with `IsExplicit` for found paths.
4. Exercise nil/zero contexts and retained contexts after successful and failed
callbacks. Both methods must use the existing fixed closed-context panic behavior.
Saved return records remain usable after callback exit. Do not broaden this work
into a new concurrency contract or a general callback framework refactor.
5. Add Go method documentation and update the configuration contract's resolution
section to describe the implemented entry-state semantics, misses, lifetime,
and source distinction. Fix the development guide's stale statement that this
implementation plan has not yet been written.
Acceptance: both methods work during an ordinary public `Load`; calls cannot access Translate engine load/bootstrap failures into root `*LoadError`/`*SecretsError`,
live mutable metadata or bypass callback scope. No-rule/no-resolver behavior and respectively. Their backing state can be delegated privately, but no public
existing mutation auditing remain unchanged. Unwrap exposes internal failures. Retain bootstrap partial reports and ordinary
writer errors/short-write behavior. Do not wrap arbitrary render/write errors into
new structured load errors or add error-disclosure guarantees.
Validation: `go test . -run 'Resolution|Callback|Metadata|Load'`, then the everyday ## Stage 1 — Record the boundary and establish compatibility evidence
checker. Do not mark complete with public methods that work only in private tests.
Completion record (2026-09-12): `resolveLoadInput` now creates a Status: not started.
`resolutionInput` after decoded/overlay metadata and passes it explicitly into
`runResolution`; the private state retains it for the callback. `Resolution.InputInfo`
and `Resolution.Source` use the existing activity guard before exact entry lookup or
source lookup, and return detached records. Public load tests cover a canonical
file-authored value, a retained default, explicit null, misses, empty/unknown IDs,
and defaults-only input; they also cover nil, zero, successful-retained, and
failed-retained contexts plus saved record independence. Go reference comments,
the configuration contract, and the development guide now describe the active work
accurately. `go test . -run 'Resolution|Callback|Metadata|Load'` and
`./scripts/check.sh` passed with Go 1.26.7 on Linux. No deviations or remaining
Stage 2 risks were identified.
## Stage 3 — Verify layered provenance and snapshot independence 1. Run baseline everyday checks. Inventory the root's exported types, constants,
fields, functions, receiver method sets, generics, and interface implementations
with Go tooling. Record the baseline revision and concise ownership mapping in
the stage handoff; do not create a second normative API specification.
2. Add the next available ADR (currently `0006-public-facade.md`) recording wrappers,
private delegation, the dependency direction, and the alias-first alternative.
Record the temporary migration mechanism here, not as durable architecture policy.
3. Update architecture policy's public API/dependency section for the agreed
boundary, preserving proportionality and existing behavior contracts. Explain
that this is accepted design, not completed implementation. Add ADR navigation.
4. Repair the three historical incoming references described at the top of this
plan to identify the old plan by Git revision. Preserve the completed provenance
evidence; do not label this facade plan as its completion record.
5. Retain/add a compact external-package facade compatibility test covering the
realistic boundary risks: root named-type identity, exported construction,
generic helper use, structured error assertions, and relevant interfaces.
Use existing tests for behavior already protected. Inspect the complete exported
surface separately; do not add golden dumps of private struct layouts.
Status: complete (2026-09-12). Dependencies: Stage 2. Acceptance: baseline passes, public surface is understood, ADR/policy accurately
distinguish decision from implementation, and documentation links pass.
Run `go test . -run 'LocalDocumentationLinks|DependencyImportBoundary|Example|Facade'`
and the everyday check. No engine migration in this stage.
Scope: public lifecycle/provenance, import/profile/override, and ownership tests; ## Stage 2 — Establish the internal relocation candidate
production fixes only for gaps exposed at those boundaries.
1. Build a real temporary import graph with path-bearing scalars and a sequence Status: not started.
element. Include a composite mapping with contributions from distinct files.
During resolution, assert scalar ownership, all container contributors, and
successful source lookup for every origin location returned by the view.
2. Apply a profile replacing one scalar and an equal-text value; verify replacing
origins and untouched siblings. Exercise equal-text CLI replacement separately.
Keep source IDs derived from returned records, not inferred from value equality.
3. Exercise byte and reader roots with filesystem context and file imports: the
memory root has no file path while imported files do. Also cover non-file input
without imports, default-only fields, and unknown source IDs. Retain default
descendants reintroduced by normal decoding instead of treating them as raw
input declarations.
4. Extend shape coverage through inlined structs/maps, typed arrays, and dynamic
structures. Assert misses for invalid indices, signed index tokens in sequences,
and explicit key/index mismatches; signed mapping keys remain literal keys.
5. Normalize, delete, add, and move fields within a callback using valid records.
Queries for original paths must stay fixed; absent destinations stay absent,
and pre-existing destinations retain their entry information. Final metadata
must still reflect resolution rather than that fixed entry view.
6. Mutate returned `Origin.Locations` and `SourceInfo.ImportedFrom`; verify repeated
queries, saved records, final results, and another load are independent. Exercise
independent loads under the race detector without sharing an active context.
Acceptance: roadmap criteria 2–8 and 11 are covered at their owning boundaries. 1. Copy root production implementation except public package orientation into
No duplicate provenance model, nearest-ancestor fallback, or source-order heuristic `internal/engine`, changing package declarations/import references mechanically.
is introduced. Any discovered defect is fixed before the stage is marked complete. Preserve file divisions and existing document/YAML packages. Keep internal
functions private unless a later facade connection actually needs an export.
2. Copy behavior/mechanism tests and needed fixtures for engine validation, excluding
repository documentation, release-script, import-boundary, public examples,
and consumer integration tests. Keep root unchanged and passing. Repair fixture
paths explicitly; do not make tests depend on arbitrary working directories.
3. Avoid blind replacements inside user-facing strings, diagnostic codes, fixture
YAML, or documentation comments. The package rename is not permission to rename
messages or behavior. Record the candidate's source baseline.
4. Update the component overview with an explicitly temporary candidate entry.
Do not claim root loads delegate yet.
Validation: `go test . -run 'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`, Acceptance: `go test ./internal/engine` and the everyday check pass. Engine imports
`go test -race ./...`, then the everyday checker. no root code, existing internal/parser boundaries remain intact, and root consumers
still execute the unchanged original implementation. This stage is deliberately
mechanical; do not mix ownership redesign into the relocation diff.
Completion record (2026-09-12): public lifecycle tests now build real temporary ## Stage 3 — Decouple reflection from the public secret type
main/import/profile graphs and prove scalar, sequence, composite-container,
profile, and equal-text CLI provenance through `InputInfo`/`Source`, deriving source
IDs from returned locations. Coverage also exercises byte and reader roots with
filesystem imports, a standalone non-file root, retained defaults, unknown IDs,
typed/inlined/map/array/dynamic shape matching, signed mapping keys, and invalid
sequence shapes. Recorded normalization, deletion, addition, and move operations
leave entry queries fixed while final metadata reflects resolution. Returned records,
completed results, another load, and concurrent independent loads remain detached.
No production gap was exposed. `go test . -run
'Resolution|Provenance|Import|Profile|Override|Metadata|Path'`, `go test -race
./...`, and `./scripts/check.sh` passed with Go 1.26.7 on Linux. No deviations or
remaining Stage 3 risks were identified.
## Stage 4 — Demonstrate source-aware normalization and safe inspection Status: not started.
Status: complete (2026-09-12). Dependencies: Stage 3. 1. Implement the exact-type SecretBinding/SecretState design above in engine.
Thread it through schema and recursive value operations explicitly; do not use
a global binding or make it an application option.
2. Replace concrete-secret assumptions in `schema.go`, `decode.go`, `clone.go`,
`projection.go`, `value_operation.go`, `secret_reference.go`, and their callers.
Find all additional references using the graph/text search; the list is not a
reason to leave a direct assertion behind.
3. Move engine tests to their local secret type/binding and remove the candidate's
production concrete Secret and public-style constructor. Preserve named-type
rejection/acceptance semantics and dynamic-value checks from the baseline.
4. Exercise unresolved/default secrets, pointer and container shapes, borrowed
callback values, lookup-once behavior, and validator replacements without
disclosing canary credentials in diagnostic expectations.
Scope: maintained Notarius-shaped example code/fixtures, executable examples, Acceptance: engine secret/schema/decode/clone/projection/validation tests and race
explanation/secret integration tests, and consumer guidance. tests pass; binding instances cannot affect each other. Run
`go test ./internal/engine -run 'Secret|Schema|Decode|Clone|Projection|Validation|Callback'`,
`go test -race ./internal/engine`, and the everyday check.
1. Add a small application-owned path-bearing configuration example in ## Stage 4 — Build candidate value, source, and diagnostic wrappers
`examples/consumers`. Keep existing catalog/version examples working. Supply the
invocation directory explicitly to the example loader rather than calling
`os.Chdir` or adding process-global state to tests.
2. Implement the example's policy using the two public queries: absolute paths
unchanged; CLI-relative paths based on the supplied invocation directory;
default-relative paths based on the canonical main source; file-authored paths
based on the scalar's declaring source. Use the existing primary-source identity
convention for main-source lookup, not a guess about contributor ordering.
3. Demonstrate file-relative normalization with fixtures that have an unambiguous
declaring file. If the example handles absent or ambiguous file provenance,
make that behavior an explicit example policy, not a correctness requirement
or prescribed Notarius policy. Rejection, a caller-supplied fallback directory,
retaining a relative value, or warning are application choices. The example's
selected behavior must be documented and tested; ConfigKit must continue to
expose the distinction rather than fabricate a file from a diagnostic label.
4. Record every changed path through `Normalize` or `Record`, including valid
dependencies where derived values depend on other configuration paths. Prove
runtime values, effective YAML, and explanation agree; retain the original
relative declaration and normalization reason in history. No existence or
confinement checks are part of the example.
5. Add a symlink fixture proving canonical target-directory semantics and an
invocation-directory mismatch proving resolution does not depend on process CWD.
Make unsupported symlink capability explicit under the testing policy.
6. Use synthetic sensitive fields to verify original/effective redaction remains
intact. With existing reader/secret lookup seams, prove queries do not reread
source data or resolve credentials during the resolver; normal later lookup
remains unchanged. Do not create credentials or environment-dependent tests.
7. Document the public workflow and canonical symlink choice in the consumer guide;
point to executable artifacts instead of duplicating large code examples.
Acceptance: roadmap criteria 1, 9, 10, and 12 are demonstrated in one-load public Status: not started.
workflows. Filesystem policy stays in the example application, not the root library.
Validation: `go test ./examples/consumers`, 1. Create temporary `internal/facade`, package `configkit`, importing engine.
`go test . -run 'Resolution|Explain|Secret|Example'`, then the everyday checker. Implement root-owned Path, Source, Secret, scalar enums/constants, and provenance
records according to the ownership table. Preserve declaration comments for
their eventual root placement. There must be no aliases or embedded engine types.
2. Implement private path/record conversions and the real facade secret binding.
Keep public Secret's small access/format/JSON methods local; its private scalar
state remains value-based, not a shared mutable reference.
3. Add LoadError and WriteDiagnostics delegation/conversion. Candidate engine may
export narrow constructors/accessors as needed internally, not public bridge
functions on the candidate facade. Preserve nil handling and typed errors.
4. Test candidate public operations from its external test package. Verify that
numeric-key versus index versus parsed paths are not collapsed; source copies
share the same reader claim; bytes are snapshotted at construction; formatting
and JSON do not expose resolved secret values.
Completion record (2026-09-12): added the maintained `NotariusPathConfig` example Acceptance: candidate wrapper tests and engine tests pass. Run
and fixtures. Its application-owned resolver uses `InputInfo`/`Source` only: file `go test ./internal/facade ./internal/engine` and the everyday check. Lifecycle
values use their exact declaring file, defaults use `source-1`, CLI values use the entry points are added in Stage 6; do not add nonfunctional stubs here.
explicit invocation directory, and absolute values remain unchanged. Missing CLI
bases or missing/composite file provenance retain the relative value and report an
application warning. Tests prove imported-file, default, CLI, absolute, and canonical
symlink-target bases without changing process CWD; the symlink test explicitly skips
when the platform cannot create it. They also prove effective/explanation agreement,
retained original declaration and reason, sensitive redaction, and that provenance
queries do not re-read a reader source or resolve credentials before normal lookup.
The consumer guide links to the executable artifact. `go test ./examples/consumers`,
`go test . -run 'Resolution|Explain|Secret|Example'`, and `./scripts/check.sh` passed
with Go 1.26.7 on Linux. No deviations or remaining Stage 4 risks were identified.
## Stage 5 — Audit the complete contract and hand off release readiness ## Stage 5 — Separate engine publication and inspection
Status: complete (2026-09-12). Dependencies: Stage 4. Status: not started.
Scope: integration review, canonical documentation, roadmap completion records, 1. Split generic engine Result into the short-lived loaded configuration carrier
and proportionate fixes for remaining acceptance failures. No feature expansion. and non-generic inspection state specified above. Move inspection methods to
the inspection receiver without changing their snapshots or algorithms.
2. Adapt `load_result.go`, `load_metadata.go`, `metadata.go`, `explain.go`,
`explain_render.go`, `render.go`, `history.go`, and `snapshot.go` as necessary.
Keep the fixed entry-state view independent of final history, including the
baseline fix that avoids copying unused resolution history.
3. Update engine tests for the internal receiver/return changes while preserving
observable expectations. Do not broaden nil/result-construction contracts.
1. Check all twelve roadmap acceptance criteria against named tests and current Acceptance: engine loading, historical provenance, detached metadata, explanation,
implementation. Record the mapping in this stage's completion entry. Passing and rendering behavior pass. Verify publication still performs its existing
broad suites alone is not evidence that a missing scenario was exercised. configuration detachment but does not store Config in inspection. Run
2. Inspect every new query path for environment/filesystem access, raw values, `go test ./internal/engine -run 'Load|Metadata|Provenance|Historical|Explain|Effective|Render|Independence'`,
caller-mutable aliases, and accidental callback-lifetime extension. Review `go test -race ./internal/engine`, and the everyday check.
allocation and traversal costs for proportionality without turning the chosen
mechanism into a public guarantee or adding unsupported performance gates.
Avoid unnecessary history duplication and unused work in no-resolver loads.
3. Review public signatures and current configuration behavior against the previous
release. Confirm the capability is additive and no parser, path, composition,
credential, or existing callback semantics changed unintentionally.
4. Update the internal overview and finish canonical Go/configuration/consumer
documentation. Transfer implemented contract prose from the feature roadmap
to its proper owners and retain a brief completion record with links. Preserve
historical ADR rationale and published notes. No new ADR is required for this
information-access extension unless implementation actually changes a governing
decision; stop and obtain review rather than silently widening scope.
5. Run the full source checker with qualifying acquired toolchains, recording the
actual versions and host. The following are the repository's recorded versions;
follow the release runbook if selecting updated toolchains:
```sh ## Stage 6 — Wire candidate lifecycle and callback wrappers
CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5 \
CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7 \
./scripts/release-check.sh
```
6. Hand off remaining publication work separately: maintainer version choice, Status: not started.
dependency reassessment, version-matched release notes, completed-roadmap
retirement and link repair, exact-commit Linux/macOS validation, publication
guards, and clean consumer retrieval. Follow [the runbook](../release.md).
Historical one-release exceptions do not carry forward to this feature.
Acceptance: all target requirements have concrete evidence; available automated 1. Add candidate Definition, Options, VersionRule, Version, FilesystemContext,
checks pass; documentation distinguishes implemented behavior from release status. Override, Resolution, and Validation with unchanged public declarations.
Implementation completion is not publication approval. Do not create a release Implement the private option and callback bridges specified above.
tag or mark unavailable native macOS evidence as passed. 2. Add candidate Load using the engine and facade secret binding. Add Result[T]
with Config and private inspection, initially with Metadata, Sources, and
Diagnostics. Implement Metadata's complete public query methods.
3. Forward all resolution methods, including InputInfo and Source, and the generic
Default/Normalize helpers. Engine owns context activity, helper verification,
resolution snapshots, and validation mutation detection.
4. Add focused external candidate tests using actual public wrapper types inside
application configuration. Exercise version callbacks, defaulting/normalization,
retained/copy contexts, root typed error assertions, and public Secret recognition.
Private engine secret tests alone are insufficient evidence for this bridge.
Validation: `./scripts/check.sh`, `go test -race ./...`, and the full checker above. Acceptance: real candidate loads with callbacks succeed/fail identically to the
baseline, no callback is called by option conversion, no extra lookup occurs,
and detached entry provenance stays fixed. Run
`go test ./internal/facade ./internal/engine -run 'Load|Resolution|Validation|Version|Metadata|Secret|Callback|Facade'`,
`go test -race ./internal/facade ./internal/engine`, and the everyday check.
Completion record (2026-09-12): all acceptance criteria have named evidence: ## Stage 7 — Complete candidate inspection and bootstrap surface
1. `TestNotariusPathExampleUsesEntryProvenance` covers canonical main-file and Status: not started.
symlink target bases without process-CWD dependence.
2. `TestResolutionInputKeepsLayeredOriginsAndSources` covers imported scalars and
sequence elements.
3. The same layered test covers profile replacement and untouched base/import
siblings.
4. The layered test covers equal-text profile and CLI replacement.
5. `TestResolutionEntryQueriesExposeDetachedLoadInput` and
`TestResolutionInputDistinguishesNonFileRootsAndImportedFiles` cover defaults,
nulls, misses, CLI/file identities, `NoSource`, bytes, and readers.
6. The layered test verifies every composite location has a source;
`TestResolutionEntryQueriesExposeDetachedLoadInput` verifies unknown IDs miss
safely.
7. `TestResolutionInputRemainsFixedAcrossRecordedMutations` covers normalization,
deletion, addition, and move stability.
8. `TestResolutionInputUsesEntryShapesAcrossTypedAndDynamicValues` covers inline,
map, array, dynamic, escaped/signed keys, and invalid shape cases.
9. `TestNotariusPathExampleUsesEntryProvenance` verifies runtime, effective YAML,
and explanation history together.
10. `TestResolutionInputQueriesDoNotReadOrResolveSecrets` plus the Notarius test
verify no extra reader/credential access and preserved redaction.
11. `TestResolutionInputKeepsLayeredOriginsAndSources` and
`TestResolutionEntryQueriesExposeDetachedLoadInput` verify detached records.
12. `NotariusPathConfig`, its fixtures, executable example, and consumer-guide
link provide the maintained public-API/standard-library workflow.
The final review found that queries use only the callback guard and detached 1. Add the public explanation/value records and Result Explain, WriteEffective,
metadata/source indexes; they do not extend callback lifetime, expose raw values, or and WriteExplanation delegation. Inspection uses the frozen engine state,
perform environment/filesystem work. The view is created only for resolver loads and not the mutable public Config field. Complete every remaining public method.
now retains only effective field/path metadata and source records. Post-implementation 2. Add SecretsReport, SecretsError, bootstrap records/status constants, and
review found that the original constructor unnecessarily cloned history; remediation LoadSecretsDir. Preserve opt-in writes, partial reports, detached entries,
replaced that clone with detached field-only construction, leaving final result writer counts/errors, and non-unwrapping typed errors.
history unchanged. The comparison with `v0.2.0` found only additive exported 3. Exercise complete load-to-inspection workflows through the candidate. Prove
methods; loading, parser, path, composition, credential, and existing callback that caller mutation of Config or returned records does not rewrite inspection
semantics remain unchanged. Canonical Go, configuration, consumer, internal, and history or affect a later query/load. Include malformed paths and nil receivers
roadmap documentation are synchronized; the feature roadmap is retained only until where the baseline already defines behavior.
release preparation. `./scripts/check.sh`, `go test -race ./...`, documentation and 4. Compare the complete candidate exported surface with the baseline inventory,
import-boundary checks, and `CONFIGKIT_BASELINE_TOOLCHAIN=go1.25.5 normalizing only the temporary import path. Resolve all signature/method/field
CONFIGKIT_NEWER_TOOLCHAIN=go1.26.7 ./scripts/release-check.sh` passed on Linux with omissions before cutover. No placeholder methods or ignored API mismatches.
Go 1.25.5 and Go 1.26.7. Publication remains unapproved: a maintainer must choose a
pre-v1 minor version, write its note, retire completed roadmaps, obtain exact Acceptance: the candidate implements the entire supported API and has real
candidate native Linux/macOS evidence, and follow the release procedure. No version, integration evidence for inspection, credentials, bootstrap, and writer failures.
release note, candidate, tag, or push was created. Run `go test ./internal/facade ./internal/engine`, race tests for both packages,
and the everyday check. Root remains unchanged until Stage 8.
## Stage 8 — Cut over the root and remove migration duplication
Status: not started.
1. Replace root production plumbing with the tested candidate facade, retaining
root package documentation and the root named types. Move files; do not leave
forwarding aliases to the temporary facade. Remove `internal/facade` entirely.
2. Root-owned unit/integration tests remain at root; engine mechanism tests remain
with engine. Remove duplicated baseline copies after confirming coverage owners.
Split mixed tests by boundary: assertions requiring private engine state move
internally, while public lifecycle/consumer assertions still exercise root.
Do not move all tests inward simply to avoid adapting public wrappers.
3. Run existing executable API examples and maintained consumers unchanged in
their imports/configuration types. Fix wrapper defects rather than migrating
consumers to a different API. Test the actual root Secret binding after the move.
4. Keep repository check tests at root. Repair documentation/fixture references,
fuzz target locations, and development task commands as files move. For links
formerly pointing at mixed implementation files, point API references at root
and implementation references at engine as appropriate.
5. Remove old root algorithms, temporary candidate tests superseded by root tests,
and staging documentation. Preserve useful narrow private test seams, but no
root accessors solely exposing engine internals to tests.
Acceptance: one engine serves the sole root facade; no candidate directory,
parallel implementation, or root import from internal production code remains.
Run the everyday check, `go test ./examples/consumers`, root examples, and
`go test -race ./...`. All gates must pass in this handoff; do not leave a broken
root suite for the final audit.
## Stage 9 — Audit the public boundary and finalize documentation
Status: not started.
1. Compare the final complete exported API against the baseline. Audit public
signatures, fields, aliases, embedded types, method sets, and error returns
for internal-type exposure. Review root code for remaining substantive
algorithms; explain small value operations deliberately retained there.
2. Extend the existing import-boundary check for inward production dependencies.
Add a narrowly scoped public-boundary check that detects exported aliases to,
embedded exposure of, and exported signatures/fields involving internal types.
Use standard Go tooling and avoid private-field/layout snapshots. Account for
generic declarations and public records referencing other public records.
Keep behavior tests as the evidence for adapter correctness.
3. Verify source ownership, secret binding isolation, zero/nil handling, helper
audit semantics, typed errors, final Config versus frozen inspection, and the
baseline resolution-entry optimization. Add tests only for uncovered risks.
4. Finalize architecture-policy wording, ADR navigation, component overview,
development test/fuzz commands, and public declaration comments. Add a concise
internal facade mechanism document if needed for the secret/callback/record
connections; do not spread implementation recipes across consumer docs.
5. Update consumer orientation only where useful. Keep YAML behavior in its
existing contract. Convert the feature roadmap to an implemented summary with
canonical links and evidence; record remaining release preparation separately.
Acceptance: documented target matches code and no unapproved API/behavior change
is hidden by the refactor. Run the everyday check, full race suite, consumer
examples, documentation/import/public-boundary checks, and tests/builds using the
supported baseline and newer Go toolchains recorded in the release procedure.
Record actual versions/platforms; do not treat Linux tests or cross-builds as
native macOS release evidence. Do not publish or select a version in this stage.
## Open Questions ## Open Questions
None blocking implementation. The API, snapshot boundary, missing-value semantics, None requiring a user decision were identified. Wrappers, public type ownership,
source identity handling, and ownership are specified by the roadmap and decisions one final engine, per-operation exact-type secret adaptation, and a single cutover
above. Example fallback behavior is illustrative application policy, not a missing are specified above. Internal helper names and file grouping remain ordinary
library contract; record any selected example behavior without prescribing it to implementation choices. A newly discovered conflict with an existing contract
consumers. Release version and publication timing are later must be surfaced with evidence and alternatives rather than resolved by changing
maintainer decisions, not unresolved implementation choices. If new evidence consumer behavior or reverting to public aliases.
contradicts a target contract, document the conflict, a recommended correction and
its rationale, and at least one viable alternative before proceeding past it.

187
docs/roadmap/public.md Normal file
View File

@@ -0,0 +1,187 @@
# 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.