13 KiB
Migration Step 3 Implementation Plan
Status
Completed on 2026-07-27. The framework characterization baseline is complete, and Migration Step 4 is the next planned work.
Objective
Implement the target state in the Step 3 framework-characterization roadmap: make Promptkit-destined tests independent of Scriptorium-owned executable examples, close the identified public contract gaps, and leave every migration-critical behavior with a clear test owner.
Follow the accepted ownership boundary in ADR 0002 and the test-value and non-duplication rules in the testing policy.
Constraints
- Execute the stages in order and satisfy each gate before proceeding.
- Do not change production behavior, public types, method signatures, package boundaries, or application interfaces.
- Limit implementation changes to testdata, tests, and roadmap status.
- Keep all default-suite tests deterministic, offline, and independent of real credentials.
- Preserve unrelated working-tree changes.
- Do not copy the complete
examples/tree into testdata. - Do not add tests merely to raise statement coverage.
- Prefer extending or consolidating an existing test over adding a parallel test for the same behavior.
Stage 1: Create The Framework Contract Corpus
Create this Promptkit-destined fixture tree:
testdata/framework/
├── fixtures/
│ ├── glossary.yml
│ └── transcript.md
├── profiles/
│ ├── contract-fast.yaml
│ └── contract-quality.yaml
├── prompts/
│ ├── contract.markdown_summary.system.md
│ ├── contract.markdown_summary.user.md
│ ├── contract.markdown_summary.yaml
│ ├── contract.structured_events.system.md
│ ├── contract.structured_events.user.md
│ └── contract.structured_events.yaml
└── schemas/
└── structured_events.schema.json
Define the corpus as follows:
contract.markdown_summary- version
1.0.0; - default profile
contract-fast; - required
transcriptinput and optionalglossaryinput; - system and user messages loaded through relative
content_filepaths; and - Markdown output with basic validation and no repair attempts.
- version
contract.structured_events- version
1.0.0; - default profile
contract-quality; - the same two inputs;
- system and user messages loaded through relative
content_filepaths; and - JSON output with JSON Schema validation through
structured_events.schema.jsonand no repair attempts.
- version
contract-fast- endpoint
http://localhost:8000/v1; - model
contract-fast-model; - temperature
0.2, max tokens500, top-p1, and timeout90; and - no credential requirement.
- endpoint
contract-quality- endpoint
http://localhost:8000/v1; - model
contract-quality-model; - temperature
0.1, max tokens1000, top-p0.9, and timeout120; and - no credential requirement.
- endpoint
- The schema requires an object containing an
eventsarray. Keep it small but valid for the same JSON Schema draft currently used by the maintained structured-output example. - The transcript and glossary contain short synthetic values suitable for rendering and hash assertions. They must contain no private or real-world data.
Do not add application configuration, HTTP requests, executable scripts, or provider credentials to this corpus.
In engine_test.go, add shared constants for the contract root, prompt IDs,
profile IDs, and fixture paths. Replace
TestPrepareWorksWithExampleDirectoriesAndFileInputs with
TestPrepareWorksWithFrameworkContractCorpus, using table cases for the
ordinary and structured prompts. Construct the public engine from the new
directories, use the new file artifacts, and assert that each prompt renders
with its intended profile. For the structured case, also assert that
PreparedRun.StructuredOutput contains the loaded JSON Schema specification.
This test is the real-parser acceptance check for the corpus.
Run:
go test .
go test ./internal/promptdef ./internal/profile ./internal/validate
Stage 1 Gate
- Every corpus file loads through its real owning parser.
- Relative prompt content resolves from the prompt file location.
- The structured schema decodes through the real validator's schema-document loader and appears in the prepared structured-output specification.
- No production or executable-example file changed.
Stage 2: Move Framework Tests Onto Framework-Owned Fixtures
Update engine_test.go to use the new corpus.
- Reuse the contract constants introduced in Stage 1.
- Rename:
newExampleEnginetonewContractEngine;newExampleEngineWithOptionstonewContractEngineWithOptions; andexampleConfigtocontractConfig.
- Make
contractConfigpoint at the corpus prompt, profile, and schema directories. - Replace each
./examples/...dependency inengine_test.gowith the matching contract fixture or a purpose-builtt.TempDir,fstest.MapFS, or in-memory profile. - Update assertions that intentionally identify fixture prompt IDs, profile IDs, models, rendered text, or hashes to the contract values. Do not change assertions that express independent public behavior.
- Rename tests whose names say “example” when they now exercise contract testdata.
Add TestEngineRunWithDirectorySourcesAndFileInputs to engine_test.go. It
must assemble the public engine from the contract prompt, profile, schema, and
file-artifact directories; inject a deterministic LLMClient; run
contract.structured_events; and assert:
- the prompt-selected
contract-qualityprofile; - a non-empty run ID, prompt hash, rendered-prompt hash, and both input hashes;
- provider-level JSON Schema structured output on the captured generation request;
- passed JSON Schema validation;
application/jsonartifact content;- preserved raw output and injected token usage; and
- non-zero ordered timestamps with non-negative duration.
Move the unique protection from
internal/usecase/integration_test.go into this public test, then delete that
internal integration test. Do not retain both assembled workflows.
The only test references to examples/ after this stage should be
Scriptorium-owned adapter or maintained-example checks. In particular, this
command must return no matches:
rg -n 'examples/' engine_test.go internal/usecase
Run:
go test .
go test ./internal/usecase
Stage 2 Gate
- Public and framework-internal tests pass without reading Scriptorium-owned executable examples.
- The new public assembled workflow subsumes the deleted internal integration test.
- Scriptorium's maintained examples are unchanged.
- No production file changed.
Stage 3: Consolidate And Complete Public Characterization
Execution-Setting Precedence
Add a table-driven TestEngineExecutionSettingPrecedence in engine_test.go.
Run through the public engine with an injected recording LLMClient. Cover
these cases:
- a profile with zero-valued optional settings receives the documented framework numeric defaults;
- non-zero profile settings replace those defaults;
- request settings replace profile settings; and
- explicit request numeric zero replaces non-zero profile settings.
Across the table, verify the effective endpoint, model, temperature,
max-tokens, top-p, timeout, service tier, reasoning effort, API-key environment
name, and extra_params where the relevant layer supplies them. Verify
ExecutionTargetPresence is false for omitted numeric request settings and
true for every explicitly supplied numeric setting, including zero.
Use relationally distinct values for each layer. Assert literal framework
defaults only in the framework-default case because those values are part of
the documented public contract. Use t.Setenv for every non-empty profile or
request API-key environment name, assert only the environment-variable names,
and never expose the test secret values.
Consolidate overlapping assertions:
- remove
TestPreparePreservesExplicitZeroExecutionOverridesonce the new table protects that behavior; and - retain
TestRunPassesPreparedRequestToInjectedLLMClientfor rendered prompt, direct-key, and structured-output handoff, but remove execution-precedence assertions now owned by the table.
Caller Cancellation
Add TestEngineRunPropagatesCallerCancellation using the built-in
OpenAI-compatible client and a custom RoundTripper.
- The transport must signal through a channel when
RoundTripbegins. - It must block on
req.Context().Done()and return the context error. - Start
Engine.Runin a goroutine, wait for the transport signal, cancel the caller context, and collect the result through a buffered channel. - Assert that the call returns and the error matches
ErrLLMGenerate. - Do not use sleeps or elapsed-time assertions.
Injected Nil Response
Add a valid-request case to TestPublicErrorsSupportErrorsIs whose injected
LLMClient returns (nil, nil). Assert ErrLLMGenerate. Extend the existing
fake only as needed to express this case; do not create a mock framework.
Reserved Provider Parameters
Add TestRunRejectsReservedExtraParamsBeforeProviderCall.
- Use the built-in client with a custom immediate
RoundTripperthat records whether it was invoked. - Supply a valid contract prompt and profile plus request
ExtraParams: map[string]any{"model": "collision"}. - Assert
ErrInvalidRequest. - Assert that the transport was not invoked.
Run:
go test .
go test -count=20 .
Stage 3 Gate
- The four-layer precedence table and presence assertions pass.
- Cancellation is deterministic and contains no wall-clock sleeps.
- Nil injected responses and reserved parameters preserve their public error categories.
- Superseded assertions or tests have been removed rather than duplicated.
- No production file changed.
Stage 4: Audit Ownership And Validate The Baseline
Ownership Audit
Review the behavior list in step3.md against the final suite. Confirm:
- root facade and public contract tests are Promptkit-destined;
internal/domain,internal/usecase,internal/promptdef,internal/prompt,internal/profile,internal/profile/builtin,internal/filecatalog, generalinternal/artifact,internal/validate, andinternal/llmtests move with Promptkit-owned behavior;- CLI, application configuration, prepared formatting, HTTP DTO, strict JSON, HTTP limit, and rooted artifact-containment tests remain Scriptorium-owned;
- HTTP and CLI tests that currently construct internal runners or inspect internal sentinels retain their observable assertions and are explicitly deferred for boundary rewrites in Migration Step 4; and
- no consequential behavior in the feature roadmap lacks a test owner.
Do not create a permanent test-inventory document. Record any unexpected
ownership exception in step3.md; otherwise the ownership table there is the
complete disposition.
Full Validation
Run:
go test ./...
go vet ./...
build_dir="$(mktemp -d)"
go build -o "$build_dir/scriptorium" ./cmd/scriptorium
go test -count=20 .
go test ./internal/adapter/http -run TestMaintainedHTTPRunExampleMatchesRequestContract
bash ./examples/render-markdown-summary.sh
go run ./cmd/scriptorium render \
--config ./examples/config.full.yml \
--prompt generic.markdown_summary \
--input transcript=./examples/fixtures/transcript.md \
--input glossary=./examples/fixtures/glossary.yml \
--format text
go run ./examples/go-library/prepare
git diff --check
Validate every local Markdown link and path in the changed roadmap files. Confirm both configuration examples were accepted through the real configuration loader by the two render commands.
Inspect the final diff and confirm the implementation changed only:
testdata/framework/**;engine_test.go;internal/usecase/integration_test.goby deletion;docs/roadmap/step3.md;docs/roadmap/implementation.md; and- the Step 3 status in
docs/roadmap/migration.md.
If a necessary change falls outside that list, stop and revise the plan or request direction rather than expanding scope implicitly.
Completion Bookkeeping
After every check passes:
- update
step3.mdto state that the target state is complete and summarize the characterized baseline without reintroducing an implementation log; - add a Step 3 gate-status entry to
migration.mdwith the completion date and a short validation summary; and - mark this implementation plan complete.
Do not begin Migration Step 4 in the same change.
Stage 4 Gate
- Every completion criterion in
step3.mdis satisfied. - The full suite and maintained examples pass offline.
- The diff contains no production behavior or API change.
- The main migration roadmap identifies Step 3 as complete and Step 4 as next.
Open Questions
None.