2026-07-04 11:38:14 -05:00
# Built-In Profiles And Library API Implementation Plan
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
This plan implements the target states in:
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
- `docs/roadmap/builtins.md`
- `docs/roadmap/library.md`
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Audience: LLM coding agents implementing the work in order. Review and follow `docs/policy/architecture.md` , `docs/policy/development.md` , and `docs/policy/documentation.md` before changing code.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
## Global Constraints
- Implement built-in profiles before the public library production upgrades.
- Keep orchestration in `internal/usecase` ; adapters and the public package should translate inputs and wire components.
- Keep `internal/*` packages internal. Public package types must remain facade types.
- Do not add a mutable global profile registry.
- Do not add a credential resolver or secret-manager abstraction.
- Do not accept raw API keys in YAML/JSON config, profile files, prompt files, CLI flags, or HTTP request bodies.
- Do not emit raw API keys in prepared output, run results, logs, or examples.
- Prefer standard library APIs. Do not add dependencies unless a later implementation prompt explicitly approves one.
2026-07-04 08:13:10 -05:00
- Keep each stage passing `go test ./...` before moving to the next stage.
2026-07-04 11:38:14 -05:00
## Stage 1: Profile Repository Foundations
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Goal: add reusable profile repository primitives that support built-ins without changing runner behavior.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Implementation Steps
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. Add an `fs.FS` -backed profile repository in `internal/profile` .
- Constructor shape should be similar to `NewFSRepository(fsys fs.FS, root string) Repository` .
- It must scan YAML files recursively below `root` .
- It must use the same strict YAML decoding, profile validation, raw `api_key` rejection, and duplicate-ID behavior as the existing filesystem repository.
- It must return the existing profile package sentinel errors where applicable.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
2. Refactor shared profile-loading behavior.
- Avoid duplicating validation and metadata logic between filesystem and `fs.FS` repositories.
- The existing `NewFilesystemRepository(dir)` API should remain available.
- It may either delegate to the `fs.FS` repository through `os.DirFS` or share unexported loader helpers.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
3. Add an overlay profile repository in `internal/profile` .
- Constructor shape should be similar to `NewOverlayRepository(primary, fallback Repository) Repository` .
- Lookup must return the primary result when primary succeeds.
- Lookup must fall back only when `errors.Is(err, profile.ErrProfileNotFound)` for the primary.
- Lookup must return primary load/validation errors directly and must not fall back after those errors.
- Nil repository inputs should be handled deliberately. Prefer treating nil primary as "no primary" and requiring a non-nil fallback for built-in-only operation.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
### Tests
Add focused tests under `internal/profile` .
Required coverage:
- `NewFSRepository` loads valid profiles from nested directories.
- `NewFSRepository` rejects unknown YAML fields.
- `NewFSRepository` rejects raw `api_key` in the selected profile.
- `NewFSRepository` ignores raw `api_key` in non-selected profiles, matching current filesystem behavior.
- `NewFSRepository` rejects duplicate IDs within one source.
- `NewFilesystemRepository` still satisfies all existing repository tests.
- `NewOverlayRepository` returns primary matches before fallback matches.
- `NewOverlayRepository` falls back on primary not found.
- `NewOverlayRepository` does not fall back after a primary invalid YAML/profile/raw-key error.
- `NewOverlayRepository` returns not found when both sources miss.
### Verification
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Run:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
```bash
go test ./internal/profile
go test ./...
2026-07-04 08:13:10 -05:00
```
2026-07-04 11:38:14 -05:00
## Stage 2: Built-In Profile Assets And Wiring
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Goal: compile built-in profiles into Scriptorium and make profile lookup use user-over-built-in precedence.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Implementation Steps
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. Add an `internal/profile/builtin` package.
- Store built-in profile YAML files in a stable asset directory under that package.
- Use Go `embed` to compile those files into the binary/package.
- Expose a constructor such as `builtin.NewRepository() profile.Repository` .
- The repository should use the `fs.FS` profile repository from Stage 1.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
2. Add built-in profile validation tests.
- Tests should load every built-in profile through the real profile loader.
- Tests should fail if the built-in profile set contains duplicate IDs.
- Tests should fail if any built-in profile contains raw `api_key` .
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
3. Add built-in profiles.
- Use `docs/roadmap/profiles/` as the source catalog for the initial built-in profile set.
- Copy those YAML files into the built-in profile asset directory, preserving provider subdirectories unless the implementation has a clear reason to flatten them.
- Do not invent a broad provider/model catalog.
- Do not add raw API keys.
- Built-in profiles may use `api_key_env` for CLI/HTTP compatibility when the provider requires authentication.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
4. Wire repositories through a small helper.
- Add an internal helper near adapter wiring, or in `internal/profile` , that returns:
- built-in repository only when no custom profile source is configured;
- overlay repository when a custom profile source is configured.
- The runner should still receive only a `profile.Repository` .
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
5. Make `profile_dir` optional.
- CLI `run` , CLI `render` , and HTTP `serve` argument/config validation should require `prompt_dir` but no longer require `profile_dir` .
- Public `NewEngine` should no longer reject an empty `Config.ProfileDir` .
- When `profile_dir` is empty, wire only built-ins.
- When `profile_dir` is non-empty, wire filesystem profiles over built-ins.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Tests
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Add or update tests under `internal/adapter/cli` , `internal/adapter/http` , root public package tests, and profile/builtin tests.
Required coverage:
- CLI parse/config tests accept missing `profile_dir` when `prompt_dir` is present.
- HTTP serve parse/config tests accept missing `profile_dir` when `prompt_dir` is present.
- Public `NewEngine` accepts missing `ProfileDir` .
- A built-in profile ID can be selected with no custom `profile_dir` .
- A prompt `default_profile` can refer to a built-in profile ID.
- A custom profile in `profile_dir` overrides a built-in with the same ID.
- A malformed selected custom profile does not fall back to a built-in with the same ID.
- A missing profile ID still maps to the existing profile-not-found behavior.
- Existing duplicate-ID tests for filesystem profiles continue to fail within the custom source.
### Documentation
After code behavior exists, update non-roadmap docs:
- `docs/config.md` : document `profile_dir` as optional and describe built-in fallback/override behavior.
- `docs/cli.md` : remove claims that `--profile-dir` is required.
- `docs/internal/adapters.md` : document built-in profile repository composition.
- Any affected examples or README snippets that imply `profile_dir` is mandatory.
Do not document built-in profile IDs outside implemented assets.
### Verification
Run:
```bash
go test ./internal/profile ./internal/adapter/cli ./internal/adapter/http .
go test ./...
go run ./cmd/scriptorium render \
--config ./examples/config.yml \
--prompt generic.markdown_summary \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--format json
2026-07-04 09:09:29 -05:00
```
2026-07-04 11:38:14 -05:00
Also run a new smoke command that uses a built-in profile without `--profile-dir` once a concrete built-in profile ID is available.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
## Stage 3: Public API Credential Value
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Goal: support the production library credential model: a direct API-key Go value, without adding a resolver or accepting raw keys in serialized config.
### Public API Decision
Use a single public credential-supply method:
2026-07-04 09:09:29 -05:00
```go
2026-07-04 11:38:14 -05:00
type RunRequest struct {
// existing fields...
APIKey string `json:"-"`
}
2026-07-04 08:13:10 -05:00
```
2026-07-04 11:38:14 -05:00
Do not add `Config.APIKey` , `WithAPIKey` , or a credential resolver in this stage. A request-level value avoids storing secrets on long-lived engines and supports per-tenant callers.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Implementation Steps
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. Add direct API-key plumbing through internal request/target types.
- Add an internal direct API-key field where needed, with `json:"-"` and `yaml:"-"` tags.
- Convert public `RunRequest.APIKey` into the internal request.
- Carry the value to the effective execution target used for LLM generation.
- Do not include the value in prepared/run public results, formatted output, logs, hashes, or docs examples.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
2. Update credential validation.
- If a selected/effective profile requires authentication and a direct API key is provided, do not require the environment variable to be set for the public path.
- Preserve existing CLI/HTTP behavior that uses `api_key_env` .
- Preserve existing errors for missing environment variables in CLI/HTTP paths.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
3. Update the OpenAI-compatible LLM client.
- Prefer the direct API-key value when present.
- Fall back to existing `api_key_env` behavior for CLI/HTTP compatibility.
- Never serialize or log the direct API-key value.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
4. Update public LLM injection conversion.
- Do not expose the raw API key to injected public `LLMClient` implementations unless that is strictly necessary for custom LLM execution.
- If custom LLM clients need the key, expose it only on the public `GenerateRequest` with `json:"-"` and document that fake/test clients should avoid logging it.
2026-07-04 09:09:29 -05:00
### Tests
2026-07-04 11:38:14 -05:00
Required coverage:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
- Public `Run` can call the default OpenAI-compatible client path with a direct API key without requiring the configured `api_key_env` environment variable.
- CLI/HTTP behavior using `api_key_env` still works.
- Missing credentials still fail clearly when a selected profile requires authentication and neither direct key nor usable env value is available.
- Public `PreparedRun` and `RunResult` JSON do not include the direct API key.
- Formatted prepared output does not include the direct API key.
- Direct API-key values are not included in hashes.
- Injected fake LLM tests either receive no key or receive it only through a `json:"-"` field, depending on the implementation choice above.
2026-07-04 08:13:10 -05:00
### Verification
Run:
```bash
2026-07-04 11:38:14 -05:00
go test ./internal/llm ./internal/usecase ./internal/adapter/cli ./internal/adapter/http .
2026-07-04 08:13:10 -05:00
go test ./...
```
2026-07-04 11:38:14 -05:00
## Stage 4: Public Asset Source Options
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Goal: let library consumers load standard Scriptorium prompt/profile/schema assets from directories, single files, and `fs.FS` sources.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
### Public API
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Keep existing `Config` directory fields working. Add options:
2026-07-04 09:09:29 -05:00
```go
2026-07-04 11:38:14 -05:00
func WithPromptFS(fsys fs.FS, root string) Option
func WithPromptFile(path string) Option
func WithProfileFS(fsys fs.FS, root string) Option
func WithProfileFile(path string) Option
func WithSchemaFS(fsys fs.FS, root string) Option
func WithSchemaFile(path string) Option
2026-07-04 09:09:29 -05:00
```
2026-07-04 11:38:14 -05:00
Rules:
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
- Directory `Config` fields remain the compatibility path.
- Explicit options override the corresponding `Config` directory field.
- Prompt source is required.
- Profile source is optional because built-in profiles exist.
- Schema source is optional and should default to the current schema default behavior when not configured.
- Nil `fs.FS` values, empty required roots, and invalid option combinations return `ErrInvalidConfig` .
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Implementation Steps
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. Add `fs.FS` prompt-definition support.
- Implement an `fs.FS` prompt repository that preserves existing strict prompt YAML behavior.
- Preserve prompt lookup by YAML `id` , not path.
- Preserve duplicate prompt ID errors within one source.
- Resolve `content_file` relative to the prompt file's directory inside the same source.
- Keep the existing filesystem repository API; it may delegate to the `fs.FS` implementation.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
2. Add public prompt source wiring.
- `Config.PromptDir` wires the filesystem repository.
- `WithPromptFS` wires the `fs.FS` repository.
- `WithPromptFile(path)` wires a single-file source and must still select by prompt YAML `id` .
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
3. Add public profile source wiring.
- Reuse the Stage 1 profile `fs.FS` repository.
- `WithProfileFS` and `WithProfileFile` become overlay primaries above built-ins.
- Empty profile source still means built-ins only.
4. Add schema `fs.FS` support.
- Extend or wrap the standard validator so schema files can be loaded from an `fs.FS` source.
- `WithSchemaFS` should preserve existing `schema_path` semantics.
- `WithSchemaFile(path)` should expose the file by its base name; prompts using it should set `schema_path` to that base name.
5. Keep adapter scope narrow.
- This stage is for the public package and shared repositories/validators.
- Do not change CLI/HTTP request shapes for prompt or schema `fs.FS` sources.
### Tests
Required coverage:
- Public `Prepare` works with prompt definitions from `embed.FS` .
- `content_file` references resolve relative to the prompt file in `embed.FS` .
- Public `Prepare` works with `WithPromptFile` .
- Public `Run` or `Prepare` works with `WithProfileFS` over built-ins.
- Public `Run` or `Prepare` works with `WithProfileFile` over built-ins.
- Public structured-output schema validation works with `WithSchemaFS` .
- `WithSchemaFile` works when the prompt's `schema_path` is the schema file base name.
- Explicit source options override `Config` directory fields.
- Invalid/nil source options return `ErrInvalidConfig` .
- Existing filesystem prompt/profile/schema behavior remains unchanged.
### Verification
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Run:
```bash
go test ./internal/promptdef ./internal/profile ./internal/validate .
go test ./...
2026-07-04 09:09:29 -05:00
```
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
## Stage 5: Public In-Memory Profiles And Profile Templates
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Goal: allow library callers to provide typed profile values and construct stable OpenAI-compatible profile templates without generating YAML.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Public API
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Add a public profile facade aligned with the profile YAML contract:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
```go
type Profile struct {
ID string
Endpoint string
Model string
Temperature float64
MaxTokens int
TopP float64
TimeoutSeconds int
ServiceTier string
ReasoningEffort string
APIKeyRequired bool
ExtraParams map[string]any
}
func WithProfiles(profiles ...Profile) Option
```
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Add an OpenAI-compatible template constructor:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
```go
type OpenAICompatibleProfileConfig struct {
ID string
Endpoint string
Model string
APIKeyRequired bool
Temperature float64
MaxTokens int
TopP float64
TimeoutSeconds int
ServiceTier string
ReasoningEffort string
ExtraParams map[string]any
}
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
func OpenAICompatibleProfile(cfg OpenAICompatibleProfileConfig) Profile
```
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Rules:
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
- `WithProfiles` profiles override built-ins with the same ID.
- If `WithProfiles` and a file/FS profile source are both configured, in-memory profiles have highest precedence, then file/FS profiles, then built-ins.
- Public profile values must not include raw API-key fields.
- Use `APIKeyRequired` to indicate whether `RunRequest.APIKey` is required for the public path. Internal conversion may map this to the existing auth-required/profile credential model without exposing raw keys.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Implementation Steps
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. Add a profile repository for public in-memory `Profile` values.
- Validate with the same effective rules as YAML profiles.
- Reject duplicate IDs within the provided values.
- Deep-copy `ExtraParams` across public/internal boundaries.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
2. Compose profile sources in public `NewEngine` .
- Highest: in-memory public profiles.
- Next: configured profile file/FS/directory source.
- Fallback: built-in profiles.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
3. Add template constructor conversion.
- `OpenAICompatibleProfile` should be a convenience constructor only.
- It should not register global state.
- It should not maintain a broad model catalog.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
4. Ensure direct API-key behavior works with in-memory/template profiles.
- Profiles marked `APIKeyRequired` should require `RunRequest.APIKey` for public default LLM execution.
- Profiles not marked `APIKeyRequired` should not require an API key.
2026-07-04 09:09:29 -05:00
### Tests
2026-07-04 11:38:14 -05:00
Required coverage:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
- Public `Prepare` /`Run` uses `WithProfiles` without any profile files.
- In-memory profiles override built-ins.
- In-memory profiles override file/FS profile sources when IDs collide.
- Duplicate in-memory profile IDs return `ErrInvalidConfig` .
- Template-created profiles execute through the same path as normal profiles.
- Template-created profiles requiring an API key work with `RunRequest.APIKey` .
- Template-created profiles that do not require an API key work without one.
- `ExtraParams` in public profiles are deep-copied and isolated from caller mutation.
2026-07-04 08:13:10 -05:00
### Verification
Run:
```bash
2026-07-04 11:38:14 -05:00
go test .
2026-07-04 08:13:10 -05:00
go test ./...
```
2026-07-04 11:38:14 -05:00
## Stage 6: Public Documentation And Examples
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Goal: document implemented behavior in canonical locations after code exists.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
### Documentation
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Update:
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
- `README.md` : add a short pointer to library usage and built-in profiles without turning the README into a manual.
- `docs/config.md` : document optional `profile_dir` , built-in override behavior, and implemented built-in profile IDs.
- `docs/cli.md` : document optional `--profile-dir` and built-in profile selection.
- `docs/internal/adapters.md` : document repository composition and public library adapter surface.
- `docs/consumers/api.md` : describe public consumer surfaces at a high level.
- `docs/consumers/pkg-scriptorium.md` : document root package usage, source options, direct API-key value, errors, and examples.
- `docs/integrations/openai-compatible-chat.md` : update only if direct API-key plumbing changes provider-call semantics.
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Do not document unimplemented roadmap behavior outside `docs/roadmap/` .
2026-07-04 08:13:10 -05:00
### Examples
2026-07-04 11:38:14 -05:00
Add or update copyable examples that require no real provider credentials:
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
- prepare with built-in profile and no `profile_dir` ;
- public library prepare from `embed.FS` ;
- public library run with injected fake LLM;
- public library run with typed/template profile and direct API key, using a fake/local provider path so no real secret is required.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
Examples must be secret-free.
2026-07-04 09:09:29 -05:00
2026-07-04 11:38:14 -05:00
### Tests And Smoke Commands
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Required verification:
2026-07-04 08:13:10 -05:00
```bash
go test ./...
go run ./cmd/scriptorium render \
--config ./examples/config.yml \
--prompt generic.markdown_summary \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--format json
```
2026-07-04 11:38:14 -05:00
Also run smoke commands for any new examples, such as:
```bash
go run ./examples/go-library/prepare
```
If an example relies on a specific built-in profile ID, use an ID from `docs/roadmap/profiles/` and include that smoke command in the implementing change.
## Final Completion Checklist
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
Before marking the combined feature complete:
2026-07-04 08:13:10 -05:00
2026-07-04 11:38:14 -05:00
1. `go test ./...` passes.
2. Existing CLI render smoke command passes.
3. At least one smoke command proves built-in profile lookup works without `profile_dir` .
4. Public package examples compile/run without real provider credentials.
5. `profile_dir` is optional in CLI, HTTP, and public engine construction.
6. Custom profiles override built-ins with the same ID.
7. Malformed selected custom profiles do not fall back to built-ins.
8. Built-in profiles use the same validation rules as file profiles.
9. Public `RunRequest.APIKey` is the documented library credential method.
10. Raw API keys do not appear in YAML/JSON config, prepared output, run results, logs, hashes, or examples.
11. Non-roadmap docs describe only implemented behavior.