801 lines
35 KiB
Markdown
801 lines
35 KiB
Markdown
# LLM Profile Comparison Implementation Plan
|
|
|
|
Status: Ready for implementation.
|
|
|
|
## Purpose And Authority
|
|
|
|
This document is the ordered implementation plan for the accepted [LLM
|
|
Profile Comparison Roadmap](profile-comparison.md). Implement every stage in
|
|
numeric order. The roadmap owns feature purpose, policy, scope, and target
|
|
state; this plan owns implementation sequence, concrete interfaces, and stage
|
|
exit criteria.
|
|
|
|
This is one feature delivered over ten bounded coding prompts. A stage may
|
|
refactor code needed by a later stage, but it must leave the repository
|
|
compiling, tested, and internally coherent. Do not expose an incomplete
|
|
`compare` command before the stages that implement its full command contract.
|
|
|
|
## Implementation Rules
|
|
|
|
Apply these rules in every stage:
|
|
|
|
- Read `docs/development.md`, the task-specific documents it identifies, and
|
|
all files under `docs/policy/` before changing code. Treat those documents
|
|
and the feature roadmap as binding.
|
|
- Preserve the existing behavior of `generate` and `run`, including output
|
|
precedence, report/date semantics, notification, debug capture, structured
|
|
results, and exit behavior.
|
|
- Keep Promptkit types and calls behind `internal/adapters/promptkit` and the
|
|
dependency-neutral `internal/promptexec` interface. Do not import Promptkit
|
|
from application, CLI, or comparison-artifact packages.
|
|
- Keep comparison artifacts operator-owned and explicit. They are not durable
|
|
application state and must never be discovered or consumed implicitly by a
|
|
later invocation.
|
|
- Do not add a Weatherreporter concurrency limit or semaphore. One shared
|
|
Promptkit executor is safe for concurrent use, and Promptkit v0.5.0 owns its
|
|
engine-local backend capacity.
|
|
- Use dependency injection and deterministic fakes for application and CLI
|
|
tests. Tests must be offline, credential-free, race-safe, and independent of
|
|
completion timing and machine-specific paths.
|
|
- Add regression coverage in the same stage as each behavior. Prefer
|
|
contract-level assertions over incidental implementation detail, in
|
|
accordance with `docs/policy/testing.md`.
|
|
- Run `gofmt` on changed Go files and `git diff --check` in every stage. Run
|
|
focused tests while developing, then `GOWORK=off go test -count=1 ./...`
|
|
before completing the stage. Stages that add concurrency or filesystem
|
|
replacement must also run the relevant packages with `-race`.
|
|
- Do not commit, tag, push, or prepare a release unless the implementing prompt
|
|
separately requests it.
|
|
|
|
## Locked Implementation Contracts
|
|
|
|
The following choices make the stages decision-complete. Do not substitute a
|
|
different command shape, artifact schema, publication algorithm, or concurrency
|
|
policy without first updating the accepted roadmap and this plan.
|
|
|
|
### Application Request And Result
|
|
|
|
Add this exported application request, using the existing application aliases
|
|
and interfaces:
|
|
|
|
```go
|
|
type ComparisonRequest struct {
|
|
Config config.Config
|
|
Report ReportKind
|
|
ProfileIDs []string
|
|
WorkingDir string
|
|
OutputDir string
|
|
Replace bool
|
|
LLMDebugDir string
|
|
Date time.Time
|
|
Clock timeutil.Clock
|
|
Collector Collector
|
|
Executor promptexec.Executor
|
|
}
|
|
```
|
|
|
|
`OutputDir` is the optional exact bundle-directory override. `Clock` supplies
|
|
the comparison identity plus start and finish timestamps and defaults to
|
|
`timeutil.SystemClock` when nil. `Date` retains the existing report-date
|
|
meaning.
|
|
|
|
Do not include a notifier in this request. Making notification structurally
|
|
unavailable is clearer and safer than passing a disabled notifier through the
|
|
comparison path.
|
|
|
|
Add `app.CompareDetailed(context.Context, ComparisonRequest)` returning a
|
|
non-nil `*app.ComparisonResult` whenever report and destination resolution have
|
|
progressed far enough to describe the attempted comparison. Use these field
|
|
names and meanings:
|
|
|
|
```go
|
|
type ComparisonResult struct {
|
|
ComparisonID string
|
|
ReportID report.ID
|
|
ReportName string
|
|
PromptID string
|
|
PromptVersion string
|
|
PromptHash string
|
|
StartedAt time.Time
|
|
FinishedAt time.Time
|
|
Timezone string
|
|
ValidPeriod timeutil.Period
|
|
OutputDirectory string
|
|
ManifestPath string
|
|
DataPackagePath string
|
|
Total int
|
|
Succeeded int
|
|
Failed int
|
|
Results []ComparisonProfileResult
|
|
}
|
|
|
|
type ComparisonProfileResult struct {
|
|
Position int
|
|
ProfileID string
|
|
BackendID string
|
|
ModelName string
|
|
Status string
|
|
ValidationStatus promptexec.ValidationStatus
|
|
ReportPath string
|
|
LLMDebugPath string
|
|
Error *comparison.SafeError
|
|
}
|
|
```
|
|
|
|
`OutputDirectory` is absolute once destination resolution succeeds. Manifest
|
|
and data-package paths are absolute and nonblank only after publication.
|
|
Successful report paths and any returned debug paths are also absolute.
|
|
Profile results always follow selection order.
|
|
|
|
Profile counts describe profile outcomes only. A publication failure after all
|
|
profiles succeed therefore leaves `total == succeeded` and `failed == 0`, while
|
|
the command-level status and returned error still report failure.
|
|
|
|
Reject fewer than two profile IDs, whitespace-only IDs, and exact
|
|
case-sensitive duplicates. Preserve every accepted ID byte-for-byte and in
|
|
request order; do not trim, case-fold, deduplicate, or merge it with
|
|
`config.promptkit.profile`. Validate this invariant both in the CLI and at the
|
|
application boundary.
|
|
|
|
### Comparison And File Identity
|
|
|
|
Use the resolved report metadata's existing run ID to form:
|
|
|
|
```text
|
|
comparison_<report-run-id>
|
|
```
|
|
|
|
This comparison ID is computed once from the injected clock and shared by the
|
|
application result, manifest, and debug identities.
|
|
|
|
Derive a profile filename slug as follows:
|
|
|
|
1. retain ASCII letters, ASCII digits, `-`, and `_`;
|
|
2. replace each run of every other character, including dots, whitespace,
|
|
separators, control characters, and non-ASCII characters, with one `-`;
|
|
3. trim leading and trailing `-` and `_`;
|
|
4. use `profile` if the result is empty; and
|
|
5. truncate the result to 64 ASCII bytes, then trim trailing `-` and `_` again,
|
|
falling back to `profile` if necessary.
|
|
|
|
The report filename is:
|
|
|
|
```text
|
|
<ordinal>-<slug>.md
|
|
```
|
|
|
|
The one-based ordinal width is the greater of two and the number of decimal
|
|
digits in the profile count. Filename collisions after slug normalization are
|
|
safe because the ordinal is authoritative. The exact profile ID remains the
|
|
logical identity in results and the manifest.
|
|
|
|
Derive a default comparison directory by requiring the resolved report output
|
|
name to end in the exact `.md` suffix, removing that suffix, and prefixing the
|
|
remaining basename with `comparison-`. Resolve it beneath configured
|
|
`output.directory`, or beneath the invocation working directory when no output
|
|
directory is configured. An explicit `--out-dir` is the exact bundle directory,
|
|
resolves relative to the invocation working directory, and completely
|
|
overrides configured output. Return an absolute cleaned path.
|
|
|
|
### Versioned Bundle Contract
|
|
|
|
Create a dependency-neutral `internal/comparison` package. It owns deterministic
|
|
naming, the logical bundle and manifest types, validation, recognition of prior
|
|
bundles, hashing, staging, and transactional publication. It must not import
|
|
CLI, configuration, Promptkit, collectors, or report-generation packages.
|
|
|
|
Use the schema version:
|
|
|
|
```text
|
|
weatherreporter.comparison.v1
|
|
```
|
|
|
|
The UTF-8 `comparison.json` document is indented by two spaces and ends with one
|
|
newline. Its exact top-level JSON fields, in struct declaration order, are:
|
|
|
|
```text
|
|
schemaVersion
|
|
comparisonId
|
|
startedAt
|
|
finishedAt
|
|
reportId
|
|
validPeriod
|
|
timezone
|
|
promptId
|
|
promptVersion
|
|
promptHash
|
|
dataPackage
|
|
total
|
|
succeeded
|
|
failed
|
|
results
|
|
```
|
|
|
|
`validPeriod` has `start` and `end`. `dataPackage` has `path` and `sha256`.
|
|
Each ordered result has:
|
|
|
|
```text
|
|
position
|
|
profileId
|
|
backendId (omit when empty)
|
|
modelName
|
|
status
|
|
validationStatus (omit when unavailable)
|
|
reportPath (omit for failures)
|
|
error (omit for successes)
|
|
```
|
|
|
|
An error object has `category` and `message`. Status values are exactly
|
|
`succeeded` and `failed`. A successful result must have a relative report path,
|
|
must not have an error, and must have passed generated-text validation. A
|
|
failed result must not have a report path and must have a safe error. Use the
|
|
dependency-neutral string returned by `promptexec.CategoryOf` when available;
|
|
use `application` for an unclassified application, validation, rendering, or
|
|
debug-callback failure. Bound a manifest or normal-summary error message to
|
|
1,024 UTF-8 bytes without splitting an encoded rune. Never serialize wrapped
|
|
causes, provider bodies, prompts, schemas, model output, endpoints, or
|
|
credentials.
|
|
|
|
Encode all manifest timestamps using Go's standard `time.Time` JSON encoding.
|
|
Comparison start and finish timestamps are UTC; valid-period start and end
|
|
retain their resolved timezone offsets. Require nonzero timestamps, a finish
|
|
that is not before the comparison start, and a valid-period end strictly after
|
|
its start.
|
|
|
|
Use lowercase 64-character hexadecimal SHA-256 digests. `data-package.yml`
|
|
contains the exact immutable byte slice supplied to every execution. A logical
|
|
bundle contains that data package, ordered successful Markdown report bytes,
|
|
and the complete manifest. Manifest counts and paths must agree exactly with
|
|
the ordered results and logical bundle.
|
|
|
|
### Destination Safety And Publication
|
|
|
|
Perform a read-only destination preflight before prompt inspection, weather
|
|
collection, directory creation, or model execution. Reject:
|
|
|
|
- a filesystem root;
|
|
- the exact invocation working directory;
|
|
- an existing regular file or non-directory;
|
|
- an existing symlink, including a dangling symlink;
|
|
- an uninspectable target; and
|
|
- a nonempty directory unless `--replace` is present and it is a recognized
|
|
current-schema Weatherreporter comparison bundle.
|
|
|
|
An absent target and an existing empty real directory are accepted without
|
|
`--replace`. `--replace` is also harmless for those cases. Recognition must
|
|
decode the manifest with unknown fields rejected, validate every manifest
|
|
invariant, require `data-package.yml` and every declared successful report to
|
|
be regular non-symlink files, verify the data-package digest, and require the
|
|
directory entries to be exactly the manifest, data package, and declared
|
|
successful reports. Extra files, subdirectories, links, missing files, invalid
|
|
paths, duplicate paths, or a failed digest make the directory unrecognized.
|
|
Never follow a manifest path outside the bundle.
|
|
|
|
Re-run the destination safety check immediately before publication. Build the
|
|
entire logical bundle in memory, create the parent only when publishing, and
|
|
write a private uniquely named sibling staging directory with directory mode
|
|
`0700` and files mode `0600`. Check cancellation while staging and immediately
|
|
before the commit operation.
|
|
|
|
For an absent target, atomically rename the completed sibling staging directory
|
|
to the target. For an existing empty or recognized target, rename the target to
|
|
a unique sibling backup, rename staging to the target, and restore the backup
|
|
if the second rename fails. If restoration also fails, retain the backup and
|
|
return an actionable joined error that identifies its path. Once this rename
|
|
transaction begins, finish commit or rollback without abandoning it because of
|
|
a concurrent cancellation. The final pre-commit context check is the
|
|
linearization point: cancellation observed before it prevents replacement;
|
|
cancellation arriving after it does not retroactively undo a completed commit.
|
|
Remove the backup after a successful replacement and clean staging artifacts on
|
|
ordinary failures without scanning or modifying unrelated paths.
|
|
|
|
### Shared Preparation And Concurrent Execution
|
|
|
|
Extract an immutable internal prepared-report value containing the resolved
|
|
report, collection-derived facts, module snapshot, briefing metadata,
|
|
generated-text handler/render inputs, source warnings, and the one serialized
|
|
data-package byte sequence. Preparation resolves, collects, derives, snapshots,
|
|
and marshals once.
|
|
|
|
Split the generated-text workflow into three internal responsibilities:
|
|
|
|
1. deterministic shared preparation;
|
|
2. one profile-specific Promptkit execution, validation, and in-memory
|
|
Markdown rendering; and
|
|
3. publication, with optional ordinary notification.
|
|
|
|
`GenerateDetailed` composes those responsibilities once and retains its current
|
|
single-report behavior. Comparison never calls `GenerateDetailed` in a loop.
|
|
|
|
For comparison, inspect the exact prompt once, then inspect every profile
|
|
sequentially in selection order, including effective backend, model, and
|
|
credential availability, before weather collection. Initialize an explicitly
|
|
requested debug writer before those operations so pre-execution diagnostics
|
|
retain current behavior. Any preflight failure causes no collection, model
|
|
calls, or bundle publication.
|
|
|
|
After shared preparation, start one goroutine per profile against one shared
|
|
executor. Pass the same immutable data-package bytes and exact inspected prompt
|
|
version to every call. Preallocate an ordered outcome slice and let each
|
|
goroutine write only its own index; use synchronization to join all goroutines.
|
|
One profile failure does not cancel peers. Parent cancellation propagates to
|
|
every call, no additional calls are started after cancellation is observed, and
|
|
all started goroutines are joined before return.
|
|
|
|
Give each execution a deterministic debug run identity:
|
|
|
|
```text
|
|
<comparison-id>_<ordinal>-<slug>
|
|
```
|
|
|
|
The existing debug preparation artifact remains authoritative for the exact
|
|
profile ID. Debug callback failure is a failure of only that profile and must
|
|
not collide with or cancel peers.
|
|
|
|
After all non-cancelled outcomes finish, order them by original selection,
|
|
capture one UTC finish timestamp, and construct the complete or coherent
|
|
partial bundle. Publish partial bundles when one or more profiles fail, then
|
|
return the non-nil result with a generic aggregate error so the CLI exits
|
|
nonzero. Do not include provider detail in that aggregate error. Cancellation
|
|
or deadline expiration publishes no bundle. No comparison path constructs or
|
|
calls a Distributor notifier.
|
|
|
|
### CLI Contract
|
|
|
|
Add:
|
|
|
|
```text
|
|
weatherreporter compare REPORT [options]
|
|
```
|
|
|
|
Support `daily`, `today`, `tomorrow`, and `hourly`, with the same date policy as
|
|
`generate`. Accept applicable existing `--config`, `--units`, `--tz`, `--date`,
|
|
`--llm-debug-dir`, and `--quiet` flags plus a repeatable custom `--profile`, the
|
|
exact-directory `--out-dir`, and boolean `--replace`. Do not accept `--out`, and
|
|
do not use the configured default Promptkit profile as a selection. Construct
|
|
exactly one executor per command invocation.
|
|
|
|
The normal stdout JSON summary has these fields in declaration order:
|
|
|
|
```text
|
|
command
|
|
comparisonId
|
|
reportId
|
|
reportName
|
|
promptId
|
|
promptVersion
|
|
promptHash
|
|
status
|
|
startedAt
|
|
finishedAt
|
|
timezone
|
|
validPeriod
|
|
outputDirectory
|
|
manifestPath (omit when unpublished)
|
|
dataPackagePath (omit when unpublished)
|
|
total
|
|
succeeded
|
|
failed
|
|
results
|
|
error (omit on complete success)
|
|
```
|
|
|
|
`command` is `compare`; command status is `succeeded` only when every profile
|
|
succeeds and publication succeeds, otherwise `failed`. Each result mirrors the
|
|
application profile result with absolute report and debug paths. Preserve
|
|
profile order. A coherent partial bundle is still a failed command and exits
|
|
nonzero. Failures before a structured result exists use the existing CLI error
|
|
path without inventing a partial JSON object. `--quiet` suppresses normal
|
|
stdout and routine progress output but never changes work, artifacts, or exit
|
|
status.
|
|
|
|
## Stage 1: Define Comparison Artifact And Naming Contracts
|
|
|
|
### Goal
|
|
|
|
Introduce the dependency-neutral logical contract without altering any command
|
|
or existing generation path.
|
|
|
|
### Work
|
|
|
|
1. Create `internal/comparison` with manifest, result, data-package reference,
|
|
valid-period, safe-error, logical-bundle, and bundle-report types matching
|
|
the locked schema.
|
|
2. Add constants for the schema version, `comparison.json`, and
|
|
`data-package.yml`, plus explicit succeeded and failed statuses.
|
|
3. Implement profile-list validation, deterministic slugging, ordinal width,
|
|
report filename construction, comparison ID construction, default directory
|
|
name derivation, SHA-256 formatting, safe UTF-8 error truncation, manifest
|
|
validation, and deterministic JSON encoding.
|
|
4. Ensure validation rejects inconsistent counts, positions, statuses, paths,
|
|
digests, timestamps, identities, successful results without reports,
|
|
failures with reports, duplicate paths, non-basename artifact paths, and
|
|
traversal-capable names.
|
|
5. Keep all filesystem mutation and application orchestration out of this
|
|
stage.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Table-test unusual profile IDs, empty slugs, separators, dots, controls,
|
|
Unicode, length limits, normalized collisions, and profile counts above 99.
|
|
- Round-trip the exact JSON field names, ordering, indentation, newline,
|
|
optional fields, safe errors, hashes, and manifest invariants.
|
|
- Confirm exact case-sensitive profile identity and duplicate handling.
|
|
- Run `GOWORK=off go test -count=1 ./internal/comparison` and the repository-wide
|
|
standard test command. The repository remains behaviorally unchanged.
|
|
|
|
## Stage 2: Implement Safe Bundle Recognition And Transactional Publication
|
|
|
|
### Goal
|
|
|
|
Complete the isolated filesystem boundary, including output resolution and
|
|
whole-directory replacement, before application concurrency depends on it.
|
|
|
|
### Work
|
|
|
|
1. Add read-only destination planning and preflight to `internal/comparison`,
|
|
parameterized by the absolute working directory, exact target, and replace
|
|
authorization.
|
|
2. Implement strict current-schema prior-bundle recognition with unknown JSON
|
|
fields rejected, manifest validation, exact-entry checks, symlink rejection,
|
|
regular-file checks, containment, and digest verification.
|
|
3. Implement sibling staging, fixed permissions, cancellation checks, absent
|
|
target publication, existing-target backup and replacement, rollback, and
|
|
targeted cleanup according to the locked algorithm.
|
|
4. Add an application output resolver, alongside existing output helpers, that
|
|
applies explicit/configured/current-directory precedence and derives the
|
|
default comparison name from the resolved report output name. Do not change
|
|
ordinary output resolution.
|
|
5. Return typed or inspectable errors sufficient for application and CLI code
|
|
to add context without exposing filesystem internals as a public schema.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Cover relative and absolute explicit destinations, configured destination
|
|
precedence, all report-derived names, and an invalid default `.md` suffix.
|
|
- Cover absent, empty, regular-file, symlink, dangling-symlink, filesystem-root,
|
|
exact-working-directory, unreadable, recognized, and unrecognized targets.
|
|
- Cover extra entries, missing reports, subdirectories, manifest traversal,
|
|
unknown fields, bad counts, digest mismatch, stale-report removal, successful
|
|
replacement, second-rename failure with restoration, and cancellation before
|
|
commit. Use narrow injected filesystem hooks only where real filesystem
|
|
behavior cannot deterministically reach a consequential failure path.
|
|
- Verify a failed publication leaves the prior recognized bundle byte-for-byte
|
|
intact and does not touch sibling paths.
|
|
- Run focused tests with `-race`, then the repository-wide standard tests.
|
|
|
|
## Stage 3: Add Ordered Multi-Profile Preflight
|
|
|
|
### Goal
|
|
|
|
Make prompt and profile validation reusable for comparison without collecting
|
|
weather or executing a model.
|
|
|
|
### Work
|
|
|
|
1. Extend `internal/app/prompt_inspection.go` with a comparison-oriented request
|
|
and result that inspect one resolved prompt exactly once and selected
|
|
profiles sequentially in request order.
|
|
2. Reuse the existing credential-lookup policy and return dependency-neutral
|
|
prompt hash/version plus effective backend and model for every profile.
|
|
3. Apply the locked application-level profile validation before any executor
|
|
inspection.
|
|
4. Fail on the first ordered preflight error with prompt/profile context, while
|
|
ensuring no collection or execution occurs. Do not silently fall back to
|
|
`config.promptkit.profile`.
|
|
5. Leave existing single and batch inspection entry points intact unless a
|
|
private shared helper can remove duplication without changing behavior.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Assert one prompt inspection, ordered profile inspections, effective
|
|
backend/model preservation, exact profile IDs, and credential failures.
|
|
- Assert invalid lists and any prompt/profile preflight failure perform no
|
|
collection or execution using explicit fakes.
|
|
- Run `GOWORK=off go test -count=1 ./internal/app` and repository-wide tests.
|
|
|
|
## Stage 4: Extract Immutable Shared Report Preparation
|
|
|
|
### Goal
|
|
|
|
Create the prepare-once boundary required for comparison while preserving all
|
|
ordinary generation behavior.
|
|
|
|
### Work
|
|
|
|
1. Extract an unexported immutable prepared-report value in `internal/app` with
|
|
the resolved report, facts, module snapshot, briefing metadata, handler and
|
|
render inputs, source warnings, and exact serialized data-package bytes.
|
|
2. Move current collection, derivation, snapshot, metadata, prompt-input, YAML
|
|
marshal, and generated-text-handler construction into one preparation
|
|
function.
|
|
3. Refactor `GenerateDetailed` and batch generation to use that function once
|
|
per generated report without altering external requests or results.
|
|
4. Copy mutable inputs where necessary at the boundary so concurrent consumers
|
|
cannot mutate shared maps or byte slices. Treat the prepared value as
|
|
read-only after construction.
|
|
5. Do not add comparison goroutines or the CLI command in this stage.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Preserve all existing generation and batch tests.
|
|
- Add focused tests showing one collector call, one deterministic serialized
|
|
package, stable warnings/metadata, and isolation from caller mutation.
|
|
- Compare ordinary generated report bytes, result fields, debug preparation,
|
|
notification, and failure behavior before and after the refactor through
|
|
existing contract tests.
|
|
- Run application tests and repository-wide tests.
|
|
|
|
## Stage 5: Separate Profile Execution And In-Memory Rendering From Publication
|
|
|
|
### Goal
|
|
|
|
Permit several profiles to consume one prepared report without duplicating or
|
|
prematurely publishing output.
|
|
|
|
### Work
|
|
|
|
1. Extract a profile-specific internal function that accepts the immutable
|
|
prepared report, exact prompt inspection, effective profile inspection, and
|
|
optional deterministic debug reference.
|
|
2. Have it invoke `promptexec.Executor.Execute`, preserve the existing
|
|
preparation callback contract, validate generated text, build render
|
|
context, and render Markdown to memory. Return a dependency-neutral outcome
|
|
and bytes; do not write the report or notify.
|
|
3. Extract ordinary single-report publication into a separate function that
|
|
performs the existing context check, atomic file write, and optional
|
|
Distributor notification.
|
|
4. Recompose `GenerateDetailed` from prepare, execute/render, and publish. Keep
|
|
its debug path, validation status, output path, notification, warnings, and
|
|
error wrapping unchanged.
|
|
5. Make debug callback bookkeeping local to one execution. Remove any shared
|
|
mutable workflow field that would race when the execution function is used
|
|
concurrently.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Prove execution/rendering alone performs no filesystem publication or
|
|
notification.
|
|
- Preserve ordinary success, validation failure, rendering failure, debug
|
|
callback failure, cancellation, atomic output, and notification tests.
|
|
- Confirm one ordinary generation still prepares and executes exactly once.
|
|
- Run application tests with `-race`, then repository-wide tests.
|
|
|
|
## Stage 6: Build The Concurrent Ordered Profile Execution Core
|
|
|
|
### Goal
|
|
|
|
Implement and test concurrency independently of destination publication and CLI
|
|
assembly.
|
|
|
|
### Work
|
|
|
|
1. Add a private comparison execution coordinator in `internal/app` that takes
|
|
one immutable prepared report, one prompt inspection, ordered profile
|
|
inspections, comparison identity, optional debug writer, and one shared
|
|
executor.
|
|
2. Preallocate ordered outcomes, start one goroutine per selected profile while
|
|
observing parent cancellation, and join every started goroutine. Each
|
|
goroutine may write only its own result slot.
|
|
3. Pass the exact same data-package byte content and prompt version to every
|
|
call. Do not recollect, rebuild, marshal, retry, or use `GenerateDetailed`.
|
|
4. Continue peers after an individual execution, validation, rendering, or
|
|
debug callback failure. Convert every failure to a bounded safe structured
|
|
error while retaining a wrapped internal error for aggregate diagnosis.
|
|
5. Use requested-profile order, not completion order, for outcomes and report
|
|
filenames. Generate distinct debug identities from comparison ID, ordinal,
|
|
and safe slug.
|
|
6. On parent cancellation or deadline, propagate context, join all work, and
|
|
mark non-successful outcomes safely. Do not construct a publishable bundle
|
|
for a cancelled coordinator.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Use a barrier-based fake executor to prove overlap without sleeps and to
|
|
observe more than one in-flight call.
|
|
- Assert the exact prompt version and byte-for-byte identical data package for
|
|
eight to twelve profiles, deterministic result order under reversed
|
|
completion, and no data races.
|
|
- Assert one failure does not cancel peers, cancellation reaches all started
|
|
calls and joins them, and debug references are distinct and deterministic.
|
|
- Do not duplicate Promptkit's internal capacity tests. Retain adapter tests
|
|
showing that Weatherreporter delegates prepared execution through one
|
|
concurrency-safe engine.
|
|
- Run `GOWORK=off go test -race -count=1 ./internal/app
|
|
./internal/adapters/promptkit`, then repository-wide tests.
|
|
|
|
## Stage 7: Assemble Application-Level Comparison And Bundle Publication
|
|
|
|
### Goal
|
|
|
|
Expose the complete application use case with safe partial publication,
|
|
aggregate failure behavior, and no notification.
|
|
|
|
### Work
|
|
|
|
1. Add the locked exported application request, result, profile-result, and
|
|
`CompareDetailed` entry point.
|
|
2. Orchestrate in this exact order: validate request; resolve report/date and
|
|
comparison identity; resolve and preflight destination; initialize optional
|
|
debug writing; inspect prompt and every profile; collect and prepare once;
|
|
execute profiles concurrently; capture finish time; build manifest and
|
|
logical bundle; re-preflight and publish.
|
|
3. Build `data-package.yml` from the exact prepared bytes and include only
|
|
successful rendered reports. Derive manifest and application results from
|
|
the same ordered outcome source so they cannot drift.
|
|
4. On one or more profile failures, publish the coherent partial bundle, set
|
|
ordered result errors and counts, then return the non-nil result plus a
|
|
generic aggregate error such as `comparison completed with N failed
|
|
profiles`.
|
|
5. On cancellation, deadline, preflight, preparation, or publication failure,
|
|
publish nothing new. Preserve a recognized prior bundle. Return the most
|
|
complete safe result available and an error with useful operation context.
|
|
6. Populate absolute manifest, data-package, successful-report, and debug paths
|
|
only when those artifacts exist. Keep manifest paths relative basenames.
|
|
7. Ensure no Distributor notifier is accepted, created, or invoked anywhere in
|
|
this path.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Add assembled application tests for complete success, mixed profile failure,
|
|
all-profile failure, preflight failure, collection/preparation failure,
|
|
cancellation, publication failure, and authorized replacement.
|
|
- Assert prompt inspection once, profile inspection before collection, weather
|
|
collection once, serialized package reuse, completion-independent order,
|
|
exact counts/digest/paths, generic aggregate errors, and no model calls before
|
|
all preflight succeeds.
|
|
- Assert complete and partial comparison never notify and that cancellation or
|
|
publication failure preserves an existing bundle.
|
|
- Run application and comparison packages with `-race`, then repository-wide
|
|
tests.
|
|
|
|
## Stage 8: Add Compare Command Parsing And Request Construction
|
|
|
|
### Goal
|
|
|
|
Introduce the complete command syntax and validation while keeping execution
|
|
injected and testable.
|
|
|
|
### Work
|
|
|
|
1. Extend `internal/cli.Runner` with an injectable comparison function, default
|
|
it to `app.CompareDetailed`, and add a private comparison command parser and
|
|
request-construction path. Do not route it from the public root switch until
|
|
Stage 9 can provide the complete summary and exit contract.
|
|
2. Implement a repeatable flag value that preserves every `--profile` token in
|
|
exact command-line order.
|
|
3. Parse applicable common flags plus `--out-dir` and `--replace`; reject
|
|
missing/unknown reports, unsupported date use, fewer than two profiles,
|
|
whitespace-only profiles, exact duplicates, unexpected positional
|
|
arguments, and unsupported flags such as `--out`.
|
|
4. Reuse existing configuration loading, units/timezone overrides, report/date
|
|
resolution semantics, working-directory handling, and debug-root behavior.
|
|
5. Pass explicit output override and ordered profiles to the application. Do
|
|
not merge the configured default profile. Construct one Promptkit executor
|
|
only after syntax/config validation and before the application call.
|
|
6. Return the application result and error through a private CLI execution
|
|
seam for Stage 9 to format. Do not add public root routing or help text in
|
|
this stage, so users cannot reach a command with an incomplete output
|
|
contract.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Table-test report/date behavior, repeated profiles and ordering, invalid
|
|
lists, output precedence inputs, `--replace`, common overrides, and
|
|
rejected flags/arguments.
|
|
- Assert invalid syntax and configuration do not construct an executor or call
|
|
the application.
|
|
- Call the private comparison execution seam in tests and assert one valid
|
|
invocation constructs one executor and one request with the exact selections
|
|
and paths.
|
|
- Run CLI tests and repository-wide tests.
|
|
|
|
## Stage 9: Complete CLI Summaries, Quiet Mode, And Exit Behavior
|
|
|
|
### Goal
|
|
|
|
Finish the end-to-end user-facing command contract, including coherent partial
|
|
failure reporting.
|
|
|
|
### Work
|
|
|
|
1. Add comparison summary and ordered profile-summary types with the exact
|
|
locked JSON field order and `omitempty` behavior.
|
|
2. Add public root routing and root help text now that parsing, execution,
|
|
formatting, and exit behavior can land as one complete command.
|
|
3. Map complete, partial, cancelled, and publication-failed application results
|
|
into one normal stdout object whenever a structured result exists. Use
|
|
absolute returned paths and safe structured errors; do not serialize wrapped
|
|
internal or provider details.
|
|
4. Return success only for complete execution and publication. Return the
|
|
application error after writing a failed structured summary so the process
|
|
exits nonzero. Preserve the existing unstructured error path for failures
|
|
before a result exists.
|
|
5. Implement `--quiet` as output suppression only. It must not alter execution,
|
|
comparison artifacts, replacement, error return, or exit status.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Golden or exact-structure tests cover successful, partial, all-failed,
|
|
pre-execution, cancelled, and publication-failed summaries, field omission,
|
|
ordered results, and safe errors.
|
|
- CLI integration tests assert stdout/stderr discipline, quiet behavior,
|
|
nonzero partial failure, default and explicit output paths, and one executor
|
|
per invocation.
|
|
- Run CLI and application tests with `-race`, then repository-wide tests.
|
|
|
|
## Stage 10: Reconcile Canonical Documentation And Run The Final Gate
|
|
|
|
### Goal
|
|
|
|
Document only the implemented behavior in its canonical owners, remove
|
|
roadmap-only language from current-state documentation, and perform the full
|
|
repository validation gate.
|
|
|
|
### Work
|
|
|
|
1. Update `docs/cli.md` with authoritative syntax, flags, profile validation,
|
|
JSON summary, quiet behavior, and exit semantics.
|
|
2. Update `docs/operations.md` with the prompt-comparison workflow, output
|
|
destination precedence by link to `docs/config.md`, sensitivity guidance,
|
|
flat bundle lifecycle, guarded replacement, partial results, cancellation,
|
|
and cleanup expectations. Do not duplicate the complete CLI reference.
|
|
3. Add `docs/integrations/comparison-bundle.md` as the canonical durable schema
|
|
and layout contract. Document the exact version, JSON fields, filenames,
|
|
path relativity, hashes, success/failure invariants, compatibility boundary,
|
|
and sensitive-content caveat.
|
|
4. Update `docs/integrations/promptkit.md` with the consumer-visible
|
|
prepare-once/execute-many boundary and Promptkit-owned backend capacity,
|
|
linking upstream rather than duplicating its concurrency reference.
|
|
5. Add or update focused `docs/internal/` documents for application comparison
|
|
orchestration, prepared report flow, concurrent execution, prompt debugging,
|
|
and bundle publication mechanics. Link to canonical CLI, configuration,
|
|
operations, integration, and architecture owners instead of restating them.
|
|
6. Update `docs/policy/architecture.md` for the implemented stateless,
|
|
concurrency, notification, and publication invariants; update
|
|
`docs/development.md` for new package/file routing; update
|
|
`docs/policy/documentation.md` only if the existing ownership table no longer
|
|
assigns every introduced topic correctly.
|
|
7. Check README and other current-state documents for navigation or stale
|
|
claims. Add only short orientation and canonical links where useful.
|
|
8. Mark the feature roadmap `Implemented; retained temporarily for
|
|
post-implementation review` and this plan `Complete`. Retain both until the
|
|
requested implementation review moves durable facts to their owners and the
|
|
maintainer authorizes roadmap cleanup.
|
|
|
|
### Tests And Exit Criteria
|
|
|
|
- Verify all changed relative links and fenced examples, and search for stale
|
|
claims that comparison is unavailable or planned.
|
|
- Confirm current-state docs do not duplicate full command, configuration,
|
|
schema, or implementation contracts outside their canonical owners.
|
|
- Run the release-equivalent local gate:
|
|
|
|
```sh
|
|
set -eu
|
|
test -z "$(git ls-files go.work go.work.sum)"
|
|
test ! -e vendor
|
|
if grep -Eq '^[[:space:]]*replace([[:space:]]|\()' go.mod; then
|
|
printf '%s\n' 'go.mod contains a replacement' >&2
|
|
exit 1
|
|
fi
|
|
GOWORK=off go test -count=1 ./...
|
|
GOWORK=off go test -race -count=1 ./...
|
|
GOWORK=off go vet ./...
|
|
GOWORK=off go build ./...
|
|
GOWORK=off go mod tidy -diff
|
|
unformatted="$(git ls-files '*.go' | while IFS= read -r file; do gofmt -l "$file"; done)"
|
|
test -z "$unformatted"
|
|
git diff --check
|
|
```
|
|
|
|
- Run `GOWORK=off go run ./cmd/weatherreporter --help`; confirm output matches
|
|
`docs/cli.md` without requiring credentials or network access.
|
|
- Inspect the final diff for accidental generated artifacts, secrets,
|
|
workspaces, vendored dependencies, release notes, or unrelated changes.
|
|
|
|
## Open Questions
|
|
|
|
None. The accepted feature roadmap and the locked contracts in this plan define
|
|
the decisions required to implement all ten stages.
|