Files
promptkit/docs/roadmap/implementation.md

671 lines
27 KiB
Markdown

# Prompt-Independent Profile Inspection Implementation Plan
**Status:** Ready for implementation.
## Purpose
This document is the decision-complete implementation plan for
[prompt-independent profile inspection](profile-inspection.md). It is written
for a coding agent that will implement each stage in order.
The feature roadmap owns the motivation, consumer workflow, policy choices,
compatibility requirements, non-goals, and target end state. This document
owns the concrete design, file-level changes, implementation sequence, test
ownership, documentation work, validation commands, and completion gates.
## Implementation Rules
- Complete the stages in order. Keep the repository compiling and the focused
tests passing at every stage boundary.
- Preserve unrelated working-tree changes. In particular, retain the accepted
feature roadmap and the corresponding future-roadmap and downstream-wishlist
edits that may already be uncommitted.
- Follow every policy under `docs/policy/`, the task-specific reading guide in
`docs/development.md`, and the accepted behavior in
`profile-inspection.md`.
- Keep the supported API in the root `promptkit` package. Profile repositories,
backend resolution, and target assembly remain below Go's `internal/`
boundary.
- Reuse the exact profile-source, backend-membership, and execution-target
precedence used by `Prepare`, `PrepareExecution`, and `Run`. Do not create a
second profile loader, backend registry, or target-merging implementation.
- Preserve the observable behavior and error ordering of existing preparation
and execution methods. The new inspection operation must not require a
prompt, prompt default profile, request override, credential value, capacity
admission, model client, renderer, artifact reader, schema source, or
validator.
- Treat structural validation as the existing profile, backend, and effective
target invariants. Do not add new endpoint URL policy, provider
connectivity checks, reserved-extra-parameter policy, credential syntax, or
file-format validation rules as part of this feature.
- Never read, retain, return, format, or log an environment credential value.
The result may contain only the effective environment-variable name and the
direct-key-required boolean.
- Return caller-owned public values. In particular, nested
`ExecutionTarget.ExtraParams` maps, slices, and objects must not alias
engine-owned state or another inspection result.
- Keep tests lean and behavior-focused. Reuse existing profile repository,
backend registry, target precedence, and preparation tests rather than
duplicating their complete matrices.
- Update exact contracts in GoDoc with the exported declarations. Update
current-state consumer, format, and internal documentation only after the
corresponding code exists.
- Do not add release notes, change a module version, create a release, or tag a
commit as part of this work.
## Fixed Design
### Public API
Add this root-package value immediately after `ExecutionTarget` in `types.go`:
```go
// ProfileInspection is the caller-owned result of Engine.InspectProfile.
// The exact GoDoc is specified below.
type ProfileInspection struct {
ProfileID string
EffectiveModelParams ExecutionTarget
APIKeyRequired bool
}
```
Add this method to `Engine` in `engine.go`, immediately before `Prepare`:
```go
func (e *Engine) InspectProfile(
ctx context.Context,
profileID string,
) (*ProfileInspection, error)
```
Do not accept `RunRequest`, `ExecutionTargetOverride`, an API key, an
environment override, or options on this method. Exact inspection of one
explicit profile ID is the complete operation.
`ProfileInspection` has no stable JSON contract. Do not add JSON tags,
`MarshalJSON`, `UnmarshalJSON`, or a custom string representation. Ordinary Go
encoding of the exported fields is not prohibited, but consumers must not be
promised compatibility for that encoding.
The exact type and method GoDoc must establish:
- surrounding whitespace is trimmed from `profileID`; the resulting nonblank
ID is looked up exactly and case-sensitively;
- the result applies the engine's ordinary in-memory, configured-source, and
built-in profile precedence;
- `EffectiveModelParams` contains framework defaults overlaid by the selected
backend and then the selected profile, with no request override;
- `EffectiveModelParams.BackendID` is empty for endpoint-only profiles;
- `EffectiveModelParams.APIKeyEnv` is an environment-variable name and never
its value;
- `APIKeyRequired` means a direct request credential is required and is
mutually exclusive with a nonblank effective `APIKeyEnv`;
- the method never derives an ID from a prompt's `default_profile`;
- the method performs no prompt lookup, rendering, artifact or schema work,
backend admission, provider connectivity check, or model generation;
- credential availability is not checked, so an absent or blank named
environment variable is not an error;
- the returned value and all nested mutable values are caller-owned;
- filesystem-backed inspection is a point-in-time lookup and does not freeze
a profile for a later execution;
- a nil engine matches `ErrInvalidConfig`;
- a blank ID matches `ErrInvalidRequest`;
- an absent exact ID matches `ErrProfileNotFound` and not `ErrProfileLoad`;
- malformed or unreadable profile data, an unknown backend, or an invalid
resolved target matches `ErrProfileLoad`;
- cancellation observed during profile loading matches `ErrProfileLoad` while
preserving the context error through `errors.Is`; and
- the method returns no partial result on error.
Update the `Engine` type GoDoc to include `InspectProfile` among the operations
safe for concurrent calls. Do not imply that injected collaborators become
safe when their existing contracts do not provide that guarantee.
Update `doc.go` in the same stage:
- include `Engine.InspectProfile` in the package's operation list;
- include inspection in the concurrency and caller-ownership summary; and
- list `ProfileInspection` among construction and inspection values without a
stable JSON representation.
Do not add `ProfileInspection` to the stable JSON list.
### Internal Result
Add this internal value to `internal/domain/domain.go` near
`ExecutionProfile` and `ExecutionTarget`:
```go
// ProfileInspection is the resolved result of exact profile inspection.
type ProfileInspection struct {
ProfileID string
EffectiveModelParams ExecutionTarget
APIKeyRequired bool
}
```
Do not add JSON or YAML tags. The internal value is a use-case result, not a
file format or persistence contract.
Add a root conversion in `convert.go`:
```go
func fromDomainProfileInspection(
inspection *domain.ProfileInspection,
) *ProfileInspection
```
The conversion must:
- return `nil` for a nil internal value;
- copy the scalar fields;
- convert the target through the existing `fromDomainExecutionTarget`; and
- therefore use the existing recursive `copyAnyMap` boundary for nested extra
parameters.
Do not add `APIKeyRequired` to the public `ExecutionTarget`. Keeping the
requirement on `ProfileInspection` preserves the existing stable JSON and
general execution-target contract.
### Shared Profile Selection
Add `internal/usecase/profile_inspection.go`. Define one private selection
value:
```go
type resolvedProfileSelection struct {
id string
profile *domain.ExecutionProfile
backend *domain.Backend
}
```
Add a private runner helper:
```go
func (r *Runner) resolveProfileSelection(
ctx context.Context,
profileID string,
) (*resolvedProfileSelection, error)
```
The helper must perform these operations in order:
1. trim surrounding whitespace from `profileID`;
2. reject a blank result with `ErrInvalidRequest`;
3. reject a nil runner profile repository with `ErrProfileLoad` rather than
panicking;
4. call the existing `profile.Repository.GetProfile` exactly once;
5. wrap every repository error with `ErrProfileLoad` while preserving the
underlying error with `%w`;
6. reject a nil profile returned without an error as `ErrProfileLoad`;
7. make a value copy of the selected profile before normalization so the
runner does not mutate repository-owned state;
8. trim the copied profile's backend ID;
9. when that ID is nonblank, resolve it exactly once through the existing
`BackendResolver`, preserving the current unknown-backend
`ErrProfileLoad` wrapping and backend ID context; and
10. return the normalized exact ID, copied profile, and optional defensive
backend value.
Do not inspect all sources, enumerate profiles, expose a source path, fall back
after a malformed higher-precedence match, or infer a backend from an endpoint
or model.
Refactor the profile-loading and backend-resolution block in
`Runner.resolvePreparation` to call `resolveProfileSelection`. Leave prompt
selection, prompt default-profile selection, prompt hashing, target overrides,
request credentials, output-contract resolution, artifact work, and all later
ordering where they currently occur.
This refactor must preserve:
- explicit request profile selection over prompt `default_profile`;
- the existing `ErrProfileRequired` result when neither exists;
- exact repository and backend lookup behavior;
- existing public not-found versus profile-load classification;
- request override precedence and target-presence tracking;
- credential checking during ordinary preparation; and
- current `Prepare`, `PrepareExecution`, `Run`, and `RunPrepared` behavior.
### Shared Target Resolution And Structural Validation
Continue using the existing `resolveExecutionTarget` for framework, backend,
profile, and optional request precedence. Do not duplicate or move the
individual merge rules into the inspection code.
Extract the two existing effective-target requiredness checks from
`resolvePreparation` into a private pure helper:
```go
func validateResolvedExecutionTarget(
target domain.ExecutionTarget,
) error
```
It checks only that the trimmed endpoint and model are nonblank and returns a
plain descriptive error. It does not validate URL syntax, contact the
endpoint, validate credentials, or introduce new parameter rules.
`resolvePreparation` calls this helper at the same current point: after
request override resolution and direct API-key assignment, and before
`validateAPIKey`. It wraps a failure with `ErrInvalidRequest`, preserving
ordinary preparation behavior.
Add the internal inspection method:
```go
func (r *Runner) InspectProfile(
ctx context.Context,
profileID string,
) (*domain.ProfileInspection, error)
```
It performs these operations:
1. trim and reject a blank ID as `ErrInvalidRequest`;
2. if the supplied context is already canceled, return an error wrapping both
`ErrProfileLoad` and `ctx.Err()` without touching the repository;
3. call `resolveProfileSelection`;
4. call `resolveExecutionTarget` with the selected backend, selected profile,
and a nil request override;
5. wrap any target-resolution error with `ErrProfileLoad`;
6. call `validateResolvedExecutionTarget` and wrap a failure with
`ErrProfileLoad`;
7. defensively clear `target.APIKey`;
8. copy `target.APIKeyRequired` into the result's `APIKeyRequired`; and
9. return the normalized profile ID and effective target.
Do not call `validateAPIKey`, `os.Getenv`, prompt repositories, artifact
readers, renderers, validators, capacity admission, output repair, or the
model client. Do not populate request target-presence state.
The internal `ExecutionTarget` may retain its private `APIKeyRequired` field;
the root conversion intentionally omits that private field from the public
target and publishes the separate inspection boolean.
### Root Facade And Error Mapping
`Engine.InspectProfile` follows the existing facade pattern:
1. reject a nil engine or nil runner with `ErrInvalidConfig`;
2. pass the context and string ID directly to `Runner.InspectProfile`;
3. map internal failures through the existing `mapPublicError`; and
4. convert a successful result with `fromDomainProfileInspection`.
No request conversion is needed. Do not add a public sentinel or typed error.
The existing error mapping already has the required ordering:
- an underlying `profile.ErrProfileNotFound` maps to
`ErrProfileNotFound` before the enclosing use-case `ErrProfileLoad` is
considered;
- other use-case `ErrProfileLoad` failures map to `ErrProfileLoad`; and
- `usecase.ErrInvalidRequest` maps to `ErrInvalidRequest`.
Do not reorder or otherwise change `errors.go` unless a focused test proves
that the existing mapping does not meet this plan. Preserve underlying
repository, backend, and context errors through `errors.Is`.
### Credentials, Ownership, And Concurrency
Inspection has no input through which a direct credential value can enter.
Backend and file-profile `APIKeyEnv` values remain names only. An in-memory
profile with `APIKeyRequired` clears an inherited backend environment name
through the existing target merge behavior.
The implementation must succeed for:
- a backend or profile naming an unset environment variable;
- a backend or profile naming an environment variable whose value is blank;
- an in-memory profile requiring a direct key when no key is supplied; and
- a profile requiring no credential.
The result expresses these effective states:
| `EffectiveModelParams.APIKeyEnv` | `APIKeyRequired` | Meaning |
| --- | --- | --- |
| nonblank | `false` | The profile resolves to the named environment source. |
| blank | `true` | A later execution must supply a direct key or explicit request environment override. |
| blank | `false` | The resolved target declares no credential requirement. |
The implementation must never produce a successful public result with both a
nonblank `APIKeyEnv` and `APIKeyRequired == true`.
Public ownership is enforced at the root conversion boundary. A consumer may
mutate the returned target and arbitrarily nested JSON-compatible extra
parameters without affecting:
- the engine registry or profile repository;
- a later `InspectProfile` call;
- `Prepare`, prepared execution, or `Run`; or
- another result already returned to a caller.
No new mutable engine state, cache, global registry, lock, or goroutine is
needed. Concurrency safety follows from the existing immutable registry and
repository contracts plus per-call values.
### Test Ownership
Add focused internal tests in
`internal/usecase/profile_inspection_test.go`. Use small repository and backend
fakes already present in the package where practical; do not build a parallel
fixture framework.
The internal tests own:
- blank-ID rejection;
- pre-canceled context classification without repository access;
- one exact profile and backend lookup;
- framework-default, backend, and profile target precedence through the
existing resolver;
- endpoint-only behavior;
- effective environment-name reporting without availability checks;
- direct-key-required behavior clearing an inherited backend environment
name;
- missing-profile and unknown-backend wrapping; and
- nil repository, nil returned profile, and invalid resolved target defenses
only if these cases are not already cheaply covered through existing runner
fakes.
Keep the internal matrix compact. Profile parser tests continue to own YAML,
duplicates, raw-key rejection, ranges, and source discovery. Backend tests
continue to own registration validation. Existing target tests continue to
own every merge field and numeric override boundary.
Add external-package public contract tests in `public_contract_test.go`. They
own:
- the exported method and result shape through normal Go use;
- a nil engine returning `ErrInvalidConfig`;
- blank, missing, malformed, and unknown-backend public error identities,
including that not-found does not also match `ErrProfileLoad`;
- a pre-canceled inspection preserving both `ErrProfileLoad` and the context
error without consulting the profile repository;
- successful inspection with an absent credential environment value;
- no prompt dependency, demonstrated with an empty configured prompt
`fs.FS`;
- no model invocation, using the existing deterministic fake client;
- the three credential-requirement result states;
- an endpoint-only profile's empty backend ID;
- deep caller ownership of nested extra parameters across repeated
inspections; and
- representative equivalence between `InspectProfile.EffectiveModelParams`
and `Prepare.EffectiveModelParams` for the same engine, profile, and source
state with no request execution override.
Combine closely related assertions into a few readable behavioral tests.
Do not add a JSON golden test because `ProfileInspection` deliberately has no
stable JSON contract. Do not duplicate the complete repository-precedence,
target-field, parser-error, or credential-execution suites at the root layer.
Existing tests that must continue passing without semantic edits include:
- profile repository and built-in fallback tests;
- backend registry defensive-copy and lookup tests;
- execution-target precedence tests;
- `Prepare` and `Run` profile/backend equivalence tests;
- prepared-execution snapshot and credential tests; and
- public error mapping tests.
### Documentation Ownership
After the implementation and public tests pass, update current-state
documentation:
- `docs/consumers/pkg-promptkit.md`: add a concise task-oriented section near
profile selection showing `Engine.InspectProfile`, explaining when to use it
instead of a synthetic `Prepare`, how to interpret `APIKeyEnv` and
`APIKeyRequired`, and that credential enforcement timing remains
application policy. Link to the exact GoDoc rather than restating every
error and field contract.
- `docs/formats.md`: update profile selection and backend-membership wording
so exact inspection is recognized as another consumer of the existing
source and profile precedence. Do not redefine the exported method here.
- `docs/internal/runner.md`: describe the shared profile-selection boundary
and explain that inspection stops after structural target resolution,
before credential availability and all prompt-dependent work.
- `docs/internal/sources.md`: record that exact profile inspection performs
one point-in-time profile-source lookup without reading prompt, input, or
schema sources, and replace any ambiguous use of “inspection value” for
`PreparedRun` with “preparation value.”
- `docs/internal/overview.md`: add profile inspection to the existing root
facade and `internal/usecase` responsibility descriptions. Do not add a new
component or package row.
- `docs/roadmap/future.md`: remove the statement that profile inspection is in
active planning and leave the remaining unselected ideas intact.
- `docs/roadmap/notarius-promptkit-wishlist.md` and
`docs/roadmap/weatherreporter-promptkit-wishlist.md`: change the profile
inspection disposition from accepted planning to implemented behavior and
link to the durable consumer guidance or GoDoc rather than treating the
roadmap as current-state documentation.
- `docs/roadmap/profile-inspection.md`: change its status to `Complete` only
after the code, tests, current-state documentation, and full validation are
complete.
- `docs/roadmap/implementation.md`: change its status to `Complete` only after
every completion gate in this plan is satisfied.
Do not update release guidance in this feature implementation. A later release
pass decides whether the change warrants a supplemental release document.
## Stage 1: Implement Shared Internal Profile Resolution
### Objective
Add the internal inspection result and operation, share profile/backend
selection and structural target validation with ordinary preparation, and
prove the internal behavior without publishing the root API yet.
### Implementation Prompt
Implement only Stage 1 of
`docs/roadmap/implementation.md`. Read the complete feature roadmap and the
implementation rules and fixed design above before editing.
1. Add `domain.ProfileInspection` to `internal/domain/domain.go` without
serialization tags.
2. Add `internal/usecase/profile_inspection.go` with
`resolvedProfileSelection`, `resolveProfileSelection`,
`validateResolvedExecutionTarget`, and `Runner.InspectProfile` exactly as
specified.
3. Refactor `Runner.resolvePreparation` in `internal/usecase/runner.go` to use
the shared selection and target-validation helpers while preserving its
current ordering, error classification, target overrides, credentials, and
results.
4. Add lean behavioral tests in
`internal/usecase/profile_inspection_test.go`.
5. Run the focused validation below and repair regressions before ending the
stage.
Do not add the root public type or method, update current-state documentation,
or alter profile formats, backend registration, credential availability,
capacity, model-client, or provider behavior in this stage.
### Focused Validation
Run:
```sh
gofmt -w internal/domain/domain.go \
internal/usecase/profile_inspection.go \
internal/usecase/profile_inspection_test.go \
internal/usecase/runner.go
go test ./internal/usecase ./internal/profile ./internal/profile/builtin \
./internal/backend
go test ./internal/usecase -run \
'TestRunner(InspectProfile|Prepare|Run|PrepareExecution|RunPrepared)'
go vet ./internal/usecase ./internal/profile ./internal/backend
```
### Completion Gate
Stage 1 is complete only when:
- inspection reaches profile and backend resolution without any prompt or
execution collaborator;
- profile/backend selection is shared with ordinary preparation;
- target merging and requiredness checks are shared rather than duplicated;
- inspection does not check credential values or capacity;
- internal errors preserve the required identities;
- the ordinary preparation and execution tests still pass without weakened
assertions; and
- no root public API or current-state documentation claims the feature yet.
## Stage 2: Publish The Root Facade And Public Contract
### Objective
Expose the minimal caller-owned inspection API through `Engine`, preserve
public error and JSON compatibility, and protect the consumer-visible
contract.
### Implementation Prompt
Implement only Stage 2 of
`docs/roadmap/implementation.md` after Stage 1 satisfies its completion gate.
Re-read the fixed public API, error, credential, ownership, and test sections
before editing.
1. Add `ProfileInspection` and its exact GoDoc to `types.go` immediately after
`ExecutionTarget`. Do not add JSON tags or change `ExecutionTarget`.
2. Add `fromDomainProfileInspection` to `convert.go` using
`fromDomainExecutionTarget`.
3. Add `Engine.InspectProfile` and its exact GoDoc to `engine.go` immediately
before `Prepare`.
4. Update `Engine` GoDoc to include concurrent inspection.
5. Update the package GoDoc in `doc.go` for operation discovery, concurrency,
ownership, and the non-stable JSON classification.
6. Add compact external-package contract coverage in
`public_contract_test.go`, using existing fixtures and fakes where they
remain clear.
7. Confirm that the existing `errors.go` mapping meets the plan; do not change
it unless a required public identity test fails for a genuine mapping
reason.
8. Run the focused validation below and repair regressions before ending the
stage.
Do not add enumeration, request overrides, profile fingerprints, JSON
stability, prompt inspection, environment credential checks, caching, or
current-state prose documentation in this stage.
### Focused Validation
Run:
```sh
gofmt -w doc.go types.go convert.go engine.go public_contract_test.go
go test .
go test ./internal/usecase
go test . -run 'Test(InspectProfile|.*Profile.*PublicError|.*Profile.*Contract)'
go vet .
go build .
```
If the repository's actual focused test names differ, use the implemented test
names rather than weakening or skipping the intended assertions.
### Completion Gate
Stage 2 is complete only when:
- a consumer can call `Engine.InspectProfile` through the root package;
- the result contains only the normalized ID, caller-owned effective target,
and direct-key-required signal;
- no credential value can enter the result;
- environment, direct, and no-credential states are unambiguous;
- nil, blank, missing, malformed, unknown-backend, and cancellation errors
have the required public identities;
- not-found remains distinct from profile-load failure;
- the effective target matches ordinary preparation absent request overrides;
- repeated calls and caller mutation cannot alter engine-owned state;
- no stable JSON or new error contract was introduced; and
- existing public preparation, execution, and stable JSON tests remain
unchanged and passing.
## Stage 3: Update Documentation And Validate The Repository
### Objective
Make implemented profile inspection discoverable in its canonical
documentation, reconcile temporary roadmap state, and complete full repository
validation.
### Implementation Prompt
Implement only Stage 3 of
`docs/roadmap/implementation.md` after Stages 1 and 2 satisfy their completion
gates.
1. Update `docs/consumers/pkg-promptkit.md`, `docs/formats.md`,
`docs/internal/runner.md`, `docs/internal/sources.md`, and
`docs/internal/overview.md` according to the documentation ownership
section above.
2. Update the future catalog and both downstream wishlist dispositions so they
no longer describe profile inspection as merely accepted work.
3. Check every changed Markdown link and confirm its file and heading target.
4. Run the full validation sequence below.
5. Only after every check passes, set the feature roadmap and this
implementation plan to `**Status:** Complete.`
6. Re-run `git diff --check` after the status edits.
Do not add release notes, an example program, a new public package, or a
duplicate API reference. Keep detailed contracts in GoDoc and task-oriented
usage in the consumer guide.
### Full Validation
Run from the repository root:
```sh
gofmt -w internal/domain/domain.go \
internal/usecase/profile_inspection.go \
internal/usecase/profile_inspection_test.go \
internal/usecase/runner.go \
doc.go types.go convert.go engine.go public_contract_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 must not require a real credential or provider.
Inspect the final diff and confirm:
- only files required by this feature and pre-existing user changes are
present;
- no credential values, private infrastructure details, workspace files,
local replacements, generated binaries, or unrelated formatting changes
were added;
- the public declarations and GoDoc own exact API behavior;
- current-state documentation describes only implemented behavior and links
to canonical owners;
- roadmap documents contain future scope or completion status rather than a
duplicate current API reference; and
- no release, commit, or tag was created.
### Completion Gate
The implementation is complete only when:
- every Stage 1 and Stage 2 gate remains satisfied;
- the complete ordinary and race-enabled suites pass;
- vet, build, formatting, the maintained offline example, Markdown links, and
whitespace checks pass;
- consumer, format, internal, future, and wishlist documentation are
consistent with the implemented boundary;
- both roadmap statuses are `Complete`;
- the working tree contains no unintended files or changes; and
- the repository is ready for maintainer review without a commit or release
having been created by this plan.
## Open Questions
None. The accepted feature roadmap and the fixed decisions above fully specify
the implementation boundary.