diff --git a/README.md b/README.md index b08fc8a..e68a32a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,9 @@ Weatherreporter is a Go CLI that turns normalized weather data into human-facing Markdown reports. It produces a Markdown report at an operator-owned destination and can upload -the completed output through Distributor. +the completed output through Distributor. It can also compare explicitly +selected Promptkit profiles against one shared prepared report and publish a +local comparison bundle. ## Quickstart @@ -24,5 +26,6 @@ guide](docs/operations.md) for command and operating details. - [CLI reference](docs/cli.md) - [Configuration reference](docs/config.md) - [Operations guide](docs/operations.md) +- [Comparison bundle contract](docs/integrations/comparison-bundle.md) - [Development guide](docs/development.md) - [Architecture policy](docs/policy/architecture.md) diff --git a/docs/cli.md b/docs/cli.md index 27cbb6e..b126064 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -1,7 +1,8 @@ # Weatherreporter CLI -`weatherreporter` generates Markdown weather reports and runs report batches. -It has no command for inspecting prior runs or application-owned state. +`weatherreporter` generates Markdown weather reports, runs report batches, and +compares explicitly selected Promptkit profiles against one prepared report. It +has no command for inspecting prior runs or application-owned state. ## Shortest Useful Command @@ -25,6 +26,7 @@ weatherreporter generate tomorrow [--config PATH] [--units VALUE] [--tz NAME] [- weatherreporter generate hourly [--config PATH] [--units VALUE] [--tz NAME] [--out PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter run morning [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] [--llm-debug-dir PATH] [--quiet] weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] [--llm-debug-dir PATH] [--quiet] +weatherreporter compare REPORT --profile PROFILE --profile PROFILE [--config PATH] [--units VALUE] [--tz NAME] [--date YYYY-MM-DD] [--out-dir PATH] [--replace] [--llm-debug-dir PATH] [--quiet] ``` `weatherreporter --version` prints the version embedded in the executable. @@ -38,10 +40,13 @@ builds report `development`. | `generate tomorrow` | Uses the next local civil day and writes `tomorrow.md` by default. | | `generate hourly` | Covers the next six hours in the effective report timezone and writes `hourly.md` by default. It does not accept `--date`, `--hours`, or `--duration`. | | `run morning` and `run evening` | Run their defined report batches beneath the configured output directory, or the current directory when none is configured. `--out-dir` selects another directory. `--out` is not accepted. | +| `compare REPORT` | Accepts `daily`, `today`, `tomorrow`, or `hourly`. It requires at least two distinct, nonblank `--profile` values in their supplied order. Daily requires `--date`; Today accepts it optionally; Tomorrow and Hourly do not accept it. | `generate` accepts the four report command names shown above. `run` accepts -only `morning` and `evening`. Batch membership and notification ordering are -described in the [operations guide](operations.md). +only `morning` and `evening`. `compare` always requires explicit profile +selection: `promptkit.profile` is not used as a comparison default. Batch +membership and notification ordering are described in the +[operations guide](operations.md). ## Output, Errors, And Quiet Mode @@ -50,21 +55,25 @@ For `generate`, the report's default filename is placed beneath PATH` selects one complete output file instead. A relative path is resolved from the current directory; an absolute path is used as given. For a batch, the configured directory has the same role and `--out-dir PATH` selects its -output directory instead. Successful summaries always report the resulting -absolute `outputPath` values. See the [configuration reference](config.md) for -the field's validation and path rules. +output directory instead. For `compare`, `--out-dir PATH` selects one exact +bundle directory; otherwise the report-derived comparison directory is placed +beneath the configured directory or current directory. `--replace` is required +to replace an existing nonempty recognized comparison bundle. See the +[configuration reference](config.md) for the field's validation and path rules +and the [comparison bundle contract](integrations/comparison-bundle.md) for the +bundle format. Outputs are written atomically. A generation, rendering, write, or cancellation failure before publication leaves an existing destination unchanged. A notification failure occurs after publication, so the newly written output remains available. -Action commands (`generate` and `run`) write a JSON summary to stdout unless -`--quiet` is set. `run` also writes compact per-report and batch status lines -to stderr. A pre-run error, such as an invalid flag, missing required argument, -or configuration-load failure, produces no partial JSON summary. When an -action fails after it has produced a result, its summary has `"status": "failed"` -and an `error` field. +Action commands (`generate`, `run`, and `compare`) write a JSON summary to +stdout unless `--quiet` is set. `run` also writes compact per-report and batch +status lines to stderr. A pre-run error, such as an invalid flag, missing +required argument, or configuration-load failure, produces no partial JSON +summary. When an action fails after it has produced a result, its summary has +`"status": "failed"` and an `error` field. `--quiet` is supported by action commands only. It suppresses action summaries and routine batch status output; it does not suppress command errors. @@ -113,19 +122,41 @@ report=today status=succeeded output="/srv/weather/reports/today.md" batch=morning total=2 succeeded=2 failed=0 ``` +### Compare Summary + +A comparison summary contains these fields in this order: `command`, +`comparisonId`, `reportId`, `reportName`, `promptId`, `promptVersion`, +`promptHash`, `status`, `startedAt`, `finishedAt`, `timezone`, `validPeriod`, +`outputDirectory`, `manifestPath`, `dataPackagePath`, `total`, `succeeded`, +`failed`, `results`, and optional `error`. Published artifact paths and each +successful `results[].reportPath` are absolute. `results` preserves the +supplied profile order and each item contains `position`, `profileId`, optional +`backendId`, `modelName`, `status`, optional `validationStatus`, optional +`reportPath`, optional `llmDebugPath`, and optional safe `error`. + +The comparison status is `succeeded` only when every selected profile succeeds +and the bundle is published. Individual profile failures still publish a +complete partial bundle and return a failed command result. Cancellation or a +failure before publication omits the artifact paths and returns a safe +top-level error; it does not expose provider diagnostics. See the +[comparison bundle contract](integrations/comparison-bundle.md) for durable +artifact fields and failure invariants. + ## Flag Reference | Flag | Accepted by | Meaning | | --- | --- | --- | | `-h`, `--help` | top level | Show help. | | `--config PATH` | all commands | Load `PATH` instead of `/usr/local/etc/weatherreporter/config.yml`. | -| `--units VALUE` | `generate`, `run` | Override `weather_api.units` for this command. | -| `--tz NAME` | `generate`, `run` | Override `weather_api.timezone` for this command. | +| `--units VALUE` | `generate`, `run`, `compare` | Override `weather_api.units` for this command. | +| `--tz NAME` | `generate`, `run`, `compare` | Override `weather_api.timezone` for this command. | | `--out PATH` | every `generate` command | Write the report to this complete file destination instead of the configured or current-directory default. | -| `--llm-debug-dir PATH` | every `generate` and `run` command | Write requested sensitive prompt diagnostics under this absolute path. | -| `--out-dir PATH` | `run morning`, `run evening` | Write batch reports beneath this directory instead of the configured or current-directory default. | -| `--quiet` | `generate`, `run` | Suppress action summaries and routine batch status output. | -| `--date YYYY-MM-DD` | `generate daily`, `generate today` | Required for Daily; optional for Today. | +| `--llm-debug-dir PATH` | every `generate`, `run`, and `compare` command | Write requested sensitive prompt diagnostics under this absolute path. | +| `--profile PROFILE` | `compare` | Select one explicit profile. Repeat at least twice with distinct, nonblank IDs. | +| `--out-dir PATH` | `run morning`, `run evening`, `compare` | Write batch reports beneath this directory, or select the exact comparison directory. | +| `--replace` | `compare` | Authorize replacement of a recognized nonempty comparison bundle. | +| `--quiet` | `generate`, `run`, `compare` | Suppress successful action output and routine batch status output. | +| `--date YYYY-MM-DD` | `generate daily`, `generate today`, `compare daily`, `compare today` | Required for Daily; optional for Today. | Distributor notification is configured through `notify.distributor`; there are no Distributor-specific CLI flags. See the [configuration reference](config.md). @@ -138,4 +169,5 @@ weatherreporter generate today --out ./reports/today.md weatherreporter generate hourly --out /srv/weather/hourly.md weatherreporter generate today --llm-debug-dir /var/tmp/weatherreporter-debug weatherreporter run morning --out-dir ./reports --llm-debug-dir /var/tmp/weatherreporter-debug +weatherreporter compare daily --date 2026-05-29 --profile weather-light --profile weather-balanced --out-dir ./comparison-daily-2026-05-29 ``` diff --git a/docs/config.md b/docs/config.md index 819e2af..8127e37 100644 --- a/docs/config.md +++ b/docs/config.md @@ -84,7 +84,7 @@ this directory, never in the YAML file. ### `output` `output.directory` selects the ordinary operator-owned publication directory -for both individual reports and batches. +for individual reports, batches, and the default parent of comparison bundles. | Field | Default | Rules | | --- | --- | --- | @@ -101,6 +101,8 @@ publication. For one `generate` command, `--out` is a complete file destination and takes precedence over `output.directory`. For `run`, `--out-dir` takes precedence. +For `compare`, `--out-dir` selects its exact bundle directory; without it, the +comparison's report-derived directory is placed beneath `output.directory`. Those explicit flags do not inspect or rebase beneath the configured directory. See the [CLI reference](cli.md) for command selection and the [operations guide](operations.md) for publication and failure handling. @@ -166,11 +168,11 @@ source keys are `observations`, `current`, `narrative`, `alerts`, `discussion`, ### `promptkit` Promptkit configuration selects the executor and prompt/profile checks for -every `generate` and `run` command. A top-level `scriptorium:` configuration +every `generate`, `run`, and `compare` command. A top-level `scriptorium:` configuration key is rejected with a migration error; it is not translated or ignored. Prompt debug capture has no YAML setting. Use `--llm-debug-dir PATH` on an -individual `generate` or `run` command when explicitly needed. +individual `generate`, `run`, or `compare` command when explicitly needed. | Field | Default | Rules | | --- | --- | --- | diff --git a/docs/development.md b/docs/development.md index 4e6520b..16805bc 100644 --- a/docs/development.md +++ b/docs/development.md @@ -21,14 +21,15 @@ boundaries and invariants. | Adding, changing, reviewing, or deleting tests | [Testing policy](policy/testing.md) and focused package tests | The policy defines risk-based sufficiency, durable test boundaries, doubles, and test-maintenance criteria. | | CLI commands, flags, output, quiet mode, or command wiring | [CLI reference](cli.md) and [CLI internals](internal/cli.md) | The reference owns the user contract; the internal guide owns command composition and output flow. | | Configuration fields, defaults, loading, overrides, validation, or secrets | [Configuration reference](config.md), [architecture policy](policy/architecture.md), and tests under `internal/config` | These separate the user-visible contract, architectural rules, and executable behavior. | -| Top-level generation, batch, collection, output publication, or notification workflow | [App orchestration internals](internal/app-orchestration.md) | It owns workflow ordering, output publication, failure propagation, and orchestration invariants. | +| Top-level generation, batch, comparison, collection, output publication, or notification workflow | [App orchestration internals](internal/app-orchestration.md), [comparison execution internals](internal/comparison-execution.md), and [comparison publication internals](internal/comparison-publication.md) | They own workflow ordering, concurrent profile execution, output publication, failure propagation, and orchestration invariants. | | Weather API transport, source envelopes, source warnings, or collection | [Weather API integration](integrations/weatherapi.md), [weather-data internals](internal/weather-data.md), and [collection internals](internal/collect.md) | These separate the external contract, normalized source facts, and app-facing collection behavior. | | Forecast periods, weather derivation, collected facts, or derived facts | [Forecast derivation internals](internal/forecast-derivation.md) and [fact contracts](internal/facts.md) | They own deterministic derivation and the fact boundaries used by reports. | | Report definitions, valid periods, report IDs, output naming, or batch composition | [Report registry internals](internal/report-registry.md) and [app orchestration internals](internal/app-orchestration.md) | Report definitions own selection and period rules; orchestration owns execution. | | Module IDs, module composition, briefing values, or prompt-facing exports | [Module contract internals](internal/module.md), [module builder internals](internal/briefing.md), and [prompt-input internals](internal/prompt-input.md) | These own module contracts, value construction, and the curated prompt-package boundary. | -| Prompt execution, profiles, prompt inputs, or result handling | `internal/promptexec`, the Promptkit adapter, and [prompt-input internals](internal/prompt-input.md) | These separate the executor contract and input construction. | +| Prompt execution, profiles, prepared report inputs, or result handling | `internal/promptexec`, the Promptkit adapter, [prepared report internals](internal/prepared-report.md), and [prompt-input internals](internal/prompt-input.md) | These separate the executor contract, immutable preparation, and input construction. | +| Durable comparison bundles or their compatibility | [Comparison bundle contract](integrations/comparison-bundle.md) and [comparison publication internals](internal/comparison-publication.md) | The integration document owns the external schema; internals own how it is published. | | Generated-text schemas, validation, render contexts, templates, or Markdown rendering | [Generated-text internals](internal/generatedtext.md), [report-template internals](internal/reporttemplate.md), and [report template guide](templates.md) | These own structured text, renderer implementation, and the maintainer-facing template surface. | -| Output destinations, atomic publication, prompt diagnosis, or legacy cleanup | [Operations guide](operations.md) and [App orchestration internals](internal/app-orchestration.md) | Operations owns operator workflows; app internals owns the implementation boundary. | +| Output destinations, atomic publication, prompt diagnosis, or legacy cleanup | [Operations guide](operations.md), [App orchestration internals](internal/app-orchestration.md), and [comparison publication internals](internal/comparison-publication.md) | Operations owns operator workflows; internals own implementation boundaries. | | Distributor bundles, uploads, notification results, or failures | [Distributor adapter internals](internal/distributor-adapter.md), [Distributor integration contracts](integrations/distributor/), and [operations guide](operations.md) | These separate adapter behavior, external contracts, and operational lifecycle. | | Maintained example configuration | [Configuration reference](config.md) and files under `examples/` | The reference owns field meaning; examples own complete copyable files. | | Release preparation, tagging, publication, or verification | [Release procedure](release.md) | It owns version selection, release-note preparation, candidate validation, tag publication, CI behavior, and post-publication checks. | @@ -44,7 +45,8 @@ present before introducing a new package or abstraction. | --- | --- | | `cmd/weatherreporter` | Binary entry point. | | `internal/cli` | Command parsing, flags, help, output, and command wiring. | -| `internal/app` | Stateless generation, batches, collection coordination, output publication, and notification. | +| `internal/app` | Stateless generation, batches, comparisons, collection coordination, output publication, and notification. | +| `internal/comparison` | Comparison identities, logical bundles, guarded destinations, and atomic bundle publication. | | `internal/config` | Configuration defaults, loading, precedence, secrets, and validation. | | `internal/adapters` | Weather API, Promptkit, and Distributor boundaries. | | `internal/weatherdata`, `internal/forecast`, `internal/facts` | Normalized source facts and deterministic derivation. | diff --git a/docs/integrations/comparison-bundle.md b/docs/integrations/comparison-bundle.md new file mode 100644 index 0000000..e2d8814 --- /dev/null +++ b/docs/integrations/comparison-bundle.md @@ -0,0 +1,92 @@ +# Comparison Bundle Contract + +A comparison bundle is the durable, flat artifact produced when one report is +executed with multiple explicit Promptkit profiles. This document is the +canonical contract for consumers of those bundles. Command invocation and JSON +action summaries belong to the [CLI reference](../cli.md); destination handling +and retention belong to the [operations guide](../operations.md). + +## Version And Layout + +The current and only supported manifest schema version is +`weatherreporter.comparison.v1`. A bundle directory contains exactly these +regular, non-symlinked files: + +```text +comparison.json +data-package.yml +NN-profile-slug.md +``` + +`comparison.json` is the manifest and `data-package.yml` is the exact YAML +input supplied to every selected profile. There is one Markdown file for each +successful result and none for failed results. `NN` is the one-based selected +profile position, zero padded to at least two digits (and widened only when +needed for 100 or more profiles). The profile slug preserves ASCII letters, +digits, `-`, and `_`; each run of other characters becomes one `-`; edge `-` +and `_` characters are removed; the value is capped at 64 bytes; and an empty +slug becomes `profile`. Logical profile IDs remain authoritative in the +manifest. + +All manifest paths are basenames relative to the bundle root. They never use +path separators, `.` or `..`. The CLI reports absolute paths only after a +bundle has been published. + +## Manifest Schema + +The manifest is UTF-8 JSON, encoded as two-space-indented JSON with one +trailing newline. Its fields appear in this order: + +```text +schemaVersion, comparisonId, startedAt, finishedAt, reportId, validPeriod, +timezone, promptId, promptVersion, promptHash, dataPackage, total, succeeded, +failed, results +``` + +`validPeriod` contains `start` and `end`; it is a nonempty half-open period. +`dataPackage` contains `path` (always `data-package.yml`) and `sha256` (the +lowercase, 64-character SHA-256 digest of that file's exact bytes). `results` +is in the explicit profile-selection order. Its result-object fields appear in +this order: + +```text +position, profileId, backendId, modelName, status, validationStatus, +reportPath, error +``` + +`startedAt` and `finishedAt` are nonzero UTC timestamps, and the latter is not +earlier than the former. `validPeriod` retains its resolved time offset. +`reportId`, `timezone`, prompt identity, model name, and comparison ID are +nonblank. `promptHash` and `dataPackage.sha256` are lowercase SHA-256 digests. + +## Result Invariants + +`total` is at least two and equals the number of results. Positions are +contiguous from one, profile IDs are distinct and nonblank, and +`succeeded + failed == total`. + +A successful result has `status: "succeeded"`, `validationStatus: "passed"`, +a unique Markdown `reportPath`, and no `error`. A failed result has +`status: "failed"`, no `reportPath`, and an `error` object with nonblank +`category` and `message`. Its validation status is absent, `failed`, or +`skipped`. Error messages are valid UTF-8 and no longer than 1,024 bytes. +`backendId` and `validationStatus` are omitted when unavailable. + +Every successful Markdown file is declared by exactly one successful result. +The directory contains no extra entries. Consumers can therefore verify the +data-package digest and the full manifest-to-file mapping without scanning a +larger workspace. + +## Compatibility And Sensitivity + +Weatherreporter recognizes a replaceable bundle only when it exactly satisfies +the current version, schema, file set, file types, relative-path rules, and +data-package digest. It rejects unknown manifest fields, multiple JSON values, +extra entries, symlinks, and future or otherwise unsupported versions. Treat a +bundle that fails recognition as an ordinary directory, not as a compatible +bundle. + +The manifest contains safe operational provenance, but `data-package.yml` and +the generated Markdown can contain sensitive weather or location context. Do +not assume these artifacts are safe for public distribution. Handle retention, +access, and deletion according to the [operations guide](../operations.md). diff --git a/docs/integrations/promptkit.md b/docs/integrations/promptkit.md index 0433cdf..72ee026 100644 --- a/docs/integrations/promptkit.md +++ b/docs/integrations/promptkit.md @@ -29,6 +29,22 @@ Profiles that require a direct API key are unsupported; a profile that reports ` Promptkit receives the YAML data package as an inline input and returns structured JSON that Weatherreporter validates before rendering its own Markdown template. Safe active provenance remains in memory. Content-rich diagnostics are opt-in through `--llm-debug-dir`; see [operations](../operations.md) for retention and permissions. +## Comparison Execution + +For `compare`, Weatherreporter validates one exact prompt and every explicitly +selected profile before weather collection. It prepares one deterministic YAML +data package, retains immutable copies of the report inputs, and executes every +profile against the same exact data-package bytes. Each profile remains an +independent Promptkit execution: one provider or validation failure does not +stop its peers, while caller cancellation applies to every in-flight execution. + +Weatherreporter starts selected profile executions concurrently and does not +add an application-level concurrency limit. Promptkit owns backend capacity and +any profile or backend concurrency policy. The durable comparison output and +its compatibility rules are defined by the +[comparison bundle contract](comparison-bundle.md); the user-facing command +contract is in the [CLI reference](../cli.md). + The generated-text schemas require `summary`, `forecast_discussion`, and `precipitation_timing`, and reject additional properties. Prompts return an empty string for `precipitation_timing` when the deterministic package contains no precipitation windows. Prompt/profile configuration and the maintained local override example are owned by the [configuration reference](../config.md). Adapter construction and mapping are documented in the [Promptkit adapter internals](../internal/promptkit-adapter.md). diff --git a/docs/internal/app-orchestration.md b/docs/internal/app-orchestration.md index d083b51..2b51589 100644 --- a/docs/internal/app-orchestration.md +++ b/docs/internal/app-orchestration.md @@ -1,6 +1,9 @@ # Application Orchestration Internals -`internal/app` owns stateless report generation, batch execution, atomic output publication, and notification coordination after `internal/cli` has parsed arguments and loaded configuration. The user contract is owned by the [CLI reference](../cli.md) and [operations guide](../operations.md). +`internal/app` owns stateless report generation, batch execution, comparison +orchestration, atomic output publication, and notification coordination after +`internal/cli` has parsed arguments and loaded configuration. The user contract +is owned by the [CLI reference](../cli.md) and [operations guide](../operations.md). ## Single-Report Flow @@ -16,6 +19,23 @@ Failures return an active partial result with safe identity, profile, warning, v Each item has an independent result. A failed item does not stop later items; successful items retain their published output paths. Per-report notification is suppressed during a batch. Batch notification runs only after every planned report has published successfully. It is skipped when any item failed. Batch result counters count report items only; a batch notification failure is represented by the top-level notification result and still produces a failed batch outcome. +## Comparisons + +`CompareDetailed` validates ordered explicit profile IDs, resolves the report, +and preflights the exact bundle destination before initializing optional prompt +debugging, prompt inspection, or collection. It then inspects the one prompt +and every selected profile, collects once, and delegates shared report +construction to the prepared-report flow. It does not accept a notifier. + +The comparison execution core starts each inspected profile independently, +keeps results in selection order, and waits for all started work. Independent +profile failures are recorded and do not stop peers. Context cancellation marks +unfinished work and prevents publication. Details of prepared values, execution +and debugging, and publication are documented in [prepared report +internals](prepared-report.md), [comparison execution +internals](comparison-execution.md), and [comparison publication +internals](comparison-publication.md). + ## Boundaries And Verification The package does not parse flags, load YAML, implement transport, construct provider SDKs, or define report-period policy. Prompt, profile, weather, and Distributor implementations remain behind project-owned contracts. diff --git a/docs/internal/comparison-execution.md b/docs/internal/comparison-execution.md new file mode 100644 index 0000000..db3f681 --- /dev/null +++ b/docs/internal/comparison-execution.md @@ -0,0 +1,25 @@ +# Comparison Execution Internals + +The comparison execution core receives an already prepared report and an +already inspected, ordered profile list. It initializes an outcome for every +selected profile, launches each started profile in its own goroutine, and +waits for every started goroutine before returning. Results retain the supplied +selection order even though execution completes in an arbitrary order. + +Every profile uses the exact inspected prompt identity and a private copy of +the same prepared data package. Provider, generated-text validation, rendering, +or debug-write failure becomes that profile's safe failed outcome and does not +cancel its peers. The application deliberately imposes no additional semaphore: +Promptkit owns backend capacity. Cancellation or a deadline marks unfinished +outcomes as skipped or failed, joins work, and prevents bundle publication. + +When debugging is enabled, each execution receives a deterministic reference +derived from the comparison identity, ordered profile position, and safe +profile slug. This keeps concurrent captures separate. The debug writer itself +owns secure-root validation and file permissions; operational retention and +sensitivity are documented in the [operations guide](../operations.md). + +The output result and its safe errors are converted into the durable contract +only by comparison publication. See [comparison publication +internals](comparison-publication.md) and the external [comparison bundle +contract](../integrations/comparison-bundle.md). diff --git a/docs/internal/comparison-publication.md b/docs/internal/comparison-publication.md new file mode 100644 index 0000000..241c04b --- /dev/null +++ b/docs/internal/comparison-publication.md @@ -0,0 +1,22 @@ +# Comparison Publication Internals + +`internal/comparison` separates the logical bundle from filesystem mechanics. +The application builds a validated manifest, exact shared data-package bytes, +and only the Markdown files for successful profiles. The durable layout, +schema, and compatibility rules are owned by the [comparison bundle +contract](../integrations/comparison-bundle.md). + +Destination planning is read-only. It requires an exact absolute target that +is neither the filesystem root nor the working directory, rejects unsafe +symlinks and non-directories, accepts a missing or empty directory, and permits +replacement only for a recognized current bundle. Publication rechecks that +plan, writes a private sibling staging directory, and installs it atomically. +Replacement temporarily moves the recognized prior bundle aside and restores it +if installation fails. + +The application preflights before prompt inspection and collection, then +preflights again before publication. A cancellation or any failure before the +commit leaves the prior destination untouched. Completed bundles include +partial profile results; comparison publication never coordinates Distributor +notification. Operator-facing lifecycle and cleanup are in the +[operations guide](../operations.md). diff --git a/docs/internal/prepared-report.md b/docs/internal/prepared-report.md new file mode 100644 index 0000000..12dee26 --- /dev/null +++ b/docs/internal/prepared-report.md @@ -0,0 +1,20 @@ +# Prepared Report Internals + +`internal/app` builds a `preparedReport` after collection and before profile +execution. This is the immutable boundary shared by ordinary report generation +and profile comparison; it is not a durable artifact. + +Preparation builds report facts, the configured module snapshot, briefing +metadata, the curated prompt-input package, serialized YAML, and the +generated-text definition. It deep-copies mutable facts, snapshots, metadata, +and data-package bytes before returning them. Consumers receive independent +copies so one execution cannot change another's input or rendering context. + +Single-report generation executes one prepared profile and publishes its +Markdown. Comparison prepares once, gives every selected profile the same YAML +bytes, and only then assembles the resulting logical bundle. The prompt-input +shape is owned by [prompt-input internals](prompt-input.md); profile execution +semantics are owned by [Promptkit integration](../integrations/promptkit.md). + +Preparation failure has no publication side effects. Tests for this boundary +cover mutation isolation, byte equality, and reuse by both execution paths. diff --git a/docs/operations.md b/docs/operations.md index d83aab9..9723e42 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -72,6 +72,38 @@ For a single report, Distributor notification follows the atomic output write. See the [configuration reference](config.md) for pipeline, bundle, idempotency-key, and per-report path templates. +## Comparison Bundles + +Use `compare` when an operator needs to evaluate explicit Promptkit profiles +against the same report input. The command writes one flat, operator-owned +bundle directory and never sends a Distributor notification. Command syntax, +profile validation, JSON output, and exit behavior belong to the +[CLI reference](cli.md); the durable file contract belongs to the +[comparison bundle contract](integrations/comparison-bundle.md). + +The output destination follows the normal `output.directory` fallback. An +explicit `--out-dir` takes precedence and names the exact bundle directory, +not a parent to be combined with another name. The standard names are derived +from the report output name, such as `comparison-today` and +`comparison-daily-2026-05-29`; see the [configuration reference](config.md) +for output-directory resolution. + +A comparison bundle contains the shared data package, a manifest, and one +Markdown file for every successful profile. Treat all of these files as +potentially sensitive: the data package and generated reports can contain +location or forecast context. Weatherreporter creates no application-owned +history, retention store, or cleanup job. Retain, archive, or remove only the +specific bundle directories your operating policy permits. + +The destination is preflighted before prompt inspection and collection, then +rechecked immediately before an atomic publish. A missing or empty directory +is usable. A nonempty directory can be replaced only when `--replace` is given +and it is recognized as a current Weatherreporter comparison bundle; ordinary +directories, symlinks, and unsafe destinations are rejected. Cancellation and +all failures before publication preserve an existing bundle. Profile failures +are different: the command publishes a complete partial bundle, with failed +profiles represented in the manifest and no Markdown file for those profiles. + ## Local Prompt Profile Override Hourly normally selects the embedded `weather-light` profile. To use a local @@ -99,9 +131,11 @@ weatherreporter generate today --llm-debug-dir /var/tmp/weatherreporter-debug The directory must be absolute. Requested captures are written with restrictive permissions beneath the supplied directory, organized by report and run. They can contain rendered prompts and generated output, so limit access to trusted -operators and remove the captures when they are no longer needed. Normal output, -summaries, and routine logs omit that sensitive content. Debug capture is never -created for an ordinary command without `--llm-debug-dir`. +operators and remove the captures when they are no longer needed. Comparison +captures additionally identify each selected profile so concurrent executions +remain distinct. Normal output, summaries, and routine logs omit that sensitive +content. Debug capture is never created for an ordinary command without +`--llm-debug-dir`. If capture creation or writing fails, the affected run fails rather than silently continuing without the requested diagnostics. @@ -111,8 +145,12 @@ silently continuing without the requested diagnostics. Start with the command error and JSON summary. For a report generation failure, the selected destination was not replaced; for a notification failure, inspect the completed destination and the notification result. For a batch failure, -use the per-report statuses and retain successful output files. Enable explicit -debug capture only when content-rich Promptkit diagnostics are necessary. +use the per-report statuses and retain successful output files. For a comparison +failure, inspect the published manifest when its path is present: individual +profile failures retain their safe result and successful Markdown files, while +cancellation and pre-publication errors leave the prior destination unchanged. +Enable explicit debug capture only when content-rich Promptkit diagnostics are +necessary. Weatherreporter does not retain runs for later inspection, resume failed work, or provide automatic cleanup, archival, remote state, daemon operation, or diff --git a/docs/policy/architecture.md b/docs/policy/architecture.md index a614c81..b5ba871 100644 --- a/docs/policy/architecture.md +++ b/docs/policy/architecture.md @@ -15,6 +15,11 @@ and renders repository-owned Markdown in memory. Completed Markdown is atomically published to an operator-owned output destination and may then be uploaded through Distributor. +An explicit profile comparison prepares one report input once, executes the +same exact prompt and data package across selected profiles concurrently, and +atomically publishes one operator-owned comparison bundle. It remains local: +it does not create application state or send a Distributor notification. + The supported report products are Daily, Today, Tomorrow, and Hourly. A batch collects once, validates its complete candidate prompt/profile set before collection, then determines and validates every planned output destination @@ -29,6 +34,8 @@ planned report succeeds. - `internal/config` owns defaults, loading, validation, and secret loading. - `internal/app` owns in-memory workflow order, partial results, atomic output publication, and notification coordination through project-owned contracts. +- `internal/comparison` owns comparison identity, durable logical bundle + validation, safe destination recognition, and atomic bundle publication. - Deterministic domain packages own weather derivation, report periods, modules, generated-text validation, and template contexts. - `internal/adapters/weatherapi`, `internal/adapters/promptkit`, and @@ -47,6 +54,9 @@ directly. before collection. - Prompt and profile validation completes before weather collection. Raw output is validated before template rendering. +- Comparison validates every explicit profile before collection, prepares one + immutable report input, and delegates backend capacity to Promptkit rather + than adding an application-wide execution limit. - Generated text fills defined prose slots only. Deterministic facts remain authoritative and repository-owned templates produce all Markdown output. - Sensitive rendered prompts, schemas, input bodies, provider endpoints, and @@ -64,11 +74,16 @@ directly. does not remove a newly published output. - Configuration or explicit CLI input selects that operator-owned destination; it does not create an application-owned state boundary. +- Comparison bundles are flat, versioned operator outputs. Their guarded + replacement accepts only a recognized current bundle; cancellation and every + pre-publication failure preserve a prior bundle, while individual profile + failures can publish a complete partial bundle. - Distributor uploads use only the published Markdown output, never a scan of local files. Single notification follows publication; batch notification follows publication of every selected report. Batch counters describe report outcomes only; a failed batch notification is represented separately at the batch level. +- Comparison never invokes Distributor notification. - Default tests are deterministic, offline, and use Promptkit/provider fakes rather than live provider calls. See the [testing policy](testing.md). diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 2634d09..2a87c09 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # LLM Profile Comparison Implementation Plan -Status: Ready for implementation. +Status: Complete. ## Purpose And Authority diff --git a/docs/roadmap/profile-comparison.md b/docs/roadmap/profile-comparison.md index 96cd16f..3b2433f 100644 --- a/docs/roadmap/profile-comparison.md +++ b/docs/roadmap/profile-comparison.md @@ -1,6 +1,6 @@ # LLM Profile Comparison Roadmap -Status: Accepted; unimplemented. +Status: Implemented; retained temporarily for post-implementation review. ## Purpose