503 lines
23 KiB
Markdown
503 lines
23 KiB
Markdown
# 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.
|