17 KiB
Cleanup Implementation Roadmap
Purpose
This roadmap turns the findings in audit.md into a staged, decision-complete cleanup plan for Scriptorium.
Audience: LLM coding agents implementing the cleanup in order.
Controlling policies:
Global Implementation Rules
- Implement stages in order.
- Keep public CLI flags, HTTP request/response shapes, config precedence, prompt/profile ID semantics, and validation behavior stable unless a stage explicitly says otherwise.
- Keep adapters thin and use-case policy in
internal/usecase. - Prefer explicit helpers over reflection, generic workflow abstractions, or broad framework-style rewrites.
- Update non-roadmap docs only when implemented behavior changes or when a stale implemented-behavior statement is found during a stage.
- Do not document future behavior outside
docs/roadmap/. - Run the stage-specific tests before moving to the next stage.
- Run
go test ./...after the final stage.
Stage 1: Execution Target Mapping Cleanup
Goal
Reduce drift when adding or changing execution/runtime fields such as service_tier, api_key_env, reasoning_effort, or future provider request keys.
Scope
Update only explicit execution-target mapping and serialization paths. Do not add new CLI flags or new provider features.
Implementation
In internal/usecase:
- Keep execution-target merge policy in
internal/usecase. - Add focused helper coverage around
resolveExecutionTarget,mergeExecutionTarget, and profile-to-target conversion. - Keep the existing semantics:
- built-in defaults first;
- profile values override defaults;
- request overrides override profile values;
- zero numeric values do not override;
- empty/whitespace string values do not override;
- non-empty
ExtraParamsreplaces the previous map with a copy.
In internal/adapter/http:
- Add local helper functions:
executionTargetFromModelOverrideDTO(*modelOverrideRequestDTO) *domain.ExecutionTargetmodelParamsDTOFromExecutionTarget(domain.ExecutionTarget) modelParamsDTO
- Use those helpers in
handler.go. - Keep DTO types unexported and transport-owned.
- Keep HTTP response field names and omission behavior unchanged.
In internal/llm:
- Add a local helper such as
openAIChatRequestFromGenerateRequest(req domain.GenerateRequest, defaultModel string) (openAIChatRequest, error)or an equivalent small function. - Keep endpoint construction, HTTP client timeout handling, API-key environment lookup, and response parsing in
Generate. - Keep outbound serialization behavior unchanged:
- send
modelandmessages; - send
temperature,max_tokens,top_p, andservice_tieronly when currently sent; - send
response_formatonly when structured output is present; - do not serialize
reasoning_effortorextra_params.
- send
Do not:
- use reflection to copy fields;
- move HTTP DTOs into
internal/domain; - add generic mapper packages;
- change prepared-run JSON tags.
Tests
Add or update tests so an all-fields domain.ExecutionTarget catches omissions.
Required tests:
- Runner merge/profile conversion:
- profile values populate all supported execution fields;
- runtime overrides beat profile values for all overrideable fields;
- empty string overrides do not erase profile values;
- empty
ExtraParamsdoes not erase profile values.
- HTTP adapter:
- request
modelobject maps every supported field intoRunRequest.Execution; - response
metadata.model_paramsincludes every supported field according to current DTO tags.
- request
- LLM client:
- outbound JSON includes every serialized execution field;
- outbound JSON omits
service_tierwhen empty; - outbound JSON still omits
reasoning_effortandextra_params.
Validation
Run:
go test ./internal/usecase ./internal/adapter/http ./internal/llm ./internal/format
Acceptance Criteria
- No public behavior changes.
- Adding a new execution target field later has obvious mapping/test locations.
- Existing HTTP and LLM behavior remains stable.
- Stage is small enough for one implementation prompt.
Stage 2: Prompt/Profile Filesystem Catalog Helper
Goal
Centralize shared recursive YAML discovery mechanics while preserving prompt/profile-specific validation and error behavior.
Scope
Create a narrow helper package for filesystem catalog mechanics only.
Recommended package:
internal/filecatalog
Implementation
Add helper functions with explicit, small responsibilities:
- recursively find YAML files under a root directory;
- honor context cancellation during walking;
- accept
.yamland.yml; - return stable sorted full paths;
- compute clean relative paths from a root;
- return filename stems with
.yaml/.ymlstripped.
Use the helper in:
internal/promptdef/filesystem_repository.gointernal/profile/filesystem_repository.go
Preserve existing behavior:
- prompt/profile lookup uses YAML
id, not file path; - subdirectories are organizational only;
- duplicate prompt/profile IDs are invalid;
- malformed likely-target files still surface errors;
- relative nested paths still appear in errors;
- prompt
content_fileresolution remains relative to the prompt YAML file; - prompt/profile strict YAML and validation stay in their existing packages.
Do not:
- create a generic repository framework;
- merge prompt and profile normalization;
- move prompt/profile domain policy into the helper;
- change error messages except for unavoidable wording caused by helper extraction.
Tests
Add tests for internal/filecatalog:
- nested YAML discovery;
.yamland.ymlaccepted;- non-YAML files ignored;
- returned paths sorted deterministically;
- relative path formatting works for nested files;
- filename stem stripping handles both extensions.
Keep existing prompt/profile repository tests passing.
Validation
Run:
go test ./internal/filecatalog ./internal/promptdef ./internal/profile
Acceptance Criteria
- Prompt/profile repository tests pass without behavior expectation changes.
- Shared filesystem scanning logic exists in one place.
- Prompt/profile packages still own their own validation and normalization.
- Stage is small enough for one implementation prompt.
Stage 3: CLI Wiring And Settings Finalization Cleanup
Goal
Reduce duplicated command setup while preserving each command's public flag surface and behavior.
Scope
Clean up internal/adapter/cli only, except for tests.
Implementation
Add CLI-local helpers. Recommended helpers:
commonCommandSettingsor similar struct containing resolvedpromptDir,profileDir,schemaDir,serverAddr, anddefaultRenderFormatwhere applicable.resolveCommonSettings(fs *flag.FlagSet, configPath string, overrides appconfig.CLIOverrides) (commonCommandSettings, error).validateRequiredLibraryDirs(promptDir, profileDir string) error.newRunner(promptDir, profileDir, schemaDir string, llmClient llm.Client) *usecase.Runner.- Optionally
newOpenAIClient() (*llm.OpenAICompatibleClient, error)if it removes exact duplication without obscuring command behavior.
Preserve command differences:
runexposes runtime model override flags and--schema-dir;renderexposes runtime model override flags and--format, but not--schema-dir;serveexposes--addrand--schema-dir, but no runtime model override flags;renderdefault format comes fromdefaults.render_formatunless--formatis set;- deprecated
--prompt-idand--profile-idaliases remain accepted.
Keep existing parse functions:
parseRunArgsparseRenderArgsparseServeArgs
Do not:
- replace the standard library
flagpackage; - introduce a command framework;
- make
serveaccept runtime model override flags; - change error prefixes such as
run parse error,render parse error, orserve parse error; - change CLI output behavior.
Tests
Required regression tests:
run,render, andservestill apply config precedence correctly.- Missing effective
prompt_dirandprofile_dirstill return the same guidance. renderstill uses config default render format and explicit--formatoverride.servestill rejects runtime model override flags.runandrenderstill build equivalent runtime override requests for shared flags.
Validation
Run:
go test ./internal/adapter/cli
Acceptance Criteria
- No CLI flag, output, exit-code, or precedence changes.
- Runner dependency construction is centralized inside the CLI adapter.
- Command-specific behavior remains easy to read.
- Stage is small enough for one implementation prompt.
Stage 4: Stable Use-Case Error Reasons For HTTP Mapping
Goal
Remove HTTP error mapping's dependency on runner error message substrings.
Scope
Change error identity, not public HTTP error responses.
Implementation
In internal/usecase:
- Add stable sentinel errors for invalid-request reasons that HTTP currently distinguishes by message text.
- Required sentinels:
- missing profile selection, for the case where neither request profile nor prompt
default_profileis available; - missing API-key environment value, for the case where
api_key_envis set but the named environment variable is unset or empty.
- missing profile selection, for the case where neither request profile nor prompt
- Wrap these sentinels with
ErrInvalidRequestso existing broad invalid-request checks keep working. - Preserve clear human-readable runner errors.
In internal/adapter/http:
- Replace
strings.Contains(err.Error(), ...)checks for these cases witherrors.Is. - Keep current HTTP status codes, error codes, and response messages:
400 profile_required;400 api_key_env_missing.
Do not:
- expose HTTP-specific error codes from
internal/usecase; - change the HTTP JSON error body shape;
- remove broad fallback handling for
usecase.ErrInvalidRequest.
Tests
Required tests:
- Runner tests assert
errors.Is(err, usecase.ErrProfileRequired)or the chosen sentinel name for missing profile selection. - Runner tests assert
errors.Is(err, usecase.ErrAPIKeyEnvMissing)or the chosen sentinel name for missing API-key environment value. - HTTP handler tests still assert unchanged status/code/message for both cases.
- HTTP handler tests should not construct errors by relying on exact runner prose for these two cases.
Validation
Run:
go test ./internal/usecase ./internal/adapter/http
Acceptance Criteria
- HTTP mapping no longer depends on runner message substrings for the two distinguished invalid-request cases.
- Public HTTP behavior is unchanged.
- Runner errors remain clear in CLI output.
- Stage is small enough for one implementation prompt.
Stage 5: Schema Failure Regression Coverage
Goal
Protect the current structured-output invariant before future schema cleanup: Prepare must load a json_schema document before any LLM call.
Scope
Add regression coverage only. Do not add schema caching or change schema loading architecture in this stage.
Implementation
In internal/usecase/runner_test.go or an appropriate package test:
- Add a test where a prompt uses
validation_mode: json_schemawith a missing or failing schema document. - Assert
Runner.Preparefails withErrValidation. - Assert no LLM call is made for
Runner.Runwhen structured-output schema loading fails.
If existing tests already cover part of this behavior, consolidate assertions without making the test suite harder to read.
Do not:
- cache compiled schemas;
- change
validate.StandardValidatorbehavior; - introduce a schema service abstraction.
Validation
Run:
go test ./internal/usecase ./internal/validate
Acceptance Criteria
- Missing structured-output schema fails before generation.
- Existing JSON Schema validation behavior is unchanged.
- Stage is small enough for one implementation prompt.
Stage 6: Test Fixture Cleanup
Goal
Reduce repeated test setup after behavior-preserving production refactors are complete.
Scope
Prefer package-local test helpers. Avoid cross-package test utility packages unless a helper is needed by more than two packages and represents a stable public fixture contract.
Implementation
In internal/adapter/cli/run_test.go:
- Consolidate repeated temp prompt/profile/input setup into local helper functions.
- Keep helper names behavior-focused, for example:
newCLITestLibrarywritePromptFileWithDefaultProfilewriteProfileFilerunCLICommand
- Do not hide assertions inside helpers unless the assertion is truly setup validation.
In internal/usecase/runner_test.go:
- Keep existing fake interfaces package-local.
- Remove only high-volume duplication that obscures test intent.
Do not:
- move package-private fake types into production code;
- create a broad
internal/testutilpackage unless a later cleanup stage proves it necessary; - rewrite tests into table-driven form when cases have meaningfully different setup.
Validation
Run:
go test ./internal/adapter/cli ./internal/usecase
Acceptance Criteria
- Test intent is at least as clear as before.
- No production behavior changes.
- Test fixture setup has less repeated boilerplate in CLI tests.
- Stage is small enough for one implementation prompt.
Stage 7: Unsupported Placeholder Sweep
Goal
Remove code that suggests unimplemented artifact behavior outside roadmap documentation.
Scope
Remove unsupported placeholders only when they are not needed by current tests or public docs.
Implementation
Remove domain.ArtifactRefS3 from internal/domain/domain.go unless new evidence shows it is intentionally needed by implemented code.
Preserve current behavior:
- supported artifact reference types remain
inlineandfile; - unsupported artifact reference types still return
artifact.ErrUnsupportedRefType; - docs continue to describe only
inlineandfileoutside roadmap files.
Update tests only if they reference the removed constant. Prefer testing unsupported artifact behavior with a literal custom type such as domain.ArtifactRefType("s3") or domain.ArtifactRefType("unsupported").
Do not:
- add S3 support;
- document S3 as implemented;
- add future-backend placeholders elsewhere.
Validation
Run:
go test ./internal/domain ./internal/artifact
Acceptance Criteria
- Unsupported placeholder constant is removed.
- Unsupported artifact-type behavior remains covered.
- No non-roadmap doc claims unimplemented artifact support.
- Stage is small enough for one implementation prompt.
Stage 8: Final Verification And Documentation Alignment
Goal
Confirm the cleanup sequence preserved behavior and documentation accuracy.
Implementation
Run the full test suite:
go test ./...
Run the maintained render smoke command:
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
Search for stale or unsupported terms:
rg -n "ArtifactRefS3|s3|strings\\.Contains\\(err\\.Error\\(\\)|TODO|future|planned" internal docs README.md examples
Review results manually:
s3should not appear as an implemented artifact type.strings.Contains(err.Error())should not be used for stable use-case reason mapping.- Any
futureorplannedwording outsidedocs/roadmap/must describe current boundaries, not aspirational behavior.
Update docs only if cleanup changed implemented behavior or if the search reveals stale implemented-behavior docs.
Acceptance Criteria
- Full test suite passes.
- Maintained render smoke command succeeds.
- No stale unsupported artifact placeholder remains.
- Non-roadmap docs describe implemented behavior only.
- Working tree contains only intentional cleanup changes.
Deferred Work
Do not implement these during the staged cleanup unless a later audit makes them high-confidence:
- schema caching or a combined raw/compiled schema service;
- artifact hash/build helper beyond a small helper introduced opportunistically during touched code;
- generic YAML repository framework;
- generic CLI command framework;
- plugin architecture for future prompt/profile/schema/artifact backends;
- durable state, manifests, checkpoints, or resume behavior.
Completion Criteria
The cleanup roadmap is complete when all stages have been implemented in order, the final verification passes, and the resulting code still satisfies:
Runner.RunreusesRunner.Prepare;- CLI and HTTP adapters instantiate
Runnerwithout a repairer; - unknown config/prompt/profile YAML and HTTP JSON fields are rejected;
- raw API key values are not accepted or emitted;
- prompt/profile subdirectories remain organizational only;
- schema paths remain explicit and relative to
schema_dirwhen not absolute; - public CLI and HTTP behavior remains stable.