# Application Fallback Profiles Implementation Plan **Status:** Ready for implementation. ## Purpose This document is the decision-complete implementation plan for [application fallback profiles](fallback-profiles.md). It is written for a `gpt-5.6-terra` coding agent that will implement each stage in order. The feature roadmap owns the motivation, policy choices, compatibility boundary, non-goals, and target end state. This document owns the concrete design, file-level work, test ownership, documentation updates, validation, and completion gates. ## Implementation Rules - Complete the stages in order. Every stage must leave the repository buildable, tested for the behavior changed in that stage, and accurately documented for its implemented state. - Preserve unrelated working-tree changes. Inspect `git status --short` and the relevant diffs before editing, and do not overwrite or reformat pre-existing user work. - Follow every policy under `docs/policy/`, the task-specific reading guide in `docs/development.md`, and the accepted behavior in `fallback-profiles.md`. - Keep the module root as the public facade. The root package owns profile source selection and composition; `internal/profile` owns repository, parsing, validation, and error-preserving overlay behavior; and `internal/profile/builtin` owns only the embedded built-in catalog. - Add only the public `WithFallbackProfileFS` option. Do not add an exported repository type, source-provenance value, fallback-specific error, config field, programmatic fallback-profile option, or public package. - Reuse `profile.NewFSRepository` and `profile.NewOverlayRepository`. Do not add another parser, validator, repository implementation, or merge model. - Preserve lazy loading. Engine construction validates the option arguments, not every file in the supplied filesystem. A profile source is read only when resolution reaches it. - Preserve existing error identities and mappings. Only `profile.ErrProfileNotFound` permits an overlay to consult its next layer; every other error from a higher layer must be returned and mapped through the existing public profile-load path. - Keep tests classical and behavior-focused. Public source precedence and exported option semantics belong in external-package root tests; generic overlay behavior remains owned by `internal/profile` tests. - Update GoDoc and current-state documentation in the same stage that exposes the public option. Do not describe the feature as implemented before that stage is complete. - Do not add release notes, change a module version, commit, tag, push, or publish a release as part of this plan. ## Fixed Design ### Public API Add this function to `engine.go` beside the existing profile-source options: ```go func WithFallbackProfileFS(fsys fs.FS, root string) Option ``` The option accepts the same filesystem and root forms as `WithProfileFS` and constructs its repository with `profile.NewFSRepository(fsys, root)`. It must return `ErrInvalidConfig` from option application when `fsys` is nil or when `strings.TrimSpace(root)` is empty. Do not normalize or replace a valid root before passing it to the repository. The fallback source is its own last-value-wins option category. Add these two private fields to `engineOptions`: ```go fallbackProfiles profile.Repository fallbackProfileSource bool ``` Use the repository field for the selected source and the boolean only to distinguish an unapplied option from an applied option. A later valid `WithFallbackProfileFS` replaces both values. Option application remains sequential, so an invalid option fails `NewEngine` immediately even if a later option could otherwise replace it. The exact GoDoc for `WithFallbackProfileFS` must state: - that it supplies application-owned fallback profile definitions; - the full four-layer lookup order; - that definitions are whole profiles and are not field-merged; - that only a missing ID falls through, while a matching read, parse, duplicate, validation, or credential-format failure stops resolution; - that loading and validation are lazy; - that files use the ordinary strict profile YAML and `api_key_env` rules; - that nil filesystems and blank roots cause `NewEngine` to match `ErrInvalidConfig`; - that repeated calls use the last valid fallback source; and - that this is definition lookup, not provider or generation failover. Update the `Option`, `Config.ProfileDir`, `WithProfileFS`, `WithProfileFile`, and `WithProfiles` GoDoc in `engine.go` where necessary so their relative precedence is unambiguous. Exact declarations and behavior remain owned by GoDoc; consumer documentation should summarize the workflow and link readers back to the API rather than reproduce every error clause. ### Repository Composition Move all profile-source composition into one private root helper in `engine.go`: ```go func newProfileRepository(profileDir string, options engineOptions) profile.Repository ``` `NewEngine` must call this helper once and pass its returned repository to the runner. The helper must build from lowest to highest precedence: 1. begin with `builtin.NewRepository()`; 2. if `options.fallbackProfileSource` is true, overlay `options.fallbackProfiles` over the built-in repository; 3. select exactly one ordinary configured source: use `options.profiles` when `options.profileSource` is true; otherwise, when `profileDir` is nonblank, use `profile.NewFilesystemRepository(profileDir)`; overlay that selected source over the current repository; 4. if `options.memorySource` is true, overlay `options.memoryProfiles` over the current repository; and 5. return the resulting chain. This preserves the existing rule that `WithProfileFS` or `WithProfileFile` replaces `Config.ProfileDir`; those sources are alternatives in one ordinary configured-source category, not two independent layers. `WithProfiles` remains a distinct highest-precedence category. The final lookup order is therefore: ```text WithProfiles -> WithProfileFile / WithProfileFS / Config.ProfileDir -> WithFallbackProfileFS -> Promptkit built-ins ``` Every arrow is a whole-profile, not-found-only fallback. Do not inspect or copy profile fields in the composition helper. ### Built-In Package Boundary Reduce `internal/profile/builtin/repository.go` to the embedded catalog leaf: ```go func NewRepository() profile.Repository ``` Remove `NewRepositoryWithPrimary` and `NewRepositoryWithDirectory`. Remove their now-obsolete tests from `internal/profile/builtin/repository_test.go` and remove imports used only by those helpers or tests. Do not move their tests to another private helper: existing root public-behavior tests own assembled precedence, and `internal/profile.TestOverlayRepository` owns not-found-only overlay semantics. Do not change built-in YAML assets, built-in validation, the `profile.Repository` interface, `profile.NewOverlayRepository`, or filesystem repository behavior. ### Resolution And Error Semantics The runner receives one assembled `profile.Repository`; do not add fallback logic to `InspectProfile`, `Prepare`, `PrepareExecution`, `Run`, or `RunPrepared`. Those paths must continue to resolve through the runner's one repository dependency. The existing overlay contract is authoritative: - a successful lookup returns the complete higher-layer profile; - `profile.ErrProfileNotFound` consults the next layer; - cancellation, filesystem read failures, malformed YAML, duplicate matches, raw `api_key`, invalid profiles, and all other errors stop lookup; and - the root facade maps failures through the existing public identities such as `ErrProfileNotFound` and `ErrProfileLoad`. Do not add eager filesystem walking in the option or `NewEngine`. A malformed asset unrelated to the requested ID retains the existing ordinary `FSRepository` behavior; this plan does not strengthen that package's global validation guarantees. ### Test Ownership Use the following test boundaries and avoid duplicating the profile parser's existing case matrix. In `public_contract_test.go`: - Add `TestFallbackProfileSourcePrecedence`. Use minimal synthetic `fstest.MapFS` profiles and, where `Config.ProfileDir` is under test, a `t.TempDir`. Cover these distinct relationships: an in-memory profile beats both ordinary and fallback definitions; an ordinary `WithProfileFS` source beats a fallback definition; `Config.ProfileDir` beats a fallback definition when no ordinary source option replaces it; a fallback definition beats a built-in definition with the same ID; and an ID absent from the fallback source still resolves from the built-in catalog. Assert the selected model or another stable complete-profile field rather than internal repository structure. - Extend `TestRepeatedOptionsUseLastValueInEachCategory` with a `fallback profile source` subtest proving that the later valid fallback filesystem is selected. - Add `TestFallbackProfileSourcePreservesLazyLoadingAndErrors`. Prove that engine construction succeeds without reading malformed fallback YAML, that an unrelated malformed file does not prevent a valid requested fallback definition from resolving under existing FS-repository semantics, that a malformed fallback file whose stem matches a built-in profile ID yields `ErrProfileLoad` instead of silently reaching the built-in, and that a malformed ordinary configured definition yields `ErrProfileLoad` instead of reaching a valid application fallback definition. Use `errors.Is`; do not assert complete error strings. - Add one representative workflow test that supplies a fallback-only profile and verifies the same effective model through `InspectProfile`, `Prepare`, a `PrepareExecution` followed by `RunPrepared`, and direct `Run`. Use the existing deterministic injected-client style, no live provider, and no real credential. This test owns the cross-workflow repository wiring; do not repeat the full precedence matrix through every method. In `engine_test.go`, extend `TestSourceOptionsRejectInvalidInputs` with nil filesystem and blank-root cases for `WithFallbackProfileFS`. Both must make `NewEngine` match `ErrInvalidConfig`. Retain `internal/profile.TestOverlayRepository` unchanged unless a genuine existing defect is found. It already owns success, not-found fallback, and non-not-found error preservation. Do not add package-private tests for the new root helper, snapshots, golden files, provider calls, or one test per profile format error already covered by `internal/profile`. ### Canonical Documentation Update current-state documentation when the option is implemented: - In `docs/formats.md`, make the source-precedence section the canonical four-layer definition lookup order. State that ordinary configured sources override application fallbacks, application fallbacks override built-ins, profiles are whole values, and only a missing ID falls through. Retain this document's ownership of strict YAML, credentials, validation, and source discovery details. - In `docs/consumers/pkg-promptkit.md`, add a short task-oriented section that shows an illustrative `embed.FS` declaration and `WithFallbackProfileFS`. Explain that application defaults belong in the embedded fallback and operator overrides belong in the ordinary configured source. Link to `docs/formats.md` for exact format and precedence rules, and do not turn the snippet into a second complete maintained application. - In `docs/internal/sources.md`, describe the root-owned four-layer composition and the existing not-found-only overlay mechanism. Remove any claim that the built-in package composes caller-selected repositories. - In `docs/internal/overview.md`, keep the root facade responsible for source assembly, describe `internal/profile/builtin` only as the embedded catalog, and reflect the implemented fallback layer without duplicating the exact public API contract. - In `engine.go`, apply the GoDoc changes under Public API. GoDoc owns the exact option signature, validation, category, and public semantics. The architecture policy already assigns assembly to the root facade and the built-in catalog to `internal/profile/builtin`; do not edit it unless the implementation reveals an actual contradiction. No integration protocol, outbound request body, profile YAML shape, README orientation, or maintained example changes as part of this feature. ## Stage 1: Move Existing Profile Composition To The Root Facade ### Objective Establish the intended ownership boundary and a single root composition point without changing public behavior or adding the fallback option. ### Implementation Prompt Implement only Stage 1 of `docs/roadmap/implementation.md`. Read the complete feature roadmap, implementation rules, and fixed design above before editing. 1. In `engine.go`, add `newProfileRepository(profileDir string, options engineOptions) profile.Repository` and move the existing three-layer assembly into it: built-ins, then the selected ordinary configured source, then `WithProfiles`. Do not add fallback fields or the public option yet. 2. Replace the inline profile assembly in `NewEngine` with one call to the helper. Preserve the existing replacement relationship between `Config.ProfileDir` and `WithProfileFS`/`WithProfileFile`. 3. In `internal/profile/builtin/repository.go`, remove `NewRepositoryWithPrimary` and `NewRepositoryWithDirectory`, leaving `NewRepository` as the only constructor. 4. Remove the three tests dedicated to the deleted built-in composition helpers from `internal/profile/builtin/repository_test.go`. Preserve tests that validate the embedded catalog itself. 5. Update `docs/internal/sources.md` and `docs/internal/overview.md` so they describe the implemented Stage 1 ownership accurately. At this boundary the source order is still in-memory, ordinary configured source, built-ins; do not document the application fallback as implemented yet. 6. Run the focused validation below. Fix in-scope regressions without adding fallback behavior early. Do not add or mention an implemented `WithFallbackProfileFS` in Stage 1. Do not change exported declarations, profile parsing, profile assets, error mapping, runner behavior, or consumer and format documentation. ### Focused Validation Run from the repository root: ```sh gofmt -w engine.go internal/profile/builtin/repository.go \ internal/profile/builtin/repository_test.go go test . -run \ 'Test(PrepareUsesBuiltInProfileWithoutProfileDir|CustomProfileOverridesBuiltInProfile|InMemoryProfilesOverrideBuiltInsAndProfileSources)$' go test ./internal/profile/... go test . ./internal/profile/... go vet . ./internal/profile/... git diff --check ``` If a focused expression does not match an existing test name, inspect the current suite and run the narrowest equivalent public precedence coverage; do not silently skip the intended relationship. ### Completion Gate Stage 1 is complete only when: - the root facade assembles the unchanged three-layer profile chain in one private helper; - ordinary source options still replace `Config.ProfileDir` and in-memory profiles still have highest precedence; - built-in profiles remain available and remain lower than consumer sources; - only `internal/profile` owns generic overlay behavior and the built-in package owns only its embedded catalog; - no public API or behavior changed; - internal current-state documentation matches that boundary; and - all focused tests, vet, formatting, and whitespace checks pass. ## Stage 2: Add Application Fallback Profiles And Public Contracts ### Objective Add the public option, insert the application fallback into the root-owned repository chain, prove its precedence and failure behavior through public workflows, and publish the canonical current-state documentation. ### Implementation Prompt Implement only Stage 2 of `docs/roadmap/implementation.md` after Stage 1 satisfies its completion gate. 1. Add `fallbackProfiles` and `fallbackProfileSource` to `engineOptions`, then implement `WithFallbackProfileFS` exactly as specified under Public API. 2. Extend `newProfileRepository` so it constructs the fixed four-layer chain in the prescribed low-to-high order. Do not alter generic overlay logic or add fallback branches to runner methods. 3. Update all affected `engine.go` GoDoc, including the option category list and the relative precedence descriptions for existing profile sources. 4. Add and extend the external-package root tests exactly as specified under Test Ownership. Reuse small existing fakes and fixture helpers where they remain clear; add only minimal synthetic YAML helpers needed by these tests. 5. Extend `TestSourceOptionsRejectInvalidInputs` with the two fallback option validation cases. 6. Update `docs/formats.md`, `docs/consumers/pkg-promptkit.md`, `docs/internal/sources.md`, and `docs/internal/overview.md` according to Canonical Documentation. 7. Run the focused validation below. Repair in-scope failures without weakening existing parser, error-identity, prepared-execution, or profile precedence guarantees. Do not add a config field, in-memory fallback API, source provenance, profile inheritance, provider failover, eager validation, application-specific assets, or backend/model policy. Do not edit `internal/profile/builtin/assets/`. ### Focused Validation Run from the repository root: ```sh gofmt -w engine.go engine_test.go public_contract_test.go go test . -run \ 'Test(FallbackProfileSource|RepeatedOptionsUseLastValueInEachCategory|SourceOptionsRejectInvalidInputs)' go test ./internal/profile/... go test . ./internal/profile/... ./internal/usecase go vet . ./internal/profile/... ./internal/usecase git diff --check ``` The focused root expression must execute the precedence, lazy/error, cross-workflow, repeated-option, and invalid-input coverage described above. If the implemented names differ slightly, run explicit equivalent expressions and record no skipped contract category. ### Completion Gate Stage 2 is complete only when: - `WithFallbackProfileFS` is the sole new public declaration and has complete, accurate GoDoc; - nil filesystem and blank root inputs fail construction with `ErrInvalidConfig`, and the last valid repeated fallback option wins; - the assembled order is in-memory, ordinary configured, application fallback, built-ins; - existing ordinary source options still replace `Config.ProfileDir`; - lookup falls through only on a missing ID and never after a matching higher-layer failure; - profiles remain whole values and loading remains lazy; - inspection, preparation, prepared execution, and ordinary execution resolve the same fallback definition through one runner repository; - no built-in asset, profile format, public error identity, provider request, or existing consumer behavior changed unintentionally; - GoDoc and all affected canonical documents describe implemented behavior without duplicating ownership; and - all focused tests, vet, formatting, links, and whitespace checks pass. ## Stage 3: Audit Compatibility And Validate The Repository ### Objective Confirm that the implementation is complete, minimal, and consistent across the public facade, internal boundaries, tests, and documentation, then mark the temporary planning documents complete. ### Implementation Prompt Implement only Stage 3 of `docs/roadmap/implementation.md` after Stage 2 satisfies its completion gate. 1. Search tracked Go and Markdown files for `NewRepositoryWithPrimary`, `NewRepositoryWithDirectory`, profile source precedence lists, and descriptions of built-in repository composition. Remove stale references and correct only feature-owned contradictions. 2. Review `newProfileRepository` directly and confirm it has exactly four possible layers in the required order, selects only one ordinary configured source, and contains no profile field merging or eager I/O. 3. Review the public tests as a suite. Confirm that each distinct risk in Test Ownership is protected once, generic parser and overlay cases remain with `internal/profile`, and no test depends on private helper shape. 4. Confirm that `internal/profile/builtin/assets/`, external wire behavior, backend configuration, credential resolution, public result shapes, and stable JSON tags have no feature-related changes. 5. Follow every added or changed Markdown link and confirm that its target and relevant heading exist. Verify that exact API details live in GoDoc, exact profile format and precedence details live in `docs/formats.md`, consumer guidance remains task-oriented, and internal documents describe only implementation responsibility. 6. Run the complete validation sequence below and repair only in-scope failures. 7. After every check passes, change the status of `fallback-profiles.md` and this document to `Complete`. Do not delete or retire either roadmap; retirement is a separate maintainer action. 8. Re-run `git diff --check`, inspect `git status --short`, and review the full diff while distinguishing pre-existing user changes from this feature. Do not add release notes, change versions, or create a commit, tag, push, or release during this stage. ### Full Validation Run from the repository root: ```sh gofmt -w engine.go engine_test.go public_contract_test.go \ internal/profile/builtin/repository.go \ internal/profile/builtin/repository_test.go gofmt -l $(git ls-files '*.go') go test ./... go test -race ./... go vet ./... go build ./... go run ./examples/go-library/prepare git diff --check git status --short ``` The `gofmt -l` command must print no paths. The maintained example must remain offline and require no real credential or provider. Inspect the final diff and confirm: - only this feature's files and pre-existing user changes are present; - no built-in asset, public value shape, stable JSON tag, provider payload, workspace file, local module replacement, generated binary, or unrelated formatting changed; - deleted built-in composition helpers have no remaining references; - the fallback option reuses the ordinary FS repository and generic overlay; - the root constructs one repository used by every resolution workflow; - the feature roadmap and this plan are both complete; and - no commit, tag, push, or release was created. ### Completion Gate The implementation is complete only when: - every Stage 1 and Stage 2 gate remains satisfied; - the ordinary and race-enabled suites pass; - vet, build, formatting, the maintained offline example, Markdown links, and whitespace checks pass; - the four-layer precedence and not-found-only fallthrough are consistent in code, GoDoc, public tests, format reference, consumer guidance, and internal documentation; - engines without `WithFallbackProfileFS` retain their previous behavior; - the public surface contains no speculative companion API or provenance; - both temporary roadmap statuses are `Complete`; and - the repository is ready for maintainer review without a commit or release having been created by this plan. ## Open Questions None. The feature roadmap and fixed design above fully specify the public API, repository composition, error and validation behavior, compatibility boundary, documentation ownership, test strategy, and staged implementation sequence.