23 KiB
Application Fallback Profiles Implementation Plan
Status: Ready for implementation.
Purpose
This document is the decision-complete implementation plan for
application fallback profiles. 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 --shortand 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 indocs/development.md, and the accepted behavior infallback-profiles.md. - Keep the module root as the public facade. The root package owns profile
source selection and composition;
internal/profileowns repository, parsing, validation, and error-preserving overlay behavior; andinternal/profile/builtinowns only the embedded built-in catalog. - Add only the public
WithFallbackProfileFSoption. Do not add an exported repository type, source-provenance value, fallback-specific error, config field, programmatic fallback-profile option, or public package. - Reuse
profile.NewFSRepositoryandprofile.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.ErrProfileNotFoundpermits 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/profiletests. - 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:
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:
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_envrules; - that nil filesystems and blank roots cause
NewEngineto matchErrInvalidConfig; - 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:
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:
- begin with
builtin.NewRepository(); - if
options.fallbackProfileSourceis true, overlayoptions.fallbackProfilesover the built-in repository; - select exactly one ordinary configured source: use
options.profileswhenoptions.profileSourceis true; otherwise, whenprofileDiris nonblank, useprofile.NewFilesystemRepository(profileDir); overlay that selected source over the current repository; - if
options.memorySourceis true, overlayoptions.memoryProfilesover the current repository; and - 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:
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:
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.ErrProfileNotFoundconsults 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
ErrProfileNotFoundandErrProfileLoad.
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 syntheticfstest.MapFSprofiles and, whereConfig.ProfileDiris under test, at.TempDir. Cover these distinct relationships: an in-memory profile beats both ordinary and fallback definitions; an ordinaryWithProfileFSsource beats a fallback definition;Config.ProfileDirbeats 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
TestRepeatedOptionsUseLastValueInEachCategorywith afallback profile sourcesubtest 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 yieldsErrProfileLoadinstead of silently reaching the built-in, and that a malformed ordinary configured definition yieldsErrProfileLoadinstead of reaching a valid application fallback definition. Useerrors.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, aPrepareExecutionfollowed byRunPrepared, and directRun. 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 illustrativeembed.FSdeclaration andWithFallbackProfileFS. Explain that application defaults belong in the embedded fallback and operator overrides belong in the ordinary configured source. Link todocs/formats.mdfor 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, describeinternal/profile/builtinonly 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.
- In
engine.go, addnewProfileRepository(profileDir string, options engineOptions) profile.Repositoryand move the existing three-layer assembly into it: built-ins, then the selected ordinary configured source, thenWithProfiles. Do not add fallback fields or the public option yet. - Replace the inline profile assembly in
NewEnginewith one call to the helper. Preserve the existing replacement relationship betweenConfig.ProfileDirandWithProfileFS/WithProfileFile. - In
internal/profile/builtin/repository.go, removeNewRepositoryWithPrimaryandNewRepositoryWithDirectory, leavingNewRepositoryas the only constructor. - 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. - Update
docs/internal/sources.mdanddocs/internal/overview.mdso 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. - 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:
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.ProfileDirand in-memory profiles still have highest precedence; - built-in profiles remain available and remain lower than consumer sources;
- only
internal/profileowns 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.
- Add
fallbackProfilesandfallbackProfileSourcetoengineOptions, then implementWithFallbackProfileFSexactly as specified under Public API. - Extend
newProfileRepositoryso 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. - Update all affected
engine.goGoDoc, including the option category list and the relative precedence descriptions for existing profile sources. - 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.
- Extend
TestSourceOptionsRejectInvalidInputswith the two fallback option validation cases. - Update
docs/formats.md,docs/consumers/pkg-promptkit.md,docs/internal/sources.md, anddocs/internal/overview.mdaccording to Canonical Documentation. - 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:
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:
WithFallbackProfileFSis 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.
- 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. - Review
newProfileRepositorydirectly 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. - 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. - 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. - 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. - Run the complete validation sequence below and repair only in-scope failures.
- After every check passes, change the status of
fallback-profiles.mdand this document toComplete. Do not delete or retire either roadmap; retirement is a separate maintainer action. - Re-run
git diff --check, inspectgit 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:
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
WithFallbackProfileFSretain 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.