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
`Read(context.Context, ArtifactRef) (*Artifact, error)`. Supplying it through
`WithArtifactReader` replaces the engine's default inline and file reader for
all inputs. Reader errors remain available through `errors.Is` alongside
`ErrArtifactLoad`; a nil artifact with no error is treated as an artifact-load
failure. Readers must supply artifact metadata and should not retain or mutate
the caller's values.
`WithArtifactReader` replaces, rather than extends, the engine's default inline
and file reader for every input. Omitting the option retains that default;
`WithArtifactReader(nil)` makes engine construction fail with
`ErrInvalidConfig`.
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
@@ -191,5 +196,8 @@ Public methods preserve these sentinel checks through `errors.Is`:
- `ErrLLMGenerate`
- `ErrValidation`
For interface selection and operational responsibilities, see the
[consumer integration overview](api.md).
`ErrProfileRequired` and `ErrAPIKeyEnvMissing` each also match
`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
Adapters translate external inputs into domain requests, compose dependencies,
and translate domain results or errors back to their interface. They own IO and
presentation mechanics; use-case decisions remain in `internal/usecase`.
Adapters translate external inputs into public engine requests and translate
public results or errors back to their interface. They own IO and presentation
mechanics; use-case decisions remain behind the root `scriptorium` facade.
External contracts are canonical in the [CLI reference](../cli.md), [HTTP API
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
`internal/adapter/cli`.
- `internal/adapter/cli` parses commands, resolves application settings through
`internal/config`, constructs a runner, and owns process output handling.
- `internal/adapter/http` decodes DTOs, maps them to `domain.RunRequest`, calls
a runner interface, and maps errors and results to HTTP DTOs.
`internal/config`, constructs the public engine, and owns process output
handling.
- `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
collaborators and maps selected internal errors to public sentinels.
- `internal/format` formats prepared runs for the CLI; `internal/llm`,
`internal/prompt`, and source packages supply runner dependencies.
- `internal/format` formats public prepared runs for the CLI.
## Wiring Flows
### CLI
The CLI resolves configuration before constructing dependencies. `run` builds a
runner with the ordinary composite artifact reader and invokes `Runner.Run`;
`render` uses the same wiring and invokes `Runner.Prepare`; `serve` replaces the
file reader with the restricted artifact reader, builds an HTTP handler, and
starts the server.
The CLI resolves configuration before constructing the public engine. `run`
calls `Engine.Run` with a public request and `render` calls `Engine.Prepare`
with the same request mapping. `serve` constructs the HTTP-owned restricted
artifact reader, injects it with `WithArtifactReader`, passes the resulting
engine directly to the HTTP handler, and starts the server.
Parser state records whether numeric runtime values were explicitly supplied.
That presence is carried into `domain.ExecutionTargetOverride`, allowing the
runner to distinguish omitted values from explicit zero overrides.
That presence is carried into `scriptorium.ExecutionTargetOverride`, allowing
the engine to distinguish omitted values from explicit zero overrides.
### HTTP
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
the domain result to the response DTO; on failure it uses `errors.Is` over
runner, source, artifact, and profile errors to choose the public error mapping.
the public result to the response DTO; on failure it uses `errors.Is` over
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,
and externally observable limit behavior.
@@ -57,10 +59,10 @@ set and keeps direct request API keys out of public results.
## 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.
- CLI and HTTP create runners without a repairer; a repairer is available only
through explicit internal runner construction.
- CLI and HTTP consume the public engine without a repairer; a repairer remains
available only through explicit internal runner construction.
- DTO conversion preserves explicit numeric-override presence.
- Error mapping matches error identities, not error text.
- No adapter creates durable run state; caller-selected output files are not
@@ -105,9 +107,10 @@ sufficiency guidance.
### Adapter Capabilities
1. Define or reuse the appropriate domain or use-case interface boundary.
2. Implement translation and IO behavior without moving use-case decisions out
of `internal/usecase`.
1. Define or reuse an adapter-local consumer interface with public facade
types when a test seam is needed.
2. Implement translation and IO behavior without moving framework decisions out
of the public engine.
3. Add focused mapping, parsing, and error-behavior tests.
4. Update this document and the affected public or integration contract. Update
[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 |
| --- | --- | --- |
| `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/http` | Maps HTTP requests and responses to domain operations and maps public errors. | [HTTP API contract](../api.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 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/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/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/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) |
## Formatting, Validation, And Model Access
| 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/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`.
`NewRunner` constructs a runner without a repairer. `NewRunnerWithRepairer`
accepts one explicitly. Adapters and the public engine choose concrete
repositories and readers; the runner does not load application configuration.
accepts one explicitly. The public engine chooses concrete repositories and
readers; executable adapters reach the runner only through that engine. The
runner does not load application configuration.
## 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,
and attempt metadata; every repaired result is validated again.
`NewDefaultOutputRepairer` delegates to the injected LLM client. CLI, HTTP, and
the public engine use `NewRunner` and therefore do not inject this repairer.
`NewDefaultOutputRepairer` delegates to the injected LLM client. The public
engine, and therefore CLI and HTTP, uses `NewRunner` and does not inject this
repairer.
## Error Translation
@@ -104,7 +106,6 @@ values in prepared or run results.
Inspect:
- `internal/usecase/runner_test.go`
- `internal/usecase/integration_test.go`
- `engine_test.go`
When changing orchestration:

View File

@@ -50,14 +50,11 @@ failures are operational errors.
## Artifacts
`internal/artifact` composes inline and file readers. The ordinary composite
reader used by CLI and the public engine reads file references from the process
filesystem. `internal/adapter/http` provides the restricted public artifact
reader for HTTP containment: it combines inline reading with a rooted file
reader and optional byte limit. The existing internal restricted composite
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.
`internal/artifact` owns the framework's ordinary inline and unrestricted file
reader. The public engine uses it by default and permits consumers to replace it
for every input through the public `ArtifactReader` extension. The
HTTP adapter owns its restricted reader for HTTP containment: `serve` injects
that reader into the public engine with `WithArtifactReader`.
The rooted reader cleans paths and applies lexical containment without resolving
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
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
them to their own external contract.
schemas. The runner categorizes source failures and the public engine preserves
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
packages create no manifests, checkpoints, or durable run state.
@@ -86,7 +84,6 @@ Inspect:
- `internal/artifact/reader_test.go`
- `internal/adapter/http/artifact_reader_test.go`
- `internal/validate/standard_validator_test.go`
- `internal/usecase/integration_test.go`
- `engine_test.go`
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
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
inventory is maintained in the [internal overview](../internal/overview.md).
It also provides a public Go package for in-process use. Executable adapters
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`.
## Core Principles
- 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 external decoding strict: configuration, prompt, and profile YAML and
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
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,
and LLM interfaces rather than adapter implementations.
- Source, rendering, validation, and LLM implementations remain behind their
@@ -52,7 +58,8 @@ their implementation.
## Error Handling And Logging
- 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.
## Testing And Documentation

View File

@@ -2,7 +2,9 @@
## 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

View File

@@ -2,7 +2,7 @@
## 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.
## 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
verified without relying on package placement.
The accepted implementation scope and intended completion state are in the
[Step 3 framework-characterization roadmap](step3.md).
The completed Step 3 gate records the accepted implementation scope and
intended completion state.
Preserve coverage of:
@@ -172,6 +172,12 @@ implementations.
**Gate:** The CLI and HTTP adapters use only the public framework API for
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
Create the Promptkit repository and Go module as an explicit out-of-band

View File

@@ -2,9 +2,10 @@
## Status
Proposed. Migration Steps 1 through 3 are complete; this roadmap defines the
required target state for Step 4. The separate
[implementation plan](implementation.md) defines the ordered work.
Complete as of 2026-07-28. The public-facade adapter boundary is established;
the [implementation plan](implementation.md) records the completed work, and
the [main migration roadmap](migration.md) identifies repository creation as
the next gate.
## Purpose
@@ -19,31 +20,18 @@ owns the long-term project boundary. The
[main migration roadmap](migration.md) owns the overall sequence. This feature
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`
workflows, directory and alternate framework sources, injected model clients,
public result values, and broad public error classification. The executable
adapters do not yet use that boundary consistently:
- 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.
The CLI, HTTP handler, and prepared-run formatter now consume public engine
values and errors. `serve` injects Scriptorium's HTTP-owned restricted artifact
reader through the public extension point, while the root facade continues to
compose the framework implementation inside this repository. A repository-level
dependency test protects the direct-import boundary.
## Target State
At completion, Scriptorium's executable path is an ordinary consumer of the
same public framework boundary used by other Go applications:
Scriptorium's executable path is an ordinary consumer of the same public
framework boundary used by other Go applications:
```text
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)
}
}
}
}
}