From ff31f8daf8c8582e654cb7193cbd348f43933dae Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 26 Jul 2026 14:20:06 +0000 Subject: [PATCH] Refocus internal component documentation --- docs/development.md | 2 +- docs/internal/adapters.md | 205 ++++++++++++-------------------------- docs/internal/llm.md | 85 ++++++++++++++++ docs/internal/overview.md | 2 +- docs/internal/runner.md | 187 +++++++++++++++------------------- docs/internal/sources.md | 200 ++++++++++++------------------------- 6 files changed, 297 insertions(+), 384 deletions(-) create mode 100644 docs/internal/llm.md diff --git a/docs/development.md b/docs/development.md index 9f82204..3d26ec6 100644 --- a/docs/development.md +++ b/docs/development.md @@ -36,7 +36,7 @@ Start with: | Application configuration | [Configuration contract](config.md), [internal component overview](internal/overview.md), [adapter internals](internal/adapters.md), and [source internals](internal/sources.md) | | Prompt, profile, schema, or artifact loading | [Configuration contract](config.md), [internal component overview](internal/overview.md), and [source internals](internal/sources.md) | | Runner orchestration, rendering, validation, or repair | [Runner internals](internal/runner.md) and [source internals](internal/sources.md) | -| OpenAI-compatible request or response behavior | [OpenAI-compatible integration](integrations/openai-compatible-chat.md), [runner internals](internal/runner.md), and [adapter internals](internal/adapters.md) | +| OpenAI-compatible request or response behavior | [OpenAI-compatible integration](integrations/openai-compatible-chat.md), [LLM internals](internal/llm.md), [runner internals](internal/runner.md), and [adapter internals](internal/adapters.md) | | Subprocess behavior | [Subprocess integration](integrations/subprocess.md) and [CLI contract](cli.md) | | Runtime operation, recovery, or troubleshooting | [Operations](operations.md) and [troubleshooting](troubleshooting.md) | | Examples or copyable assets | The owning contract for the demonstrated behavior and the related files under `examples/` | diff --git a/docs/internal/adapters.md b/docs/internal/adapters.md index 09d1a4b..b37e1b0 100644 --- a/docs/internal/adapters.md +++ b/docs/internal/adapters.md @@ -2,148 +2,86 @@ ## Purpose -Adapters translate external interfaces into domain requests and translate domain results back out. They wire dependencies, apply app config, and own IO concerns, but they do not make runner decisions. +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`. -Source-loading behavior belongs in `docs/internal/sources.md`. User-facing CLI, HTTP, and package contracts belong in `docs/cli.md`, `docs/api.md`, and `docs/consumers/pkg-scriptorium.md`. +External contracts are canonical in the [CLI reference](../cli.md), [HTTP API +reference](../api.md), and [Go package contract](../consumers/pkg-scriptorium.md). -## Adapter Map +## Components And Collaborators -- `cmd/scriptorium`: process entrypoint. -- `internal/adapter/cli`: command parsing, config handoff, runner construction, stdout/stderr, exit codes. -- `internal/adapter/http`: `POST /v1/runs` request/response mapping and HTTP error/status mapping. -- root package `scriptorium`: public Go facade over internal runner types and dependencies. +- `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. +- 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. -Supporting implementation packages used during adapter wiring: +## Wiring Flows -- `internal/config` -- `internal/defaults` -- `internal/format` -- `internal/llm` -- `internal/prompt` +### CLI -## Inputs And Outputs +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. -CLI adapter: +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. -- Input: process args, optional config file, filesystem sources, environment variables. -- Output: process exit code, stdout artifact/prepared output, stderr summaries and errors. +### HTTP -HTTP adapter: +The handler first enforces transport limits, strict JSON decoding, and the +minimal request shape. It maps DTO values to domain 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. -- Input: HTTP request method/path/headers/body for `POST /v1/runs`. -- Output: JSON success or error body with mapped status code. +The [HTTP API reference](../api.md) owns the route, DTO schema, status codes, +and externally observable limit behavior. -Public Go facade: +### Public Go Facade -- Input: typed `scriptorium.Config`, `Option`, and `RunRequest` values. -- Output: typed `PreparedRun` and `RunResult` values plus public sentinel errors. +`NewEngine` applies public options, selects filesystem, `fs.FS`, single-file, +or in-memory dependencies, and constructs a runner. The conversion functions +copy maps and slices across the boundary so callers do not receive internal +domain values. The facade maps selected internal errors to the public sentinel +set and keeps direct request API keys out of public results. -## Boundaries +## Package-Local Guarantees -- Adapters convert external shapes to `domain.RunRequest` and back. -- Runner orchestration remains in `internal/usecase`. -- Prompt/profile/schema/artifact source rules remain in repository, validator, and artifact packages. -- LLM provider request serialization remains in `internal/llm`. -- Public package types are facade types; internal domain types do not leak across the package boundary. +- Adapters do not embed runner 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. +- 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 + application state. -## Config Fields Used +## Failure And Verification Boundaries -Adapter app settings: +Keep external error payloads concise, preserve strict external decoding, and do +not serialize resolved secret values. Validation content failures remain result +state; runtime failures remain errors for the relevant adapter to map. -- `prompt_dir` -- `profile_dir` -- `schema_dir` -- `server.addr` -- `server.artifact_root` -- `server.max_request_bytes` -- `server.max_artifact_bytes` -- `server.max_response_bytes` -- `defaults.render_format` - -Execution request/profile settings passed through the runner: - -- `endpoint` -- `model` -- `temperature` -- `max_tokens` -- `top_p` -- `timeout_seconds` -- `service_tier` -- `api_key_env` -- `reasoning_effort` -- `extra_params` - -CLI and HTTP preserve numeric override presence so omitted values and explicit zero values remain distinct. - -## CLI Adapter - -Implemented commands: - -- `run` -- `render` -- `serve` - -Behavior: - -- `run` constructs a runner with direct filesystem artifact reading and calls `Runner.Run`. -- `render` constructs a runner and calls `Runner.Prepare`; it does not call the LLM. -- `serve` constructs a restricted artifact reader and HTTP handler, then starts an unauthenticated HTTP server. -- `run` exits `2` when generation succeeds but validation fails. -- parse, runtime, and output-write errors exit `1`. -- deprecated `--prompt-id` and `--profile-id` aliases are accepted. - -## HTTP Adapter - -Behavior: - -- Accepts only `POST /v1/runs`. -- Decodes JSON strictly and rejects unknown fields and trailing JSON tokens. -- Rejects empty `prompt_id` and empty `inputs` before calling the runner. -- Does not accept raw API key values in the request body. -- Returns validation failures as `200` responses with failed validation details. -- Maps request-body, artifact, and encoded-response size failures to `413`. -- Maps domain and repository errors to stable error codes without returning wrapped internal cause text. - -The HTTP adapter has no built-in authentication or authorization. Deployment controls must be provided outside the process. - -## Public Go Facade - -Behavior: - -- `NewEngine` wires the same default runner components as CLI/HTTP unless options override them. -- Prompt, profile, and schema sources may come from directories, single files, or `fs.FS` roots. -- `WithProfiles` adds in-memory profiles ahead of file-backed and built-in profiles. -- `WithLLMClient` injects custom model behavior. -- `RunRequest.APIKey` is request-scoped and direct; it is used only for generation and is stripped from public results. -- internal errors are mapped to public sentinels in `errors.go`. - -## Failure Behavior - -Adapters should: - -- keep external error payloads concise and stable. -- avoid leaking raw secret values. -- use sentinels and typed errors for mapping. -- preserve strict external input decoding. -- keep validation content failures distinct from runtime errors. - -CLI writes human-readable summaries to stderr. HTTP writes JSON error envelopes. The public Go facade returns typed errors. - -## State And Manifests - -Adapters do not add durable run state. - -- No adapter writes run manifests. -- No adapter implements checkpoint, skip, or resume behavior. -- CLI output files are caller-selected artifacts, not internal state. - -## Tests To Inspect +Inspect focused tests when changing this area: - `internal/adapter/cli/run_test.go` - `internal/adapter/http/handler_test.go` - `engine_test.go` - `internal/format/prepared_run_test.go` -- `internal/llm/openai_compatible_client_test.go` + +Run the affected adapter package tests and recheck the relevant canonical +contract. The [testing policy](../policy/testing.md) owns global test +sufficiency guidance. ## Change Recipes @@ -154,33 +92,22 @@ Adapters do not add durable run state. precedence while wiring it through its consuming adapter. 3. Add focused configuration and adapter tests for parsing, mapping, and effective behavior. -4. Update the [configuration contract](../config.md) and any external contract - affected by the new behavior. +4. Update the [configuration contract](../config.md) and any affected external + contract. ### CLI Flags -1. Add the flag to the relevant command in `internal/adapter/cli/run.go`. +1. Add the flag to the relevant parser in `internal/adapter/cli/run.go`. 2. Keep command scope and application-configuration precedence intentional. 3. Add or update parser and command tests in `internal/adapter/cli/run_test.go`. -4. Update the [CLI contract](../cli.md) and any maintained examples affected by - the invocation. +4. Update the [CLI contract](../cli.md) and affected maintained examples. ### Adapter Capabilities 1. Define or reuse the appropriate domain or use-case interface boundary. -2. Implement translation and IO behavior in the adapter without moving - use-case decisions out of `internal/usecase`. +2. Implement translation and IO behavior without moving use-case decisions out + of `internal/usecase`. 3. Add focused mapping, parsing, and error-behavior tests. -4. Update this document and the affected canonical public or integration - contract. Update [source internals](sources.md) when source-loading behavior - changes. - -## Architectural Invariants - -- Adapter packages stay thin and translation-focused. -- App config is resolved before dependency construction. -- External input strictness is part of contract stability. -- CLI and HTTP construct runners without a repairer. -- HTTP endpoint details remain canonical in `docs/api.md`. -- Public Go package details remain canonical in `docs/consumers/pkg-scriptorium.md`. +4. Update this document and the affected public or integration contract. Update + [source internals](sources.md) when source-loading behavior changes. diff --git a/docs/internal/llm.md b/docs/internal/llm.md new file mode 100644 index 0000000..bb97ad4 --- /dev/null +++ b/docs/internal/llm.md @@ -0,0 +1,85 @@ +# LLM Internals + +## Purpose + +`internal/llm` defines the provider-neutral `Client` interface and the +OpenAI-compatible client implementation. The [OpenAI-compatible integration +contract](../integrations/openai-compatible-chat.md) owns the outbound HTTP wire +format and protocol behavior. + +## Construction + +`NewOpenAICompatibleClient` validates a non-empty configured base URL, records +an optional default model, and establishes the default timeout. A non-positive +configured timeout uses the internal default. + +When callers supply an `http.Client`, construction clones it rather than +mutating the caller's instance. A supplied client with no timeout receives the +resolved default in the clone; a supplied non-zero timeout is retained. The +client stores the trimmed base URL, default model, timeout, and cloned client. + +## Generate Flow + +`Generate` receives a `domain.GenerateRequest` from the runner: + +1. validate the effective timeout and choose the request endpoint; +2. map the domain request to the internal wire-request representation; +3. validate and flatten extra parameters, encode JSON, and create the HTTP + request; +4. prefer a direct API key, otherwise resolve the configured key environment + variable; +5. derive a request HTTP client when an explicit timeout changes the configured + client; +6. execute the request, reject non-success status responses without returning + provider response bodies; and +7. decode the response subset into `domain.GenerateResponse`. + +`openAIChatRequestFromGenerateRequest` is the conversion boundary for effective +model defaults, explicit numeric-presence state, rendered messages, structured +output, and session-ID validation. `openAIChatRequestPayload` protects reserved +fields and JSON encoding before an HTTP call. The external payload shape is +defined only in the [integration contract](../integrations/openai-compatible-chat.md). + +## Error Categories + +The package uses these internal sentinels: + +- `ErrInvalidConfig` for invalid client construction; +- `ErrInvalidRequest` for invalid effective generation input; +- `ErrRequestFailed` for request construction or transport failures; +- `ErrUnexpectedStatus` for non-success HTTP responses; and +- `ErrMalformedResponse` for invalid or incomplete successful-response data. + +The runner maps an invalid LLM request to its invalid-request category and +other LLM failures to its generation category. Adapters then apply their public +error contracts. + +## Package-Local Guarantees + +- The default-model fallback happens before wire encoding. +- Per-request timeout handling clones a configured HTTP client when needed; it + does not mutate shared client state. +- Direct API keys take precedence over environment lookup within this client. +- Provider response bodies are discarded for non-success status responses. +- The client does not implement retries, tool calls, or a stateful session + store. + +## Verification And Change Recipe + +Inspect: + +- `internal/llm/openai_compatible_client_test.go` +- `internal/usecase/runner_test.go` +- `internal/adapter/http/handler_test.go` + +When changing the client: + +1. keep domain-to-wire mapping inside `internal/llm` and preserve the `Client` + interface; +2. test construction, timeout selection, mapping, and error categorization; +3. update the [OpenAI-compatible integration contract](../integrations/openai-compatible-chat.md) + for any observable wire or protocol change; and +4. update [runner internals](runner.md) if the client boundary or structured + output handoff changes. + +The [testing policy](../policy/testing.md) owns global test sufficiency. diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 0dd890b..8721015 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -41,7 +41,7 @@ and invariants; public behavior belongs in the linked contracts. | --- | --- | --- | | `internal/format` | Formats 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), [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) | Focused internal documents describe the components that have detailed orchestration, adapter, or source behavior. Package tests live alongside the diff --git a/docs/internal/runner.md b/docs/internal/runner.md index 66e5d38..3582f20 100644 --- a/docs/internal/runner.md +++ b/docs/internal/runner.md @@ -2,145 +2,118 @@ ## Purpose -`internal/usecase.Runner` is the core prompt-execution orchestrator. It prepares prompt requests, calls the configured LLM client for `Run`, validates generated output, and returns domain results. +`internal/usecase.Runner` is the prompt-execution orchestrator. It prepares +domain requests, invokes an injected LLM client, validates output, and returns +domain results. Transport parsing, response mapping, and public type conversion +remain outside this package. -Transport parsing, DTOs, CLI output, HTTP status mapping, and public package type conversion belong outside the runner. +The [configuration reference](../config.md) owns prompt, profile, schema, and +runtime-setting definitions. Public error behavior is defined by the +[HTTP API](../api.md) and [Go package](../consumers/pkg-scriptorium.md) +contracts. -## Inputs And Outputs +## Dependencies And Construction -Primary inputs: +`Runner` receives these collaborators: -- `domain.RunRequest` -- repositories/readers/renderers/validators injected at construction -- `context.Context` for cancellation +- `promptdef.Repository`; +- `profile.Repository`; +- `artifact.Reader`; +- `prompt.Renderer`; +- `llm.Client`; +- `validate.Validator`; and +- an optional `OutputRepairer`. -Primary outputs: - -- `domain.PreparedRun` from `Prepare` -- `domain.RunResult` from `Run` -- wrapped sentinel errors for adapter mapping - -LLM boundary types: - -- `domain.GenerateRequest` -- `domain.GenerateResponse` - -## Dependencies - -`Runner` depends on package interfaces instead of concrete adapter types: - -- `promptdef.Repository` -- `profile.Repository` -- `artifact.Reader` -- `prompt.Renderer` -- `llm.Client` -- `validate.Validator` -- optional `usecase.OutputRepairer` - -The CLI, HTTP adapter, and public Go package construct these dependencies and pass them in. - -## Config Fields - -`Runner` does not read app config files. Effective behavior is determined by injected dependencies and the `domain.RunRequest`. - -Adapter wiring commonly reflects these app config fields: - -- `prompt_dir` -- `profile_dir` -- `schema_dir` -- `server.artifact_root` -- HTTP request/artifact/response size limits - -Runtime model settings are resolved from the selected profile plus request overrides. +`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. ## Prepare Flow -`Prepare`: +`Prepare` performs one deterministic preparation pass for a request: -1. requires a non-empty prompt ID. -2. loads the prompt definition and computes its hash. -3. selects the profile from request `profile_id`, then prompt `default_profile`. -4. loads the selected execution profile. -5. merges built-in execution defaults, profile values, and request overrides. -6. applies request-scoped direct API key values for public Go callers. -7. validates endpoint, model, and credential requirements. -8. resolves the output contract and JSON Schema document when required. -9. reads input artifacts. -10. renders prompt messages and hashes the rendered prompt. -11. returns a prepared run without calling the LLM. +1. validate the prompt ID and load the prompt definition; +2. hash the definition and select the explicit or default profile; +3. load the profile and resolve effective execution settings; +4. validate endpoint, model, and credential availability; +5. resolve the output contract and, for JSON Schema output, load a structured + schema document before model execution; +6. read and hash input artifacts; +7. render messages and the session ID; and +8. return a `PreparedRun` containing the effective state and rendered-prompt + hash. -Numeric request overrides are presence-aware: omitted values preserve the current effective value, while explicit zero values are real overrides. +Execution settings merge defaults, profile values, and a request override. +Numeric override presence is retained so explicit zero values are not confused +with omissions. -## Run Flow +## Run And Validation Flow -`Run`: +`Run` creates a run ID and timestamps, then calls `Prepare` rather than +duplicating preparation. It sends the prepared prompt, effective target, +target-presence state, and optional structured-output specification to the LLM +client. It converts the returned content to an output artifact, validates it, +and returns the artifact, validation, hashes, usage, and timing metadata. -1. creates a run ID and start timestamp. -2. calls `Prepare`. -3. calls the injected LLM client with rendered messages, effective target, target presence, and structured-output settings. -4. builds the output artifact. -5. validates the output. -6. optionally attempts bounded repair when a repairer is injected and the contract permits repair. -7. returns the run result with artifact, raw output, validation, hashes, selected profile/model metadata, usage, and timing. +A validator can return a content result or an operational error. Content +failures stay in the result; schema loading, compilation, and validator +operational failures are returned as `ErrValidation`. The canonical distinction +for callers is documented by the public contracts. -`Run` must reuse `Prepare`; prepare logic should not be duplicated elsewhere. +## Repair Boundary -## Validation And Repair +Repair is an internal optional loop. It starts only when a repairer is present, +the output contract permits one or more attempts, validation failed, and the +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. -Validation content failures are returned as successful run results with `Validation.Status == failed`. They are not runtime errors. +`NewDefaultOutputRepairer` delegates to the injected LLM client. CLI, HTTP, and +the public engine use `NewRunner` and therefore do not inject this repairer. -Validation runtime failures, such as schema load or compile errors, return `ErrValidation`. +## Error Translation -Repair attempts occur only when all conditions are true: - -- a repairer is injected -- `repair_attempts` is greater than zero -- validation status is `failed` -- validation mode is `json` or `json_schema` - -CLI and HTTP wiring call `usecase.NewRunner(...)`, which does not inject a repairer. Normal CLI and HTTP execution therefore does not repair invalid output. - -## Failure Behavior - -Stable runner sentinels include: +Runner sentinels identify failure categories for adapters: - `ErrInvalidRequest` - `ErrProfileRequired` -- `ErrAPIKeyEnvMissing` -- `ErrAPIKeyRequired` -- `ErrPromptLoad` -- `ErrProfileLoad` -- `ErrArtifactLoad` +- `ErrAPIKeyEnvMissing` and `ErrAPIKeyRequired` +- `ErrPromptLoad`, `ErrProfileLoad`, and `ErrArtifactLoad` - `ErrPromptRender` - `ErrLLMGenerate` - `ErrValidation` -Adapters should use `errors.Is` against sentinels and lower-level repository errors instead of matching message text. +Wrap errors with those sentinels and preserve their identities through +`errors.Is`; adapters must not classify errors by message text. The runner +passes direct keys only to the LLM boundary and never includes resolved key +values in prepared or run results. -Secret values must not appear in prepared output, run results, logs, HTTP responses, or serialized public package results. The effective API-key environment-variable name may appear. +## Package-Local Guarantees -## State And Manifests +- `Run` always reuses `Prepare`. +- Schema documents are loaded before the initial LLM call when structured output + is required. +- Output validation records attempts used, including repair attempts. +- Runner state is per request; the package does not create a durable run store + or manifest. +- Source, renderer, validator, and LLM implementations remain injected + boundaries. -The runner is stateless across requests. +## Verification And Change Recipe -- No durable run store. -- No manifest files. -- No checkpoint, skip, or resume behavior. -- Recovery is a new request after correcting inputs, config, or environment. - -## Tests To Inspect +Inspect: - `internal/usecase/runner_test.go` - `internal/usecase/integration_test.go` - `engine_test.go` -- `internal/adapter/cli/run_test.go` -- `internal/adapter/http/handler_test.go` -## Architectural Invariants +When changing orchestration: -- Use-case decisions stay in `internal/usecase`. -- `Run` reuses `Prepare`. -- Prompt/profile/artifact/schema loading remains behind injected boundaries. -- Validation content failures are result state; validation runtime failures are errors. -- Repair loops are bounded by `repair_attempts` and repairer presence. -- Resolved secret values are never serialized or emitted. +1. identify the collaborator boundary and the affected `Prepare` or `Run` state; +2. preserve the `Run`-through-`Prepare` path and error identity; +3. add focused runner or integration tests for changed state transitions, + validation, or repair behavior; and +4. update the owning external contract and any affected source or LLM internal + document. + +The [testing policy](../policy/testing.md) owns global test sufficiency. diff --git a/docs/internal/sources.md b/docs/internal/sources.md index 2875c6a..411a3cb 100644 --- a/docs/internal/sources.md +++ b/docs/internal/sources.md @@ -2,142 +2,79 @@ ## Purpose -This document covers implemented prompt, profile, schema, artifact, and catalog source behavior. It is for developers changing loaders or source wiring. +This document describes how source packages load prompt definitions, profiles, +schemas, and artifacts. The [configuration reference](../config.md) owns their +user-facing formats and settings. The [HTTP API reference](../api.md) owns +HTTP-visible artifact outcomes; [operations](../operations.md) owns deployment +handling. -Full user-facing YAML and config reference material belongs in `docs/config.md`. +## Prompt Definitions -## Prompt Definition Sources +`internal/promptdef` provides filesystem and `fs.FS` repositories. Both use +`internal/filecatalog` for recursive YAML discovery, deterministic ordering, +display paths, and root cleaning. -`internal/promptdef` provides directory-backed and `fs.FS` repositories. +Repositories select a prompt by YAML ID and optional version rather than by +path. They decode through strict YAML handling, reject duplicate matching +definitions, and resolve `content_file` relative to the definition. The `fs.FS` +implementation resolves content paths inside its source root; absolute paths and +traversal outside that root are rejected before file access. -Behavior: +## Profiles And Built-Ins -- recursively scans `.yaml` and `.yml` files. -- decodes YAML with known-fields checking. -- looks up prompts by YAML `id`, not by path. -- optionally filters by prompt `version`. -- rejects duplicate matching prompt IDs. -- requires `id`, `version`, and at least one message. -- requires each message to set exactly one of `content` or `content_file`. -- resolves filesystem `content_file` values relative to the prompt YAML file. -- resolves `fs.FS` `content_file` values inside the configured source root. -- permits prompt subdirectories only as organization; they are not part of prompt identity. +`internal/profile` provides filesystem, `fs.FS`, and overlay repositories. +`internal/profile/builtin` exposes embedded assets through the same repository +interface. -For `fs.FS` roots, absolute paths and relative traversal outside the source root are rejected by catalog path helpers. +An overlay asks its primary source first. It falls back only when the primary +reports `ErrProfileNotFound`; invalid YAML, duplicate IDs, validation failures, +and raw-key failures are returned rather than hidden by fallback. This makes a +custom ID override a built-in ID while retaining errors in the custom source. -## Profile Sources +The public engine can overlay in-memory profiles ahead of both file-backed and +built-in repositories. Profile field definitions, validation ranges, and the +built-in catalog remain in the [configuration reference](../config.md). -`internal/profile` provides directory-backed, `fs.FS`, and overlay repositories. `internal/profile/builtin` embeds built-in profile YAML assets and exposes them through the same repository interface. +## Schemas -Behavior: +`internal/validate` supplies `StandardValidator` for filesystem sources and +`FSValidator` for `fs.FS` sources. Directory-backed validation loads the named +schema path; it does not search directories by basename. `fs.FS` schema paths +are cleaned and checked against their configured root, while a single-file +source matches its file base name. -- recursively scans `.yaml` and `.yml` files. -- decodes YAML with known-fields checking. -- looks up profiles by YAML `id`, not by path. -- rejects duplicate IDs inside the same source. -- rejects raw `api_key` fields in YAML; file-backed profiles must use `api_key_env`. -- validates required `endpoint` and `model` values. -- validates numeric profile ranges. +The runner requests a schema document before generation when it needs +structured output. JSON and schema mismatches in generated content are +validation results; source access, decoding, registration, and compilation +failures are operational errors. -Overlay behavior: +## Artifacts -- custom profiles are primary. -- built-in profiles are fallback. -- fallback occurs only after a primary `ErrProfileNotFound`. -- primary validation, YAML, duplicate, and raw-key errors are returned directly. -- duplicate IDs across custom and built-in sources are allowed because the custom profile overrides the built-in one. +`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. The restricted composite reader used by the HTTP adapter combines +inline reading with a rooted file reader and optional byte limit. -The public Go facade can add in-memory profiles ahead of file-backed and built-in profiles. +The rooted reader cleans paths and applies lexical containment without resolving +symlinks. It checks relative references against the configured root and accepts +absolute references only when they remain inside that lexical root. The OS still +follows symlinks after that check. The public containment outcome is documented +by the [HTTP API reference](../api.md); deployment permissions belong in +[operations](../operations.md). -## Schema Sources +## Failure Boundaries -`internal/validate` provides: +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. -- `StandardValidator` for filesystem paths. -- `FSValidator` for `fs.FS` roots and single-file public schema sources. +Source reads use current filesystem or `fs.FS` content for each request. These +packages create no manifests, checkpoints, or durable run state. -Behavior: +## Verification And Change Recipe -- `json_schema` validation requires a non-empty `schema_path`. -- filesystem schema paths resolve relative to `schema_dir` unless absolute. -- directory-backed schema lookup uses the explicit `schema_path`; it does not search recursively by basename. -- `fs.FS` schema paths must remain inside the configured source root. -- single-file schema sources match by the configured file base name. -- schema documents are loaded before the LLM call for structured output. -- JSON parse failures are validation content failures. -- schema access, decode, registration, and compile failures are runtime validation errors. - -## Artifact Sources - -`internal/artifact` supports two input artifact reference types: - -- `inline` -- `file` - -Inline behavior: - -- requires a non-empty body. -- produces text/plain artifacts. -- hashes the body bytes. - -Direct file behavior: - -- used by CLI `run`, CLI `render`, and the public Go facade. -- requires a non-empty URI. -- reads from the process filesystem without HTTP artifact-root restrictions. -- infers content type from file extension, defaulting to text/plain. - -Restricted file behavior: - -- used by HTTP `serve`. -- allows inline artifacts even when no artifact root is configured. -- denies file artifacts when no artifact root is configured. -- resolves relative file URIs against `server.artifact_root`. -- accepts absolute file URIs only when they pass containment checks. -- applies `server.max_artifact_bytes` when configured. - -Restricted containment is lexical. It cleans paths and checks the relative path against the configured root; it does not resolve symlinks. Symlinks inside the root are followed by the operating system, including symlinks that target files outside the root. - -## Catalog Helpers - -`internal/filecatalog` centralizes shared source helpers: - -- recursive YAML discovery for filesystem and `fs.FS` roots. -- deterministic sorting. -- `.yaml` and `.yml` filtering. -- display paths for diagnostics. -- YAML file stems. -- `fs.FS` root cleaning and containment checks. - -Repository code should use these helpers instead of reimplementing path traversal and containment rules. - -## Failure Behavior - -Common source failures: - -- missing prompt/profile/schema/artifact files. -- invalid YAML or JSON. -- unknown YAML fields. -- duplicate prompt or profile IDs. -- prompt/profile validation errors. -- raw API key fields in profile YAML. -- unsupported artifact reference type. -- missing inline body or file URI. -- artifact outside HTTP root. -- artifact exceeding HTTP size limit. -- schema load or compile failure. - -Prompt/profile repository lookup errors are mapped by adapters separately from runtime runner errors. Validation content failures remain result state; source and schema runtime failures return errors. - -## State And Manifests - -Source packages do not persist run state. - -- No manifests are read or written. -- No source package implements skip or resume behavior. -- Source reads reflect the current filesystem or `fs.FS` state for each request. - -## Tests To Inspect +Inspect: - `internal/promptdef/repository_test.go` - `internal/profile/repository_test.go` @@ -147,23 +84,14 @@ Source packages do not persist run state. - `internal/usecase/integration_test.go` - `engine_test.go` -## Change Recipe +When updating prompt, profile, schema, or built-in assets: -When updating prompt, profile, schema, or built-in-profile assets: +1. keep assets valid for the strict loader and the relevant source boundary; +2. update the [configuration reference](../config.md) when a file-format, + catalog, or default changes; +3. run focused source and integration tests, including the built-in repository + test when embedded assets change; and +4. update this document when discovery, precedence, containment, or failure + mechanics change. -1. Keep files valid for their strict loader and source boundary. -2. Keep maintained examples and fixtures secret-free. -3. Run focused prompt, profile, schema, or validation tests for the changed - source. -4. Update the [configuration contract](../config.md) and every affected - external contract; update this document when loading or precedence mechanics - change. - -## Architectural Invariants - -- Prompt/profile identity comes from YAML `id`. -- External YAML decoding remains strict. -- File-backed profile YAML never accepts raw API key values. -- Built-in profiles are fallback, not a replacement for custom source validation. -- HTTP file artifacts remain rooted by lexical containment. -- Schema runtime failures remain errors, while JSON/schema content mismatches remain validation results. +The [testing policy](../policy/testing.md) owns global test sufficiency.