Complete the public facade adapter boundary

This commit is contained in:
2026-07-28 00:57:53 +00:00
parent 280916bf4a
commit 3c33b52b15
10 changed files with 159 additions and 83 deletions

View File

@@ -113,11 +113,16 @@ counts. `RenderedPrompt`, `RenderedMessage`, `CacheControl`, and
`ArtifactReader` implements `ArtifactReader` implements
`Read(context.Context, ArtifactRef) (*Artifact, error)`. Supplying it through `Read(context.Context, ArtifactRef) (*Artifact, error)`. Supplying it through
`WithArtifactReader` replaces the engine's default inline and file reader for `WithArtifactReader` replaces, rather than extends, the engine's default inline
all inputs. Reader errors remain available through `errors.Is` alongside and file reader for every input. Omitting the option retains that default;
`ErrArtifactLoad`; a nil artifact with no error is treated as an artifact-load `WithArtifactReader(nil)` makes engine construction fail with
failure. Readers must supply artifact metadata and should not retain or mutate `ErrInvalidConfig`.
the caller's values.
Reader failures are surfaced as errors matching `ErrArtifactLoad` while
preserving the reader's original error identity for `errors.Is`. A `(nil, nil)`
reader response is also an artifact-load failure. Readers are responsible for
artifact metadata, although the engine assigns the input-map name when the
returned name is empty; readers should not retain or mutate caller values.
## Requests, Inputs, And Overrides ## Requests, Inputs, And Overrides
@@ -191,5 +196,8 @@ Public methods preserve these sentinel checks through `errors.Is`:
- `ErrLLMGenerate` - `ErrLLMGenerate`
- `ErrValidation` - `ErrValidation`
For interface selection and operational responsibilities, see the `ErrProfileRequired` and `ErrAPIKeyEnvMissing` each also match
[consumer integration overview](api.md). `ErrInvalidRequest`, so callers can select either the broad request category or
the specific condition.
For the HTTP interface, see the [HTTP API reference](../api.md).

View File

