diff --git a/README.md b/README.md index a647522..00aa5ca 100644 --- a/README.md +++ b/README.md @@ -68,12 +68,14 @@ The [architecture goals](docs/policy/architecture.md#success-criteria) explain these priorities and how genuine new needs can justify extending the shared model. The [deferred scope](docs/roadmap/deferred.md) records deliberate exclusions. -The [configuration contract](docs/config.md) describes implemented behavior. The -[consumer adoption guide](docs/consumers/README.md) provides maintained simple -native-filesystem, layered, and sibling-inheritance examples alongside the migration shapes. -See the [development guide](docs/development.md) for repository orientation and the -[release procedure](docs/release.md) for maintainer-only candidate and publication -steps. +The [configuration contract](docs/config.md) describes implemented behavior, +including [input destinations](docs/config.md#input-destinations). The +[consumer adoption guide](docs/consumers/README.md) provides maintained simple, +layered, and mounted-collection examples alongside the migration shapes. The +accepted placement decision is recorded in [ADR 0011](docs/adr/0011-input-destinations.md). +See the [development guide](docs/development.md) for repository orientation and +the [release procedure](docs/release.md) for maintainer-only candidate and +publication steps. ConfigKit code is licensed under the [LGPL v3.0 or later](COPYRIGHT). diff --git a/docs/consumers/README.md b/docs/consumers/README.md index 4303427..50dd972 100644 --- a/docs/consumers/README.md +++ b/docs/consumers/README.md @@ -111,6 +111,11 @@ lifetime and precedence rules. ## Input destinations and mounted collections +Main and underlay inputs use the same destination field; imported application +contributions inherit the declaring input's destination. A destination is an +application mapping path, independent of filesystem context. The configuration +contract owns the complete behavior and validation rules. + The small [`Example_inputDestination`](../../examples/consumers/destinations_test.go) mounts one mapping source beneath literal collection/prompt keys and loads a second input without a destination to show that root placement remains the diff --git a/docs/development.md b/docs/development.md index 928cbca..481725c 100644 --- a/docs/development.md +++ b/docs/development.md @@ -39,6 +39,7 @@ public behavior. | Task | Read | | --- | --- | | Repository orientation or component responsibility | [Component overview](internal/overview.md) | +| Input destinations | [Configuration contract](config.md#input-destinations), [ADR 0011](adr/0011-input-destinations.md), and the destination tests below | | Documentation changes | The owning document | | Licensing, dependencies, attribution, or release-document changes | [Project grant](../COPYRIGHT) and [attribution](oss/attribution.md); release changes also follow the release procedure | | Architecture decisions or future work | [Deferred scope](roadmap/deferred.md) and relevant ADRs listed below | @@ -55,6 +56,7 @@ Accepted decisions: - [Explicit overrides and safe explanation](adr/0005-explicit-overrides-and-safe-explanation.md) - [Keep the public facade root-owned](adr/0006-public-facade.md) - [Compare frozen effective configuration snapshots](adr/0010-effective-configuration-diff.md) +- [Place inputs at literal application mapping paths](adr/0011-input-destinations.md) For cross-cutting changes, follow every applicable row. Completed implementation and remediation plans are preserved in Git history at `cb5b435`; use current @@ -164,6 +166,7 @@ code changes. Use the component overview to find the corresponding implementatio | Composition, selected overlays, CLI precedence | [Composition](../internal/engine/compose_test.go), [overlay](../internal/engine/overlay_test.go), [selected overlays](../internal/engine/selected_overlay_test.go), [overrides](../internal/engine/override_test.go) | `go test ./internal/engine -run 'Compose\|Overlay\|Override'` | | Sibling inheritance, provenance, history, sensitivity | [Expansion](../internal/engine/inheritance_test.go), [destination scopes](../internal/engine/inheritance_walk_test.go), [origins](../internal/engine/inheritance_origin_test.go), [history](../internal/engine/inheritance_history_test.go), [secrets](../internal/engine/inheritance_secret_test.go), [public loading](../facade_inheritance_test.go) | `go test ./internal/engine -run 'Inheritance\|Inherited'` and `go test . -run '^TestFacadeInheritance\|^TestFacadeInheritedSecrets'` | | Layered loading and source equivalence | [Grouped lifecycle](../internal/engine/load_request_test.go), [layered facade](../facade_layered_test.go), [equivalence](../facade_equivalence_test.go) | `go test ./internal/engine -run '^TestGrouped'`, `go test . -run '^TestFacadeLayered'`, and `go test . -run '^TestEquivalentSourceForms'` | +| Input destinations, mounted sources, and source ownership | [Destination engine tests](../internal/engine/destination_test.go), [tier and import tests](../internal/engine/destination_tiers_test.go), [integration and provenance tests](../internal/engine/destination_integration_test.go), [public facade tests](../facade_destination_test.go), and [consumer examples](../examples/consumers/destinations_test.go) | `go test ./internal/engine -run 'Destination|Mounted'`, `go test . -run 'Destination|FacadeMounted'`, and `go test ./examples/consumers -run 'CollectionBundle|CollectionPeer'` | | Schema, defaults, projection, cloning | [Schema](../internal/engine/schema_test.go), [decoding](../internal/engine/decode_test.go), [projection](../internal/engine/projection_test.go) | `go test ./internal/engine -run 'Schema\|Decode\|Projection\|Clone\|Inline\|CompareDocument'` | | Lifecycle, resolution, validation, callback ownership | [Loading](../internal/engine/load_test.go), [resolution](../internal/engine/resolution_test.go), [validation](../internal/engine/validation_test.go), [ownership](../internal/engine/callback_ownership_test.go), [independence](../internal/engine/independence_test.go) | `go test ./internal/engine -run 'Load\|Resolution\|Validation\|Validator\|Hook\|Callback\|Parallel'` | | Paths, metadata, provenance | [Paths](../internal/engine/path_test.go), [metadata](../internal/engine/metadata_test.go), [provenance](../internal/engine/provenance_test.go) | `go test ./internal/engine -run 'Path\|Metadata\|Provenance\|Queries'` | diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 0bc2408..009914f 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -8,8 +8,10 @@ private state; public signatures and fields do not expose engine types. Implemented component owners: - `internal/engine` owns host and native-filesystem source acquisition, - grouped strict composition, recursive precedence assembly, and destination-local - sibling inheritance before typed decoding. It also owns schema + grouped strict composition, recursive precedence assembly, per-input mapping + placement within that assembly, and destination-local sibling inheritance + before typed decoding. It retains source placement alongside physical source + identity and owns schema inspection, lifecycle coordination, callback auditing, provenance/history, passive source-context retention, secret resolution and bootstrap, safe inspection rendering, and effective-value comparison over @@ -41,11 +43,13 @@ these constraints and rejects exported aliases, fields, or signatures involving internal types. The [configuration contract](../config.md) owns implemented YAML and lifecycle -semantics, including [effective configuration diffs](../config.md#effective-configuration-diffs). +semantics, including [input destinations](../config.md#input-destinations) and +[effective configuration diffs](../config.md#effective-configuration-diffs). The [consumer adoption guide](../consumers/README.md) owns application guidance. The [architecture policy](../policy/architecture.md) owns contributor boundaries; [ADR 0006](../adr/0006-public-facade.md) records the facade decision and [ADR 0010](../adr/0010-effective-configuration-diff.md) records the diff +decision; [ADR 0011](../adr/0011-input-destinations.md) records the placement decision. See the [development guide](../development.md) for validation commands and [release procedure](../release.md) for source-candidate gates. [ADR 0009](../adr/0009-sibling-inheritance.md) records why inheritance is a built-in diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index 82d686f..796fdb1 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -99,6 +99,13 @@ types, field meanings, defaults, normalization, semantic validation, and schema version policy. Applications also own main-file search conventions, CLI wiring, remote acquisition, runtime initialization, and resource checks. +The engine applies each input's mapping destination within the existing source +assembly and retains that placement with source identity and inspection records; +the root facade owns the public fields and conversions. The +[configuration contract](../config.md#input-destinations) defines the observable +placement behavior, and [ADR 0011](../adr/0011-input-destinations.md) records the +accepted decision. + Library code must not depend on consuming applications or initialize their runtime resources. It must not perform implicit network access, discover application-specific paths, or change process-global state during ordinary `Load`. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8f5388e..b6dd1e2 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,8 +1,7 @@ # Implement configuration input destinations -Status: planned. Execute Stage 1 through Stage 12 in order, one focused stage -per Luna implementation prompt. The [feature roadmap](input-destinations.md) -owns the approved behavior and rationale; the +Status: complete. Stages 1–12 are implemented and audited. The +[feature roadmap](input-destinations.md) owns the approved behavior and rationale; the [consumer request](promptkit-feature-request.md#public-acceptance-scenarios) supplies the public acceptance scenarios. This document owns execution and completion evidence. Planning baseline: `6353598`. @@ -504,7 +503,7 @@ structural pass. ## Stage 12 — Reconcile documentation and audit the complete implementation -Status: planned. Scope: final integration audit and evidence, not release work. +Status: complete. Scope: final integration audit and evidence, not release work. 1. Update README navigation, consumer guidance, the component overview, and development reading/test maps. Add ADR 0011 to navigation. Update architecture @@ -529,6 +528,24 @@ Status: planned. Scope: final integration audit and evidence, not release work. and owning contracts agree. Retain completed planning evidence until separately authorized release preparation. +Audit evidence for the approved decisions and request acceptance surface: + +| Behavior reviewed | Evidence and what it proves | +| --- | --- | +| Root-owned `Input.Destination` and `SourceInfo.Destination`; detached conversions and publication | `TestSourceInfoDestinationConversionsAndCopies`, `TestFacadeFailedLoadExposesDetachedSourceDestination`, and `TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff` verify root/engine conversion, error records, and returned record detachment. `TestPublicFacadeBoundary` and `TestDependencyImportBoundary` verify no engine types cross the facade. | +| Literal mapping keys, parsed numeric keys, reserved structural tokens, and invalid-path preflight | `TestNormalizeDestinationUsesLiteralMappingKeys`, `TestNormalizeDestinationRejectsIndicesAndReservedKeys`, and `TestLayeredLoadPreflightsDestinationsBeforeReadingSources` cover mapping-only normalization and rejection before reader consumption; `TestFacadeDestinationRootEmptyMappingNoSourceAndInvalidPaths` verifies the public error boundary. | +| Placement of acquired bodies, empty mappings versus absence, imports, all source kinds, physical identity, and authored coordinates | `TestFacadeInputDestinationsAcrossSourceKinds` covers host/native/bytes/reader/mapping sources; `TestFacadeSameFragmentMountsAtSeparateCollectionDestinations` proves independent public collections can retain the same local key. `TestLayeredImportOnlyInputMaterializesDestination`, `TestLayeredHostImportsInheritInputDestinationTransitively`, `TestLayeredNativeFilesystemImportsInheritInputDestination`, and `TestLayeredDestinationsDoNotChangeRepeatedSourceIdentity` cover inherited import placement and unchanged identity. `TestPlaceDocumentWrapsEmptyMappingAndPreservesSourceCoordinates`, `TestPlaceDocumentPreservesAuthoredEntryCoordinates`, and `TestLayeredDestinationDiagnosticsUseEffectivePathsAndAuthoredLocations` distinguish fallback wrapper locations from authored coordinates. | +| Strict composition, explicit-wrapper equivalence, precedence, empty values, and domain `version` overrides | `TestLayeredDestinationStrictCompositionBoundaries`, `TestLayeredDestinationMatchesExplicitlyWrappedInput`, `TestEmptyMountedMappingRetainsLowerChildren`, and `TestLayeredDestinationOverlayRetainsOrdinaryValueSemantics` prove existing group and overlay semantics are retained; the latter includes ordinary field-only updates rather than whole-record replacement. | +| Main-owned version checks and imported, underlay, inheritance, overlay, CLI, and ancestor protection | `TestLayeredDestinationChecksVersionOnceOnComposedMain`, `TestLayeredDestinationVersionClaimsUsePlacedApplicationPaths`, `TestLayeredImportedVersionClaimsUseEffectiveDestination`, and `TestLayeredMountedVersionProtectionCoversInheritanceAndUpperTiers` cover effective paths, one main check, protected-path claims/ancestors, and nonclaiming mapping ancestors. `TestLoadVersionDestinationTypesKeepOrdinaryDecoding` preserves exact configured-path conversion and ordinary nested decoding. | +| Final-shape sibling inheritance, no cross-collection lookup, one lifecycle, and failure without a usable partial result | `TestLayeredDestinationInheritanceUsesFinalSiblingScopes`, `TestLayeredDestinationInheritanceDoesNotSearchOtherCollections`, `TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape`, and `TestLayeredDestinationDecodeFailureReturnsNoResultOrLaterCallbacks` exercise these behaviors. Reused inheritance, lifecycle, and callback tests cover generic behavior that destination placement does not alter. | +| Effective paths, presence, provenance/history, safe rendering, sensitivity, bilateral diff, and source-relative resources | `TestLayeredDestinationsRetainOriginsHistoryAndDetachedRecords`, `TestLayeredDestinationLifecycleAndSensitiveInspectionUseFinalShape`, `TestLayeredDestinationDiffUsesBilateralSensitivity`, `TestLayeredImportedInheritedValuesRetainResourceContexts`, and `TestFacadeMountedMainAndUnderlayInspectionResourcesAndDiff` connect mounted values to inspection. The imported-inheritance test specifically proves the inherited value keeps the declaring source's passive resource context. | +| Shared depth/node limits and unchanged root/absence accounting | `TestPlaceDocumentRootAndBounds`, `TestDestinationPlacementCountsAgainstSharedNodeBudget`, `TestMultipleMountedInputsSharePlacementCharges`, `TestImportedEmptyBodiesChargeEachDestinationWrapper`, `TestSelectedOverlayAndCLIUsePlacementNodeCharges`, and `TestNoSourceAndRootPlacementAddNoWrapperCharges` cover per-document and aggregate limits without adding a new limit class. | +| Maintained consumer usage and strict peer behavior | `Example_inputDestination`, `TestCollectionBundleAssemblesMountedPeersAndResources`, and `TestCollectionPeerPromptConflictsRemainStrict` verify the simple and representative collection examples against actual values, resources, source records, and conflict behavior. | + +The wider baseline-to-candidate review covered all 38 changed files (`6353598..2d8df22`): production changes are limited to the root facade fields/conversions, private input/import assembly and accounting, source record ownership, and the inheritance version-claim correction described above. The contract and tests do not impose identifier grammar, namespace uniqueness, main-only placement, source-read ordering, new budgets, nested version conversion, or expanded secret policy. The implementation wraps only acquired application bodies after processing source-root directives; placement does not change physical source identity, authored node locations, filesystem resolution, or later-tier semantics. Stage evidence above reuses existing general composition, resource, inheritance, version, lifecycle, sensitivity, and boundary tests where those behaviors are unchanged; mounted-specific cases are named separately. + +Completion record: audited source candidate `2d8df22dd1cb9fde8e60853da3e2260731acf2bb`. On `go1.26.8 linux/amd64`, `./scripts/check.sh` passed, including formatting, whitespace, documentation links, tests, vet, and build. `GOTOOLCHAIN=local GOPROXY=off go test -race ./...` also passed with dependency downloads disabled. No remaining implementation limitations were found; release preparation, CI, benchmark campaigns, and downstream upgrades remain separately scoped. + Done: one complete layered load supports input destinations with coherent values, diagnostics, inspection, limits, and source ownership across the approved surface. diff --git a/docs/roadmap/input-destinations.md b/docs/roadmap/input-destinations.md index afea488..fc84a68 100644 --- a/docs/roadmap/input-destinations.md +++ b/docs/roadmap/input-destinations.md @@ -1,10 +1,10 @@ # Configuration Input Destinations Roadmap -Status: design decisions approved; implementation has not started. This roadmap -responds to the [PromptKit feature request](promptkit-feature-request.md) and owns -the target state and selected design decisions. The -[implementation plan](implementation.md) owns sequencing, implementation details, -and completion evidence. +Status: implemented. This roadmap records the approved behavior and rationale +for the [PromptKit feature request](promptkit-feature-request.md). The +[implementation plan](implementation.md) records the completed sequence and +audit evidence; the [configuration contract](../config.md#input-destinations) +owns current behavior. ## Targeted end state @@ -88,9 +88,9 @@ namespace ownership enforcement, dependency solving, live reload, filename-based identity, arbitrary transformations, or new merge strategies. PromptKit owns its collection inventory, qualified IDs, and execution semantics. -## Current baseline +## Implementation baseline (v0.7.1) -The request reviewed v0.7.1. The current worktree has these relevant boundaries: +The request reviewed v0.7.1. At that baseline, the relevant boundaries were: - [Input](../../layered.go) holds a source, import opt-in, and filesystem context, but no destination. Root types delegate to the engine through the existing diff --git a/facade_destination_test.go b/facade_destination_test.go index 0ced938..e01fa96 100644 --- a/facade_destination_test.go +++ b/facade_destination_test.go @@ -153,6 +153,36 @@ func TestFacadeImportedSourcesRetainInputDestination(t *testing.T) { } } +func TestFacadeSameFragmentMountsAtSeparateCollectionDestinations(t *testing.T) { + weatherDestination := configkit.Keys("collections", "weather", "prompts") + homeDestination := configkit.Keys("collections", "homewatch", "prompts") + result, err := configkit.LoadLayered( + configkit.Input{Source: configkit.NoSource()}, + configkit.LayeredOptions[map[string]any]{Underlays: []configkit.Underlay{{Inputs: []configkit.Input{ + {Source: configkit.Bytes("weather", []byte("summarize.alerts: {title: weather}\n")), Destination: weatherDestination}, + {Source: configkit.Bytes("homewatch", []byte("summarize.alerts: {title: home}\n")), Destination: homeDestination}, + }}}}, + ) + if err != nil { + t.Fatalf("LoadLayered: %v", err) + } + for _, test := range []struct { + path []string + want string + }{ + {path: []string{"collections", "weather", "prompts", "summarize.alerts", "title"}, want: "weather"}, + {path: []string{"collections", "homewatch", "prompts", "summarize.alerts", "title"}, want: "home"}, + } { + if got := facadeDestinationValue(t, result.Config, test.path...); got != test.want { + t.Errorf("value at %v = %#v, want %q", test.path, got, test.want) + } + } + sources := result.Sources() + if len(sources) != 2 || sources[0].Destination.String() != weatherDestination.String() || sources[1].Destination.String() != homeDestination.String() { + t.Fatalf("independent collection source placements = %#v", sources) + } +} + type destinationCountingReader struct{ reads int } func (reader *destinationCountingReader) Read([]byte) (int, error) { diff --git a/internal/engine/destination_tiers_test.go b/internal/engine/destination_tiers_test.go index 463511b..64af972 100644 --- a/internal/engine/destination_tiers_test.go +++ b/internal/engine/destination_tiers_test.go @@ -131,6 +131,25 @@ func TestLayeredDestinationMatchesExplicitlyWrappedInput(t *testing.T) { } } +func TestEmptyMountedMappingRetainsLowerChildren(t *testing.T) { + t.Parallel() + + destination := Keys("collections", "weather") + loaded, err := LoadLayered( + Input{Source: Bytes("empty-main", []byte("{}\n")), Destination: destination}, + testSecretBinding(), + LayeredOptions[map[string]any]{Underlays: []Underlay{{Inputs: []Input{{ + Source: Bytes("lower", []byte("prompt: {title: retained}\n")), Destination: destination, + }}}}}, + ) + if err != nil { + t.Fatalf("LoadLayered: %v", err) + } + if got := layeredMapValue(t, testLoaded(loaded).Config, "collections", "weather", "prompt", "title"); got != "retained" { + t.Fatalf("lower child beneath empty mounted mapping = %#v, want retained", got) + } +} + func TestLayeredDestinationOverlayRetainsOrdinaryValueSemantics(t *testing.T) { t.Parallel()