18 KiB
LLM Profile Comparison Implementation Plan
Status: Follow-up work planned after post-implementation review.
Purpose And Authority
This document is the ordered implementation plan for the accepted LLM Profile Comparison Roadmap. The roadmap owns the feature purpose, policy, scope, and desired end state. This plan records the completed implementation and defines the remaining corrective work discovered during post-implementation review.
Stages 1 through 10 are complete. Implement Stages 11 through 15 in numeric order, using one gpt-5.6-terra coding prompt per stage. Each stage is scoped to leave the repository compiling, tested, documented to its implemented boundary, and internally coherent.
Implementation Rules
Apply these rules in every remaining stage:
- Read
docs/development.md, the task-specific documents it identifies, all files underdocs/policy/, and the feature roadmap before changing code. - Preserve the existing
generate,run, andcomparecommand contracts except for the explicit comparison corrections defined below. - Keep Promptkit types and calls behind
internal/adapters/promptkitand the dependency-neutralinternal/promptexecinterface. - 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.
- Preserve comparison's prepare-once, execute-concurrently, order-results-by- selection, publish-on-profile-failure, and never-notify invariants.
- Do not add a Weatherreporter concurrency limit. Promptkit owns backend capacity.
- Never expose provider bodies, prompts, schemas, model output, endpoints, credentials, or arbitrary wrapped error text in normal JSON summaries or manifests.
- Use deterministic, offline, credential-free tests. Test filesystem safety, concurrency, and recovery through the narrowest stable behavioral boundary; do not rely on timing-only sleeps or host permission behavior.
- Run
gofmton changed Go files andgit diff --checkin every stage. Run focused tests while developing andGOWORK=off go test -count=1 ./...before completing each stage. Stages involving concurrency or filesystem mutation must also run affected packages with-race. - Do not commit, tag, push, or prepare a release unless the implementing prompt separately requests it.
Completed Stages
Stage 1: Comparison Artifact And Naming Contracts
Added the dependency-neutral comparison model, schema version, manifest validation and encoding, safe errors, deterministic profile filenames, comparison identities, default directory names, and content hashing.
Stage 2: Destination Recognition And Transactional Publication
Added read-only destination planning, strict recognition of current comparison bundles, private sibling staging, guarded replacement, rollback, and atomic directory publication.
Stage 3: Ordered Multi-Profile Preflight
Added exact prompt inspection followed by sequential profile inspection before weather collection, including effective backend, model, and credential checks.
Stage 4: Immutable Shared Report Preparation
Extracted one immutable prepared-report value so comparison collection, derivation, module construction, and data-package serialization happen once.
Stage 5: Profile Execution And In-Memory Rendering
Separated profile-specific Promptkit execution, generated-text validation, and Markdown rendering from output publication while preserving ordinary report generation behavior.
Stage 6: Concurrent Ordered Profile Execution
Added one goroutine per selected profile using one shared executor and one prepared input, deterministic debug identities, isolated profile failures, joined cancellation, and selection-ordered results.
Stage 7: Application-Level Comparison
Added app.CompareDetailed, coherent complete and partial bundle construction,
aggregate profile-failure behavior, absolute published paths, and the
application-level guarantee that comparison never notifies Distributor.
Stage 8: Compare Command Parsing
Added the compare command request path, repeatable ordered --profile, exact
--out-dir, guarded --replace, applicable common flags, validation, and one
executor construction per invocation.
Stage 9: CLI Results And Exit Behavior
Added structured success and failure summaries, quiet-mode suppression, ordered per-profile results, safe bounded errors, and nonzero exit behavior for partial or command-level failure.
Stage 10: Canonical Documentation And Initial Validation
Documented the implemented CLI, operations, Promptkit integration, comparison bundle, application orchestration, execution, publication, architecture, and development contracts, then passed the original repository-wide validation gate.
Stage 11: Make Concurrent Prompt Debug Creation Race-Safe
Goal
Ensure concurrent comparison profiles can create their distinct debug runs under one new report/date directory without spuriously failing or leaving a test goroutine blocked.
Work
- Update
internal/promptdebug.ensureSecureDirectoryso concurrent creation of the same missing directory is idempotent. Ifos.Mkdirreports that the path already exists, inspect the path withLstatand accept it only when it is the expected real directory. Continue to reject symlinks, non-directories, unsafe modes, and every unrelated filesystem error. - Preserve the existing absolute-path, containment,
0700directory,0600file, and no-symlink guarantees. Do not weaken debug-root validation or make allEEXISTerrors successful. - Add a focused prompt-debug concurrency regression that starts multiple writers beneath a shared missing ancestor, joins every goroutine, and verifies every expected artifact and permission invariant.
- Make comparison execution test barriers time-bounded and failure-aware. A
callback failure before executor entry must fail the test promptly rather
than leave
waitForProfileStartswaiting forever. - Retain distinct deterministic debug references and profile-local debug failure behavior.
Tests And Exit Criteria
-
The focused prompt-debug concurrency test passes repeatedly and with the race detector.
-
TestExecuteComparisonProfilesUsesDistinctDeterministicDebugReferencescannot hang when a profile fails before reaching the fake executor. -
Run, at minimum:
GOWORK=off go test -count=100 ./internal/promptdebug GOWORK=off go test -count=100 -run TestExecuteComparisonProfilesUsesDistinctDeterministicDebugReferences ./internal/app GOWORK=off go test -race -count=1 ./internal/promptdebug ./internal/app GOWORK=off go test -count=1 ./...
Stage 12: Make Replacement Authorization Commit-Safe
Goal
Prevent a destination changed after the final read-only preflight from being treated as the previously authorized empty directory or recognized bundle and then deleted during replacement.
Work
-
Replace
DestinationPlan.Existsas the publication decision with an explicit destination-state classification: absent, empty real directory, or recognized current-schema bundle. KeepReplacein the plan so publication can apply the same authorization policy at commit time. -
Continue to call
PlanDestinationimmediately before publication. For an absent target, install staging with one rename; a concurrently created target must cause that rename to fail without modifying the new target. -
For an existing target, rename that exact filesystem entry to the unique sibling backup before deleting or installing anything. Classify the moved backup while it is at its stable backup path and authorize it under the original replacement policy:
- an empty real directory is allowed with or without
Replace; - a recognized current-schema comparison bundle is allowed only with
Replace; and - a file, symlink, unrecognized/nonempty directory, unreadable entry, or other classification failure is not allowed.
- an empty real directory is allowed with or without
-
Treat this post-move classification as the destructive-action authorization point. If it fails, restore the moved entry to the target and return an error without installing staging. If the target has concurrently reappeared or restoration otherwise fails, retain the backup and return an actionable joined or typed error that identifies its recovery path; never delete either entry to force restoration.
-
Install staging only after the moved target has passed authorization. Never remove a backup that did not pass post-move authorization.
-
Preserve the existing final cancellation linearization rule: cancellation observed before the rename transaction prevents replacement; after the transaction starts, finish commit or rollback rather than abandoning it.
-
Add a package-private filesystem-operation seam only if needed for deterministic tests. Keep the public destination and publication APIs free of test-only hooks.
Tests And Exit Criteria
- Deterministically replace an initially accepted destination after final preflight but before its move with each consequential unauthorized type: unrelated nonempty directory, regular file, and symlink. Publication must fail, staging must not become the target, and the moved entry must be restored or retained at a reported recovery path.
- Cover an initially empty directory whose contents change before its move and a recognized bundle swapped for an unrecognized directory.
- Retain coverage for absent targets, empty directories, recognized bundle replacement, cancellation before commit, install failure, successful rollback, failed rollback, and cleanup of ordinary staging failures.
- Run
GOWORK=off go test -race -count=1 ./internal/comparisonand the repository-wide standard test command.
Stage 13: Represent Committed Publication Cleanup Failures Accurately
Goal
Keep application and CLI results truthful when the new comparison bundle has been committed but removal of the old sibling backup fails.
Work
-
Change comparison publication to return a dependency-neutral result as well as an error:
type PublicationResult struct { Committed bool RetainedBackupPath string } func Publish( ctx context.Context, plan DestinationPlan, bundle LogicalBundle, ) (PublicationResult, error) -
Define
Committedas meaning the complete staged bundle is now installed at the target. Pre-commit, staging, authorization, install, and successful- rollback failures returnCommitted == false. A successful install returnsCommitted == trueeven if later backup cleanup fails. -
Add a typed post-commit cleanup error that unwraps its filesystem cause and records the retained backup path for operator recovery. On this error, return
Committed == trueand the absolute retained backup path. Do not roll back or remove the newly committed valid bundle merely because old backup cleanup failed. -
In
app.CompareDetailed, populateManifestPath,DataPackagePath, and successful profileReportPathvalues whenever publication reportsCommitted == true, before returning any cleanup error. -
Treat post-commit cleanup failure as a command-level operational failure: return the non-nil structured result plus an error, produce status
failed, and exit nonzero even though the published artifact paths are present. The ordinary safe JSON error must not contain the raw filesystem cause or backup path; the wrapped diagnostic returned on stderr may identify the retained backup for recovery. -
Keep
RetainedBackupPathout of the versioned comparison manifest. It describes an incomplete local transaction cleanup, not the logical bundle.
Tests And Exit Criteria
- Inject a deterministic backup-removal failure after successful installation
and assert the target is the new recognized bundle, the old bundle remains
at the reported backup,
Committedis true, and the error is inspectable by type. - At the application boundary, assert all committed artifact paths are absolute and populated while the method still returns an error.
- At the CLI boundary, assert status
failed, nonzero return, present artifact paths, and a bounded generic safe error with no raw filesystem detail. - Retain tests showing every pre-commit or rolled-back failure omits published artifact paths.
- Run comparison, application, and CLI tests with
-race, then the repository-wide standard test command.
Stage 14: Complete Structured Failure Metadata And Classification
Goal
Make every non-nil comparison result a reliable description of the attempted run and preserve useful safe error categories in the top-level CLI summary.
Work
-
In
app.CompareDetailed, assign the absolute resolvedOutputDirectoryimmediately after output-directory resolution and before destination preflight. Do not wait forPlanDestinationto succeed. -
Once the initial
ComparisonResultexists, guarantee that every return path sets a nonzero UTCFinishedAtthat is not beforeStartedAt. Use one centralized finalization path or a defer; do not scatter timestamp writes across individual failures. -
Continue to omit manifest, data-package, and report paths until publication commits. Preserve whatever prompt identity fields have actually been resolved; never invent a hash or profile result for a phase that did not run.
-
Update
safeComparisonSummaryErrorto use this stable mapping, always passing messages throughcomparison.NewSafeError:Error Category Safe message aggregate profile failure applicationexisting bounded aggregate message context.Canceledcanceledcomparison canceledcontext.DeadlineExceededdeadline_exceededcomparison deadline exceededcategorized promptexecerrorexact promptexec.CategoryOfvaluecomparison prompt operation failedcomparison.DestinationErrordestination_<kind>comparison destination preflight failedpost-commit cleanup error publication_cleanupcomparison published but cleanup did not completeany unknown error applicationcomparison did not complete -
Apply the most specific mapping before a more general wrapped match. In particular, detect the post-commit cleanup and destination types before falling back to a nested filesystem or context cause.
-
Do not copy
DestinationError.Target, wrapped causes, or arbitraryerror.Error()text into normal JSON. Detailed returned errors remain available on stderr and through Go error inspection.
Tests And Exit Criteria
- Add application tests for destination-preflight, debug initialization, prompt-preflight, collection, and preparation failures. Whenever a non-nil result is returned, assert an absolute output directory, nonzero ordered UTC timestamps, and omission of unpublished artifact paths.
- Add table-driven CLI tests for every mapping row, including wrapped errors, and assert that unsafe sentinel text cannot enter serialized output.
- Preserve existing ordered profile-level categories and safe messages.
- Run application and CLI tests with
-race, then the repository-wide standard test command.
Stage 15: Remove Temporary Seams, Reconcile Documentation, And Validate
Goal
Remove review-discovered maintenance debt, document the corrected implemented behavior in its canonical owners, and complete the release-equivalent gate.
Work
-
Remove the unused
Runner.resolveComparisonwrapper. Remove the test-orientedRunner.executeComparisonseam if it has no production caller, and rewrite its remaining coverage throughRunner.Run,resolveComparisonAction, or another stable behavioral boundary. -
Remove
comparisonProfileOutcome.errif production code still does not use it. Keep raw failures in returned/wrapped errors or explicit internal error types; do not retain an otherwise dead field solely for private test assertions. -
Correct the
internal/comparisonpackage comment so it describes the package's actual ownership of both logical comparison contracts and filesystem destination/publication behavior. -
Update only the canonical current-state documents affected by Stages 11 through 14:
docs/internal/comparison-publication.mdowns post-move authorization, commit state, rollback, retained backups, and cleanup mechanics;docs/internal/comparison-execution.mdowns concurrency and debug-write behavior;docs/internal/app-orchestration.mdowns partial results and committed publication error handling;docs/cli.mdowns structured status, safe category, path, and exit behavior; anddocs/operations.mdowns operator recovery for a retained sibling backup.
Link rather than duplicating complete contracts, and update architecture or integration documentation only if its existing invariant is inaccurate.
-
Mark this plan
Completeand restore the feature roadmap's implemented status after every exit criterion below passes. Retain or remove the two roadmap documents only according to a later maintainer-directed roadmap cleanup; do not archive them as a second current-state reference in this stage.
Tests And Exit Criteria
-
Confirm no production-only helper or field remains solely to support tests, and no test loses meaningful behavioral coverage during cleanup.
-
Verify changed relative links and fenced examples. Search current-state docs for stale claims about comparison publication, debug behavior, results, or recovery.
-
Run the release-equivalent local gate:
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 --helpandGOWORK=off go run ./cmd/weatherreporter compare --helpwithout credentials or network access, and confirm that help agrees withdocs/cli.md. -
Inspect the final diff for accidental generated artifacts, secrets, workspaces, vendored dependencies, release notes, or unrelated changes.
Open Questions
None. The roadmap and the contracts in Stages 11 through 15 define the remaining decisions needed to complete the corrective work.