Files
ConfigKit/docs/roadmap/remediation.md

418 lines
23 KiB
Markdown

# Pre-release Maintainability Remediation
## Status and scope
Status: in progress; completed stages and their validation are recorded below.
Audit baseline: `9b147bfd96aec25e0125dcae66e136e56dc7a670`.
This roadmap owns the agreed maintainability cleanup before v0.1.0. It replaces
the completed implementation-defect remediation record, whose findings, fixes,
and validation evidence remain available in Git history. It does not reopen
those defects or replace the original [implementation plan](implementation.md).
The audit found worthwhile consolidation and ownership improvements, rather than
a need to redesign ConfigKit. The offline default suite and vet passed at the
audit baseline. That evidence is not validation of future changes or a release
candidate.
Read the [development guide](../development.md),
[architecture policy](../policy/architecture.md),
[testing policy](../policy/testing.md), and
[documentation policy](../policy/documentation.md) before implementation.
The [configuration contract](../config.md) owns current semantics; the
[component overview](../internal/overview.md) owns implementation inventory.
Release preparation remains governed by the [release policy](../policy/release.md)
and [maintainer procedure](../release.md).
## Execution rules and invariants
Implement the numbered stages in order. Each stage is one reviewable unit of
work: inspect its current owners and tests, make the change, validate it, update
its status and evidence here, and commit the nonempty unit with a short plain
English message. Do not create empty commits. Record any discovered behavioral
defect separately with a reproduction rather than silently changing semantics
under a refactoring stage.
Keep the root public package and the existing `internal/document` and
`internal/yaml` packages. Do not introduce dependencies, public API changes,
provider interfaces, configurable merge policies, or a generic visitor framework.
Private file splits do not require new packages or ADRs. Parser-specific types
must remain behind the existing YAML adapter boundary.
Preserve the implemented contracts, particularly:
- Strict composition remains distinct from intentional profile/CLI overlay.
- Input precedence, final-only typed decoding, retained defaults, canonical
decoding explanation events, and recorded domain resolution remain unchanged.
- Source identity, containment, optional-import rules, limits, diagnostic codes,
source coordinates, ordering, and early callback termination remain intact.
- Semantic paths retain explicit key/index distinctions and historical shapes.
- Returned values and inspection snapshots remain detached; secret values remain
absent from history, rendering, and library-generated diagnostics.
- Resolution runs before secret lookup and validation; ordinary loading never
bootstraps environment variables implicitly.
Use focused existing behavioral tests during each stage and the baseline checks
from the development guide before committing production changes. Add tests only
for meaningful gaps exposed by extraction, not to enforce file locations or
private call choreography. Update current owning documentation alongside each
move. Planned filenames below are private implementation destinations, not new
consumer contracts.
## Stage 1 — Remove unused overlay value snapshots
Status: complete.
Completion: removed unread overlay before/after trees and their construction and
cloning, including the now-unused CLI position lookup. Field-read review confirmed
that resolution snapshots and history applications have separate live consumers;
those and displaced origins remain intact. The full offline test suite, vet,
build, and whitespace checks passed with Go 1.26.7 on Linux/amd64.
Evidence: `overlayRecord` in [overlay.go](../../overlay.go) retains `before` and
`after` trees; profile and CLI processing construct them and
`cloneOverlayRecords` clones them. Production consumers use the record's change
and displaced origins, while explanation uses `historyApplication` instead.
Work:
1. Reconfirm all field reads before removal. Remove only the unused overlay
snapshot fields and their construction/cloning in `overlay.go` and
[override.go](../../override.go).
2. Preserve `change`, `displaced`, input applications, and their ownership rules.
Do not remove resolution snapshots or retained decoded trees: those have
separate consumers and budget semantics.
3. Remove locals and imports made unnecessary by the deletion. Do not replace
the removed state with a new cache or history representation.
Acceptance: profile/CLI provenance, explanation replay, composition independence,
and redaction tests pass unchanged in observable behavior. Source inspection
confirms no unused overlay snapshot construction remains. An allocation benchmark
is not required to justify removing provably unread clones.
## Stage 2 — Centralize parser issue translation
Status: complete.
Completion: `parserDiagnostics` in `parse.go` now owns detached, canonical
translation for selected, imported, and profile sources. Existing tests cover
safe selected-source errors and imported/profile source attribution, so no
duplicate regression was added. Full offline tests, vet, build, and whitespace
checks passed with Go 1.26.7 on Linux/amd64.
Evidence: [parse.go](../../parse.go) translates adapter issues inline, while
`importParseDiagnostics` in [imports.go](../../imports.go) repeats the translation
for imports and profiles.
Work:
1. Define one private `parserDiagnostics` helper in `parse.go` that maps adapter
issues to detached public diagnostics, including related locations.
2. Route selected-source, import, and profile parsing through it. Remove the
import-specific translator and keep upstream parser errors private.
3. Return canonically ordered diagnostics from the shared translation boundary.
Preserve existing codes, messages, primary/related coordinates, and source IDs.
Keep per-source parsing budgets and graph accounting at their current owners.
Acceptance: malformed selected/imported/profile input retains safe, correctly
attributed diagnostics. Use existing parser and import diagnostic tests; add a
small cross-entry regression only if a source path lacks equivalent coverage.
## Stage 3 — Share bounded regular-file read mechanics
Status: complete.
Completion: `source_file.go` owns regular-file preflight/open/recheck/close and
bounded reads, with private failure kinds and stat/open-only optional-missing
classification. Callers retain containment, metadata, budgets, accounting, and
limit diagnostics. Existing filesystem, FIFO, budget, and reader tests pass;
a private regression covers nondeterministic filesystem failure phases. Full
offline baseline and race suites passed with Go 1.26.7 on Linux/amd64.
Evidence: `readSource`, `importTraversal.readImport`, and `readProfileFile` repeat
preflight stat, open, post-open stat, regular-file checks, size checks, and bounded
reads in [source.go](../../source.go), [imports.go](../../imports.go), and
[profile.go](../../profile.go).
Work:
1. Introduce private helpers in `source_file.go` for regular-file opening and
bounded reading. Use narrow function parameters for stat/open operations so
callers can supply `os.Stat`/`os.Open` or root-bound methods. Do not introduce
an exported filesystem interface or substitute unrestricted opening for
`os.Root` access.
2. Keep preflight before open and post-open verification. Close every opened
file on all paths. Preserve the existing treatment of read-only file close
errors; root-directory close diagnostics are a separate responsibility.
3. Return private classified failures with enough distinction for callers to
map errors safely. Optional imports may suppress only the currently allowed
missing-target failures at stat/open, not later read failures. Do not expose
filesystem errors downstream through wrapping.
4. Keep source metadata, canonicalization, containment, per-source/aggregate
budgets, byte accounting, and caller-specific limit diagnostics outside the
shared mechanics. Pass the computed byte limit into the reader and retain
both the size precheck and limit-plus-one read enforcement.
5. Reuse bounded read mechanics for reader/byte sources only where it simplifies
existing code without changing single-use or ownership behavior. Keep secrets
bootstrap independent: its symlink, environment, and reporting policies differ.
Acceptance: source, import, profile, FIFO, containment, symlink, optional-missing,
exact-budget, and reader-ownership tests pass. Retain the deadline-isolated FIFO
regression. Cover private failure classification only where public tests cannot
reliably induce an important filesystem failure. Run race tests for this unit.
## Stage 4 — Establish shared document traversal ownership
Status: complete.
Completion: `document_walk.go` owns nil-safe preorder value walking, borrowed
indexing, and semantic lookup. Metadata shapes and projected sensitivity use the
walker with defensive path copies. Specialized origins and budget traversals
remain explicit. Full offline tests (including historical path shapes, metadata
detachment, and dynamic secrets), vet, build, and whitespace checks passed with
Go 1.26.7 on Linux/amd64.
Evidence: `indexDocument`, `walkDocumentPaths`, and `projectedSensitivity` repeat
semantic tree traversal in [load_metadata.go](../../load_metadata.go),
[metadata.go](../../metadata.go), and [secret.go](../../secret.go). General document
lookup currently lives in [load.go](../../load.go).
Work:
1. Create root-private `document_walk.go` with a nil-safe preorder walker taking
a starting `Path` and a callback receiving both path and node. Visit mappings
in stored order and sequences in index order; construct explicit Key/Index
segments. It visits value nodes, not synthetic mapping-key nodes.
2. Move `indexDocument` and `documentAtPath` to this owner. Implement indexing,
metadata shape collection, and projected sensitivity collection using the
walker where their traversal semantics match.
3. Document callback borrowing: callers must clone paths or nodes when retaining
mutable state. Preserve existing defensive copies and avoid sharing mutable
metadata across results.
4. Keep path-bearing helpers in the root package so `internal/document` does not
depend on the public package. Pure node representation and cloning stay in
`internal/document`.
5. Leave specialized traversals explicit where they need inherited origins,
pruning, node identity deduplication, or different counting. In particular,
input limits count mapping keys and retained-history limits count shared nodes
once; do not implement either using the new walker without those semantics.
Acceptance: numeric map keys versus sequence indices, escaped paths, unknown
paths, historical shape changes, deleted descendants, dynamic secret tags, and
metadata detachment remain protected by existing tests. No public path behavior
or predicate coverage changes. Do not add tests that assert walker call counts.
## Stage 5 — Separate typed projection, cloning, and comparison
Status: complete.
Completion: projection, detached cloning, shared reflection state, and value-only
comparison now have separate owners. Named secret clone modes preserve the two
ownership barriers. Validation combines value and sensitivity comparison in a
named helper and separately audits resolved secret state. Standard `cmp.Compare`
replaces the custom comparator; tests changed only for private symbol renames.
Full offline baseline and race suites passed with Go 1.26.7 on Linux/amd64,
including codec/cycle/limit, callback ownership, sensitivity, and secret-state
regressions.
Evidence: [projection.go](../../projection.go) combines roughly 622 lines of
projection, detached cloning, dynamic schema handling, cycle/limit enforcement,
and structural comparison. Value equality deliberately ignores sensitivity,
which validation must check separately.
Work:
1. Keep canonical typed projection in `projection.go`; move cloning into
`clone.go`, shared reflection operation state and cycle helpers into
`value_operation.go`, and value-tree comparison into `document_compare.go`.
Preserve the descriptor-driven approach and per-operation schema cache.
2. Rename the private equality/comparison entry points to make configuration-value
semantics explicit. Document that location and sensitivity metadata are
excluded, mapping order is immaterial, and sequences are compared atomically
for change reporting. Update all callers without changing their semantics.
3. Give validator mutation comparison a named private owner in `validation.go`.
It must combine projected value changes and sensitivity changes, while
retaining private resolved-secret-state auditing as an additional check.
Do not make resolver value comparison start treating sensitivity as a value.
4. Preserve the distinction between cloning unresolved public secret references
and publication cloning of already validated private state. Name/document
the clone mode explicitly rather than leaving an unexplained boolean at
sensitive call sites.
5. Replace custom `cmpString` calls with the standard library comparator and
remove that helper. Do not perform unrelated test-style rewrites.
Acceptance: projection/cloning tests cover nil versus empty, custom text codecs,
inline fields, dynamic types, cycles, and limits. Validator mutation regressions
must still reject equal-text sensitivity changes and secret-state changes.
Callback ownership and secret lookup tests must prove publication does not share
borrowed values or reread the environment. Run race tests for this unit.
## Stage 6 — Clarify source assembly and override ownership
Status: complete.
Completion: filesystem context/canonicalization/containment moved to
`filesystem.go`; directive parsing and restrictions moved to `directives.go`;
input node counting now shares the document owner with explicit key accounting.
Override preparation, parser-neutral value building, and application are separate;
profile and CLI application share the composition clone. Deferred profile cleanup
preserves primary failures and explicitly checks the success-path close once.
Full offline tests, vet, build, and whitespace checks passed with Go 1.26.7 on
Linux/amd64, including imports/globs, profiles, CLI, limits, and provenance.
Evidence: [imports.go](../../imports.go) mixes filesystem context, directives,
graph traversal, and diagnostics; [override.go](../../override.go) combines value
construction/validation and application. Shared composition cloning is located
inside the override feature, while profile overlay repeats it.
Work:
1. Keep import traversal and route diagnostics in `imports.go`. Move filesystem
context validation/canonicalization/containment to `filesystem.go`, and
directive structures/parsing/fragment restrictions to `directives.go`.
Give source-independent node counting an appropriate shared document owner,
with its mapping-key accounting documented.
2. Keep override validation and preparation in `override.go`, move parser-neutral
value building to `override_value.go`, and application/displacement mechanics
to `override_apply.go`.
3. Move `cloneComposedDocument` next to `composedDocument` in `compose.go`; use it
for both profile overlay and overrides. Ensure every retained field is copied
according to its existing ownership contract.
4. Keep strict composition, overlay, and history replay separate. Share small
mechanics when identical; do not introduce merge strategy flags or one general
merge engine. Preserve the existing profile-versus-CLI recording behavior.
5. Simplify profile root cleanup using deferred failure-path cleanup plus an
explicit checked success-path close. Preserve primary failure diagnostics and
the existing successful-read root-close error diagnostic; avoid double-close
effects. Import traversal cleanup may remain explicit where already clear.
Acceptance: imports/globs, directive restrictions, source ordering, profiles,
overrides, version restrictions, limits, and composition independence tests pass.
No new cross-package dependencies appear. Root handles close on every path and
profile/CLI provenance remains identical.
## Stage 7 — Extract explicit loader lifecycle phases
Status: complete.
Completion: `load.go` now visibly coordinates ordered phases using source assembly
in `load_input.go`, option selection in `options.go`, and typed phase outputs and
publication in `load_result.go`. Version checking lives in `validation.go`.
One composition initializer covers profiles, CLI, and NoSource overrides without
changing budgets. Diagnostics and source handoffs remain explicit, both ownership
clones remain, and publication adds no history copies. Full offline baseline and
race suites passed with Go 1.26.7 on Linux/amd64; lifecycle, early-stop, warning,
precedence, provenance, callback ownership, and secret tests passed unchanged.
Evidence: `loadWithSecretLookup` in [load.go](../../load.go) coordinates roughly
200 lines of source assembly, decoding, callbacks, history, secrets, and result
publication, including repeated creation of a composition handoff.
Work:
1. Keep the exported entry point and a readable linear coordinator in `load.go`.
Move option selection to `options.go`, input assembly to `load_input.go`, and
typed lifecycle/result publication to `load_result.go`. Move version checking
beside the validation machinery rather than keeping it among document helpers.
2. Use small private handoff structs for assembled input and necessary phase
outputs, not a mutable all-purpose loader object or a long list of output
parameters. Reuse one composition initialization helper for profile and CLI
processing, including NoSource override handling and source byte/node totals.
3. Preserve the exact existing order of option validation, parsing/import checks,
schema validation, version checking, profile/CLI application, defaults factory,
typed decoding, resolution, retained-history limit enforcement, secret lookup,
validation, metadata/classification, and result publication.
4. Keep diagnostic/source accumulation explicit. Warnings survive successful
phases; errors stop dependent work and never return a usable partial Result.
5. Preserve both ownership barriers: detach resolver-borrowed values before secret
resolution and validation, then detach equal-valued validator replacements
before publication while preserving validated secret state. Do not remove
either clone as apparently redundant work.
6. Group Result's frozen inspection state only if it clarifies this handoff; do
not add an additional history copy or change caller ownership of Config.
Acceptance: lifecycle ordering/early-stop tests, defaults-only loading, all input
combinations, callback ownership, secrets, warning retention, and complete
provenance agreement pass. Keep orchestration visible enough to verify phase
ordering by reading the coordinator. Run the complete race suite.
## Stage 8 — Separate explanation replay, snapshots, and rendering
Status: complete.
Completion: public records and Explain coordination remain in `explain.go`;
immutable applications, replay/coalescing, and shared-node budgeting live in
`history.go`; shared effective/explanation redaction lives in `snapshot.go`;
human output lives in `explain_render.go`. Metadata versus replay ownership is
documented beside history. Existing reconciliation, historical sensitivity,
container/leaf, retained-budget, detachment, and writer-failure tests passed
without expectation changes. Full offline tests, vet, build, and whitespace
checks passed with Go 1.26.7 on Linux/amd64.
Evidence: [explain.go](../../explain.go) combines approximately 576 lines of public
records, replay algorithms, snapshot redaction, retained-history accounting, and
human-readable output.
Work:
1. Keep public explanation records and Explain coordination in `explain.go`.
Move application/snapshot replay and history budgeting to `history.go`, safe
snapshot projection to `snapshot.go`, and WriteExplanation with its formatting
helpers to `explain_render.go`.
2. Make effective rendering and explanation snapshots use the existing shared
redaction implementation; do not create separate sensitivity rules.
3. Retain the canonical decoding reconciliation event between input replay and
domain resolution, overlapping resolution intent coalescing, added/deleted
paths, effective source attribution, and historical secret classification.
4. Document the differences between metadata origin records and value replay:
they serve different query contracts but must agree on effective results.
Do not replace them with a new event-store abstraction in this cleanup.
5. Preserve writer failure behavior and ownership of retained trees. Do not cache
explanations or optimize full-tree replay without measured need.
Acceptance: explanation, effective rendering, history budget, provenance, secret
redaction, and writer-failure tests pass without output changes. Container and
leaf histories still reconcile with the frozen effective typed configuration.
## Stage 9 — Complete ownership documentation and integration validation
Status: pending.
Work:
1. Review the final code for obsolete helpers, duplicate traversal/translation,
unread retained state, stale comments, and imports left by earlier stages.
Confirm file splits reflect discrete responsibilities rather than line quotas.
2. Update the component overview to describe the actual owners and fix its stale
claim that metadata queries are empty. Keep detailed consumer semantics in
their existing contract; repair navigation and moved-file links throughout
documentation, including this roadmap's evidence links if necessary.
3. State the retained dependency direction clearly: public lifecycle coordination
uses private source/document/schema/inspection helpers; the owned document
representation has no dependency on the root package; only the private YAML
adapter imports the upstream YAML package.
4. Run the full baseline and race suites, local documentation and dependency
boundary checks, and the complete source-candidate checker described in the
release procedure with available qualifying toolchains. Record exact versions,
host platform, results, and any unavailable platform evidence. Cross-builds
are not native execution evidence.
5. Review the accumulated diff for public API/configuration/diagnostic changes.
Expected outcome: no consumer migration or dependency change. If any contract
change is discovered, resolve it explicitly before closing this roadmap.
6. Record completion evidence and commit this final nonempty unit. Release
preparation must subsequently retire completed roadmap material and validate
the exact committed candidate under release policy. Completing this roadmap
does not create a tag, push refs, or authorize publication.
Acceptance: all stages are complete with recorded validation, existing behavioral
regressions remain protected, documentation matches the new ownership layout,
and the worktree is clean after commits. Required release gates remain visible
in the release procedure rather than being declared satisfied by this refactor.
## Open Questions
None required to begin implementation. The package boundaries, compatibility
constraints, ordered units, and acceptance criteria above are decided. Routine
private naming adjustments are acceptable when they preserve those decisions;
new behavioral discoveries require explicit evidence and disposition.