@@ -2,9 +2,9 @@
## Purpose ## Purpose
Adapters translate external inputs into domain requests, compose dependencies, Adapters translate external inputs into public engine requests and translate
and translate domain results or errors back to their interface. They own IO and public results or errors back to their interface. They own IO and presentation
presentation mechanics; use-case decisions remain in `internal/usecase`. mechanics; use-case decisions remain behind the root `scriptorium` facade.
External contracts are canonical in the [CLI reference](../cli.md), [HTTP API External contracts are canonical in the [CLI reference](../cli.md), [HTTP API
reference](../api.md), and [Go package contract](../consumers/pkg-scriptorium.md). reference](../api.md), and [Go package contract](../consumers/pkg-scriptorium.md).
@@ -14,35 +14,37 @@ reference](../api.md), and [Go package contract](../consumers/pkg-scriptorium.md
- `cmd/scriptorium` passes process arguments and streams to - `cmd/scriptorium` passes process arguments and streams to
`internal/adapter/cli`. `internal/adapter/cli`.
- `internal/adapter/cli` parses commands, resolves application settings through - `internal/adapter/cli` parses commands, resolves application settings through
`internal/config`, constructs a runner, and owns process output handling. `internal/config`, constructs the public engine, and owns process output
- `internal/adapter/http` decodes DTOs, maps them to `domain.RunRequest`, calls handling.
a runner interface, and maps errors and results to HTTP DTOs. - `internal/adapter/http` decodes DTOs, maps them to public run requests,
calls its local public `Runner` interface, and maps public errors and results
to HTTP DTOs.
- The root `scriptorium` package maps its public types and options to internal - The root `scriptorium` package maps its public types and options to internal
collaborators and maps selected internal errors to public sentinels. collaborators and maps selected internal errors to public sentinels.
- `internal/format` formats prepared runs for the CLI; `internal/llm`, - `internal/format` formats public prepared runs for the CLI.
`internal/prompt`, and source packages supply runner dependencies.
## Wiring Flows ## Wiring Flows
### CLI ### CLI
The CLI resolves configuration before constructing dependencies. `run` builds a The CLI resolves configuration before constructing the public engine. `run`
runner with the ordinary composite artifact reader and invokes `Runner.Run`; calls `Engine.Run` with a public request and `render` calls `Engine.Prepare`
`render` uses the same wiring and invokes `Runner.Prepare`; `serve` replaces the with the same request mapping. `serve` constructs the HTTP-owned restricted
file reader with the restricted artifact reader, builds an HTTP handler, and artifact reader, injects it with `WithArtifactReader`, passes the resulting
starts the server. engine directly to the HTTP handler, and starts the server.
Parser state records whether numeric runtime values were explicitly supplied. Parser state records whether numeric runtime values were explicitly supplied.
That presence is carried into `domain.ExecutionTargetOverride`, allowing the That presence is carried into `scriptorium.ExecutionTargetOverride`, allowing
runner to distinguish omitted values from explicit zero overrides. the engine to distinguish omitted values from explicit zero overrides.
### HTTP ### HTTP
The handler first enforces transport limits, strict JSON decoding, and the The handler first enforces transport limits, strict JSON decoding, and the
minimal request shape. It maps DTO values to domain types without deciding minimal request shape. It maps DTO values to public types without deciding
prompt selection, source behavior, or validation semantics. On success it maps prompt selection, source behavior, or validation semantics. On success it maps
the domain result to the response DTO; on failure it uses `errors.Is` over the public result to the response DTO; on failure it uses `errors.Is` over
runner, source, artifact, and profile errors to choose the public error mapping. public framework errors and HTTP-local artifact-policy errors to choose the
public error mapping.
The [HTTP API reference](../api.md) owns the route, DTO schema, status codes, The [HTTP API reference](../api.md) owns the route, DTO schema, status codes,
and externally observable limit behavior. and externally observable limit behavior.
@@ -57,10 +59,10 @@ set and keeps direct request API keys out of public results.
## Package-Local Guarantees ## Package-Local Guarantees
- Adapters do not embed runner orchestration or source-loading decisions. - Adapters do not embed framework orchestration or source-loading decisions.
- Configuration is resolved before adapter dependency composition. - Configuration is resolved before adapter dependency composition.
- CLI and HTTP create runners without a repairer; a repairer is available only - CLI and HTTP consume the public engine without a repairer; a repairer remains
through explicit internal runner construction. available only through explicit internal runner construction.
- DTO conversion preserves explicit numeric-override presence. - DTO conversion preserves explicit numeric-override presence.
- Error mapping matches error identities, not error text. - Error mapping matches error identities, not error text.
- No adapter creates durable run state; caller-selected output files are not - No adapter creates durable run state; caller-selected output files are not
@@ -105,9 +107,10 @@ sufficiency guidance.
### Adapter Capabilities ### Adapter Capabilities
1. Define or reuse the appropriate domain or use-case interface boundary. 1. Define or reuse an adapter-local consumer interface with public facade
2. Implement translation and IO behavior without moving use-case decisions out types when a test seam is needed.
of `internal/usecase`. 2. Implement translation and IO behavior without moving framework decisions out
of the public engine.
3. Add focused mapping, parsing, and error-behavior tests. 3. Add focused mapping, parsing, and error-behavior tests.
4. Update this document and the affected public or integration contract. Update 4. Update this document and the affected public or integration contract. Update
[source internals](sources.md) when source-loading behavior changes. [source internals](sources.md) when source-loading behavior changes.

View File

@@ -17,8 +17,8 @@ and invariants; public behavior belongs in the linked contracts.
| Component | Implemented responsibility | References | | Component | Implemented responsibility | References |
| --- | --- | --- | | --- | --- | --- |
| `internal/adapter/cli` | Parses CLI commands, applies application wiring, and handles process input and output. | [CLI contract](../cli.md), [adapter internals](adapters.md) | | `internal/adapter/cli` | Parses CLI commands, constructs the public engine from application settings, and handles process input and output. | [CLI contract](../cli.md), [adapter internals](adapters.md) |
| `internal/adapter/http` | Maps HTTP requests and responses to domain operations and maps public errors. | [HTTP API contract](../api.md), [adapter internals](adapters.md) | | `internal/adapter/http` | Maps HTTP requests and responses through public engine values, maps public errors, and owns restricted HTTP artifact policy. | [HTTP API contract](../api.md), [adapter internals](adapters.md) |
| `internal/domain` | Defines core request, result, output-contract, and LLM-boundary types. | [runner internals](runner.md) | | `internal/domain` | Defines core request, result, output-contract, and LLM-boundary types. | [runner internals](runner.md) |
| `internal/usecase` | Implements `Runner` preparation, execution, validation coordination, and the repairer boundary. | [runner internals](runner.md) | | `internal/usecase` | Implements `Runner` preparation, execution, validation coordination, and the repairer boundary. | [runner internals](runner.md) |
@@ -32,14 +32,14 @@ and invariants; public behavior belongs in the linked contracts.
| `internal/profile` | Loads filesystem and `fs.FS` execution profiles and combines profile repositories. | [configuration contract](../config.md), [source internals](sources.md) | | `internal/profile` | Loads filesystem and `fs.FS` execution profiles and combines profile repositories. | [configuration contract](../config.md), [source internals](sources.md) |
| `internal/profile/builtin` | Provides embedded built-in execution profiles as a repository. | [configuration contract](../config.md), [source internals](sources.md) | | `internal/profile/builtin` | Provides embedded built-in execution profiles as a repository. | [configuration contract](../config.md), [source internals](sources.md) |
| `internal/filecatalog` | Provides shared YAML discovery and source-root helpers. | [source internals](sources.md) | | `internal/filecatalog` | Provides shared YAML discovery and source-root helpers. | [source internals](sources.md) |
| `internal/artifact` | Reads inline and file-backed input artifacts. | [configuration contract](../config.md), [HTTP API contract](../api.md), [source internals](sources.md) | | `internal/artifact` | Provides the framework's ordinary inline and unrestricted file artifact reader. | [configuration contract](../config.md), [source internals](sources.md) |
| `internal/prompt` | Renders prompt templates into messages. | [runner internals](runner.md) | | `internal/prompt` | Renders prompt templates into messages. | [runner internals](runner.md) |
## Formatting, Validation, And Model Access ## Formatting, Validation, And Model Access
| Component | Implemented responsibility | References | | Component | Implemented responsibility | References |
| --- | --- | --- | | --- | --- | --- |
| `internal/format` | Formats prepared-run information for CLI output. | [CLI contract](../cli.md), [adapter internals](adapters.md) | | `internal/format` | Formats public prepared-run information for CLI output. | [CLI contract](../cli.md), [adapter internals](adapters.md) |
| `internal/validate` | Defines validation interfaces and provides standard filesystem and `fs.FS` schema validation. | [configuration contract](../config.md), [source internals](sources.md), [runner internals](runner.md) | | `internal/validate` | Defines validation interfaces and provides standard filesystem and `fs.FS` schema validation. | [configuration contract](../config.md), [source internals](sources.md), [runner internals](runner.md) |
| `internal/llm` | Defines the provider-neutral LLM client boundary and its OpenAI-compatible implementation. | [OpenAI-compatible integration](../integrations/openai-compatible-chat.md), [LLM internals](llm.md), [runner internals](runner.md) | | `internal/llm` | Defines the provider-neutral LLM client boundary and its OpenAI-compatible implementation. | [OpenAI-compatible integration](../integrations/openai-compatible-chat.md), [LLM internals](llm.md), [runner internals](runner.md) |

View File

@@ -25,8 +25,9 @@ contracts.
- an optional `OutputRepairer`. - an optional `OutputRepairer`.
`NewRunner` constructs a runner without a repairer. `NewRunnerWithRepairer` `NewRunner` constructs a runner without a repairer. `NewRunnerWithRepairer`
accepts one explicitly. Adapters and the public engine choose concrete accepts one explicitly. The public engine chooses concrete repositories and
repositories and readers; the runner does not load application configuration. readers; executable adapters reach the runner only through that engine. The
runner does not load application configuration.
## Prepare Flow ## Prepare Flow
@@ -68,8 +69,9 @@ validation mode is JSON or JSON Schema. Each repair receives the previous
output, validation errors, effective target, structured-output specification, output, validation errors, effective target, structured-output specification,
and attempt metadata; every repaired result is validated again. and attempt metadata; every repaired result is validated again.
`NewDefaultOutputRepairer` delegates to the injected LLM client. CLI, HTTP, and `NewDefaultOutputRepairer` delegates to the injected LLM client. The public
the public engine use `NewRunner` and therefore do not inject this repairer. engine, and therefore CLI and HTTP, uses `NewRunner` and does not inject this
repairer.
## Error Translation ## Error Translation
@@ -104,7 +106,6 @@ values in prepared or run results.
Inspect: Inspect:
- `internal/usecase/runner_test.go` - `internal/usecase/runner_test.go`
- `internal/usecase/integration_test.go`
- `engine_test.go` - `engine_test.go`
When changing orchestration: When changing orchestration:

View File

@@ -50,14 +50,11 @@ failures are operational errors.
## Artifacts ## Artifacts
`internal/artifact` composes inline and file readers. The ordinary composite `internal/artifact` owns the framework's ordinary inline and unrestricted file
reader used by CLI and the public engine reads file references from the process reader. The public engine uses it by default and permits consumers to replace it
filesystem. `internal/adapter/http` provides the restricted public artifact for every input through the public `ArtifactReader` extension. The
reader for HTTP containment: it combines inline reading with a rooted file HTTP adapter owns its restricted reader for HTTP containment: `serve` injects
reader and optional byte limit. The existing internal restricted composite that reader into the public engine with `WithArtifactReader`.
reader remains a temporary bridge for the current handler and serve wiring; it
does not define the HTTP reader's long-term boundary or carry a compatibility
promise.
The rooted reader cleans paths and applies lexical containment without resolving The rooted reader cleans paths and applies lexical containment without resolving
symlinks. It checks relative references against the configured root and accepts symlinks. It checks relative references against the configured root and accepts
@@ -70,8 +67,9 @@ by the [HTTP API reference](../api.md); deployment permissions belong in
Source packages report repository, decoding, duplicate, validation, and read Source packages report repository, decoding, duplicate, validation, and read
failures to their callers. They do not select public status codes or response failures to their callers. They do not select public status codes or response
schemas. The runner wraps source failures with use-case categories; adapters map schemas. The runner categorizes source failures and the public engine preserves
them to their own external contract. the corresponding public error identities; adapters map those identities to
their own external contract.
Source reads use current filesystem or `fs.FS` content for each request. These Source reads use current filesystem or `fs.FS` content for each request. These
packages create no manifests, checkpoints, or durable run state. packages create no manifests, checkpoints, or durable run state.
@@ -86,7 +84,6 @@ Inspect:
- `internal/artifact/reader_test.go` - `internal/artifact/reader_test.go`
- `internal/adapter/http/artifact_reader_test.go` - `internal/adapter/http/artifact_reader_test.go`
- `internal/validate/standard_validator_test.go` - `internal/validate/standard_validator_test.go`
- `internal/usecase/integration_test.go`
- `engine_test.go` - `engine_test.go`
When updating prompt, profile, schema, or built-in assets: When updating prompt, profile, schema, or built-in assets:

View File

@@ -8,15 +8,18 @@ It is for developers and LLM coding agents. User-facing behavior belongs in `REA
Scriptorium is a narrow prompt-execution application with three executable Scriptorium is a narrow prompt-execution application with three executable
entry paths: CLI `run`, CLI `render`, and the HTTP service started by `serve`. entry paths: CLI `run`, CLI `render`, and the HTTP service started by `serve`.
It also provides a public Go package for in-process use. Its current component It also provides a public Go package for in-process use. Executable adapters
inventory is maintained in the [internal overview](../internal/overview.md). consume framework behavior through that public facade; the facade continues to
compose the framework implementation inside this single repository. Its current
component inventory is maintained in the [internal overview](../internal/overview.md).
Domain behavior is centralized in `internal/usecase` and `internal/domain`. Domain behavior is centralized in `internal/usecase` and `internal/domain`.
## Core Principles ## Core Principles
- Keep orchestration narrow: Scriptorium executes one prompt request; it is not a multi-step workflow engine. - Keep orchestration narrow: Scriptorium executes one prompt request; it is not a multi-step workflow engine.
- Keep adapter logic thin: adapters map external shapes to domain requests/results and should not hold domain decisions. - Keep adapter logic thin: adapters map external shapes to public engine
requests/results and should not hold framework decisions.
- Keep boundaries explicit: repositories/loaders/renderers/validators/LLM client stay behind package interfaces. - Keep boundaries explicit: repositories/loaders/renderers/validators/LLM client stay behind package interfaces.
- Keep external decoding strict: configuration, prompt, and profile YAML and - Keep external decoding strict: configuration, prompt, and profile YAML and
HTTP JSON should reject unknown fields. HTTP JSON should reject unknown fields.
@@ -26,6 +29,9 @@ Domain behavior is centralized in `internal/usecase` and `internal/domain`.
- Adapters translate external shapes and IO concerns; they do not make - Adapters translate external shapes and IO concerns; they do not make
use-case decisions. use-case decisions.
- Executable adapters and prepared-run formatting use the public facade for
framework behavior rather than importing framework implementation packages
directly.
- Use-case and domain code depend on explicit repository, renderer, validator, - Use-case and domain code depend on explicit repository, renderer, validator,
and LLM interfaces rather than adapter implementations. and LLM interfaces rather than adapter implementations.
- Source, rendering, validation, and LLM implementations remain behind their - Source, rendering, validation, and LLM implementations remain behind their
@@ -52,7 +58,8 @@ their implementation.
## Error Handling And Logging ## Error Handling And Logging
- Wrap errors with domain/operation context. - Wrap errors with domain/operation context.
- Map domain errors to adapter-appropriate statuses/codes without leaking sensitive internals. - Map public error identities to adapter-appropriate statuses/codes without
leaking sensitive internals.
- Never emit raw secret values. - Never emit raw secret values.
## Testing And Documentation ## Testing And Documentation

View File

@@ -2,7 +2,9 @@
## Status ## Status
Proposed. Implement each stage in order and satisfy its gate before continuing. Complete as of 2026-07-28. All implementation gates, the dependency guard,
documentation reconciliation, and required validation passed. Migration Step 5
repository creation is the next gate; it was not started here.
## Objective ## Objective

View File

@@ -2,7 +2,7 @@
## Status ## Status
Accepted plan. Steps 1 through 3 are complete. Steps 4 through 9 remain Accepted plan. Steps 1 through 4 are complete. Steps 5 through 9 remain
proposed and are not yet implemented. proposed and are not yet implemented.
## Objective ## Objective
@@ -125,8 +125,8 @@ is accepted and records the required ownership and coordination decisions.
Strengthen or add contract-focused tests where needed so extraction can be Strengthen or add contract-focused tests where needed so extraction can be
verified without relying on package placement. verified without relying on package placement.
The accepted implementation scope and intended completion state are in the The completed Step 3 gate records the accepted implementation scope and
[Step 3 framework-characterization roadmap](step3.md). intended completion state.
Preserve coverage of: Preserve coverage of:
@@ -172,6 +172,12 @@ implementations.
**Gate:** The CLI and HTTP adapters use only the public framework API for **Gate:** The CLI and HTTP adapters use only the public framework API for
framework behavior, and all tests and documented smoke commands pass. framework behavior, and all tests and documented smoke commands pass.
**Gate status:** Complete as of 2026-07-28. CLI `run`, `render`, and `serve`,
the HTTP handler, and prepared-run formatting use the public facade; the
restricted HTTP reader is injected through the public extension point. The
dependency guard, full tests, vet, build, race checks, maintained examples, and
configuration smoke checks passed. Step 5 repository creation is next.
### Step 5: Create The Promptkit Repository ### Step 5: Create The Promptkit Repository
Create the Promptkit repository and Go module as an explicit out-of-band Create the Promptkit repository and Go module as an explicit out-of-band

View File

@@ -2,9 +2,10 @@
## Status ## Status
Proposed. Migration Steps 1 through 3 are complete; this roadmap defines the Complete as of 2026-07-28. The public-facade adapter boundary is established;
required target state for Step 4. The separate the [implementation plan](implementation.md) records the completed work, and
[implementation plan](implementation.md) defines the ordered work. the [main migration roadmap](migration.md) identifies repository creation as
the next gate.
## Purpose ## Purpose
@@ -19,31 +20,18 @@ owns the long-term project boundary. The
[main migration roadmap](migration.md) owns the overall sequence. This feature [main migration roadmap](migration.md) owns the overall sequence. This feature
roadmap defines the desired Step 4 state, not an implementation sequence. roadmap defines the desired Step 4 state, not an implementation sequence.
## Current Gap ## Achieved Boundary
The public `Engine` facade already supports the ordinary `Prepare` and `Run` The CLI, HTTP handler, and prepared-run formatter now consume public engine
workflows, directory and alternate framework sources, injected model clients, values and errors. `serve` injects Scriptorium's HTTP-owned restricted artifact
public result values, and broad public error classification. The executable reader through the public extension point, while the root facade continues to
adapters do not yet use that boundary consistently: compose the framework implementation inside this repository. A repository-level
dependency test protects the direct-import boundary.
- the CLI constructs framework repositories, readers, renderer, validator, and
OpenAI-compatible client directly, then calls the internal runner;
- the HTTP handler accepts internal domain request and result values;
- HTTP error mapping inspects framework-internal sentinels;
- the HTTP artifact-containment reader implements the internal artifact-reader
interface;
- prepared-run formatting accepts an internal domain value; and
- adapter tests frequently construct internal runners or use internal domain
values.
Those dependencies would prevent Scriptorium from compiling after the
framework packages move to Promptkit. They also allow the executable to exercise
a different composition path from downstream Go consumers.
## Target State ## Target State
At completion, Scriptorium's executable path is an ordinary consumer of the Scriptorium's executable path is an ordinary consumer of the same public
same public framework boundary used by other Go applications: framework boundary used by other Go applications:
```text ```text
cmd/scriptorium cmd/scriptorium

View File

@@ -0,0 +1,64 @@
package adapter_test
import (
"go/parser"
"go/token"
"os"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
func TestScriptoriumAdaptersUseOnlyPublicFrameworkBoundary(t *testing.T) {
_, testFile, _, ok := runtime.Caller(0)
if !ok {
t.Fatal("locate dependency guard source")
}
adapterDir := filepath.Dir(testFile)
directories := []string{
filepath.Join(adapterDir, "cli"),
filepath.Join(adapterDir, "http"),
filepath.Join(adapterDir, "..", "format"),
}
forbidden := map[string]struct{}{
"gitea.maximumdirect.net/eric/scriptorium/internal/domain": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/usecase": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/promptdef": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/prompt": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/profile": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/profile/builtin": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/validate": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/llm": {},
"gitea.maximumdirect.net/eric/scriptorium/internal/artifact": {},
}
for _, directory := range directories {
entries, err := os.ReadDir(directory)
if err != nil {
t.Fatalf("read source directory %s: %v", directory, err)
}
for _, entry := range entries {
if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") {
continue
}
path := filepath.Join(directory, entry.Name())
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
t.Fatalf("parse imports in %s: %v", path, err)
}
for _, imported := range file.Imports {
importPath, err := strconv.Unquote(imported.Path.Value)
if err != nil {
t.Fatalf("parse import path in %s: %v", path, err)
}
if _, found := forbidden[importPath]; found {
t.Errorf("%s directly imports forbidden framework package %s", path, importPath)
}
}
}
}
}