Files
weatherreporter/docs/roadmap/audit.md

92 KiB

Repository Audit Ledger

Status: In progress; Stages 1-6 complete.

This temporary roadmap document is the evidence ledger for the staged audit defined by the audit plan. It records audit evidence and status, not implemented product behavior. Current contracts remain with the canonical owners identified by the documentation policy.

Executive Summary

Stage 1 established a clean, reproducible baseline. The repository-wide test suite, CLI help check, formatting check, and vet check all pass. Stage 2 found that the implemented package graph and principal workflows follow the intended dependency direction and assigned ownership. It recorded one low-severity candidate finding for unused internal persistence helpers left outside the documented stateless workflows. Stage 3 found one medium-severity correctness defect in wall-clock window construction on daylight-saving transition days and one medium-severity test gap around Daily run-ID uniqueness. Report registry identities, command and configuration lookup policies, batch membership, civil-day periods, and rolling-hour periods otherwise match their documented contracts. Stage 4 found three medium-severity validation defects: unsupported missing-source keys are silently accepted, two single-report notification identities may render blank, and programmatically constructed report overrides are silently ignored. It also found one low-severity secret loading atomicity defect. Defaults, file and CLI precedence, strict YAML field handling, active Promptkit settings, output-path preservation, and maintained examples otherwise match their contracts. Stage 5 found two medium-severity CLI correctness defects: operating-system interrupts never become workflow cancellation, and generate validates report dates only after constructing its executor. It also found a medium-severity test gap around assembled generate and ordinary batch result/exit behavior. Command ownership, flag acceptance, output routing, quiet mode, comparison error bounding, and the implemented result-to-exit mapping otherwise follow the documented contract. Stage 6 found four medium-severity defects at the Weather API boundary: unsupported URL schemes pass validation until transport, non-2xx response bodies flow verbatim into normal diagnostics, the nominal response limit silently truncates instead of rejecting oversized bodies, and required hourly products accept periods without usable time bounds. It also found one low-severity retry-policy defect because warmup retries permanent HTTP failures. Source availability policy, checked-empty products, query construction, cancellation, provenance, normalization ownership, and focused offline fixtures otherwise match their contracts. Subsystem conclusions and final disposition remain pending the later stages.

Baseline Metadata

Item Recorded baseline
Audit date 2026-08-12 (UTC)
Commit e7c7262404ba0e8e74009ed840d38f7a0142b347 (Add audit workflow plan)
Expected commit from audit plan 151c536cb91ebedb6039270b752bae219dfe33d0 (Add comparison diagnostics to the future roadmap)
Baseline difference The audit uses the current main, one commit ahead of the expected commit. The intervening commit adds the audit workflow plan used to conduct this audit.
Branch main
Initial worktree Clean; git status --porcelain=v1 produced no entries before the audit ledger was created.
Go toolchain go version go1.26.5 linux/amd64; go env GOVERSION reported go1.26.5.
Module gitea.maximumdirect.net/eric/weatherreporter; module file /home/eric/Workspace/weatherreporter/go.mod; go 1.26.
Workspace context Module mode with no go.work; go env GOWORK was empty and the repository contains only ./go.mod.
Graph project home-eric-Workspace-weatherreporter
Graph refresh Refreshed from the baseline checkout in moderate mode without a persisted artifact: 2,674 nodes and 11,572 edges. The indexer excluded docs, examples, embedded-asset directories, and test-data directories, so their inventories below come from tracked files rather than the graph.
Baseline exclusions None. There were no pre-existing worktree changes.
Environmental limitations None encountered. Validation was deterministic and offline.

Scope And Inventory

Stage 1 inventories the checked-out repository without judging subsystem correctness or test sufficiency. The repository has 219 tracked files. Its Go inventory contains 24 packages, 83 non-test source files, and 50 test files.

Go Packages And Files

Package directory Production .go files _test.go files
cmd/weatherreporter 1 0
internal/adapters/distributor 1 1
internal/adapters/promptkit 1 1
internal/adapters/weatherapi 1 1
internal/app 10 11
internal/briefing 21 6
internal/buildinfo 1 0
internal/cli 4 5
internal/collect 1 1
internal/comparison 2 2
internal/config 7 2
internal/facts 1 1
internal/fileutil 1 1
internal/forecast 3 1
internal/generatedtext 8 7
internal/module 1 1
internal/promptassets 1 1
internal/promptdebug 1 1
internal/promptexec 2 1
internal/promptinput 1 1
internal/report 8 1
internal/reporttemplate 2 1
internal/timeutil 3 2
internal/weatherdata 1 1
Total 83 50

Fixtures

Nine tracked fixtures are present:

  • internal/adapters/weatherapi/testdata/alerts.json
  • internal/adapters/weatherapi/testdata/convective_outlooks.json
  • internal/adapters/weatherapi/testdata/current.json
  • internal/adapters/weatherapi/testdata/discussion.json
  • internal/adapters/weatherapi/testdata/hourly.json
  • internal/adapters/weatherapi/testdata/narrative.json
  • internal/adapters/weatherapi/testdata/observations.json
  • internal/adapters/weatherapi/testdata/weather_story.json
  • internal/forecast/testdata/daily_bundle.json

Embedded Assets

The three //go:embed declarations cover 26 tracked assets:

  • one SPC definition file under internal/briefing/assets/;
  • three Promptkit profiles, ten prompt files, and four generated-text schemas under internal/promptassets/assets/; and
  • four report templates and four template partials under internal/reporttemplate/templates/.

The exact embedding owners are internal/briefing/spc_convective_outlook_definitions.go, internal/promptassets/promptassets.go, and internal/reporttemplate/reporttemplate.go.

Canonical Documents And Maintained Examples

There are 38 tracked, non-roadmap canonical documents and three maintained example files. The canonical inventory is:

  • product and maintainer references: README.md, docs/cli.md, docs/config.md, docs/development.md, docs/operations.md, docs/release.md, and docs/templates.md;
  • policies: docs/policy/architecture.md, docs/policy/documentation.md, and docs/policy/testing.md;
  • architecture decisions: docs/adr/0001-stateless-execution.md;
  • integration contracts: docs/integrations/comparison-bundle.md, docs/integrations/promptkit.md, docs/integrations/weatherapi.md, and the three documents under docs/integrations/distributor/;
  • internal documents: all 17 tracked documents under docs/internal/; and
  • release notes: the four tracked documents under docs/releases/.

The maintained examples are examples/config.yml, examples/minimal-config.yml, and examples/weather-light-local-profile.yml. Roadmaps are coordination records, not canonical current-state documents; the four pre-existing roadmap files are therefore outside the canonical count.

Baseline Validation

Command Result Evidence
go test ./... Pass Exit 0. All 24 packages were evaluated; 22 package test suites passed and cmd/weatherreporter plus internal/buildinfo reported no test files.
go run ./cmd/weatherreporter --help Pass Exit 0. Help printed usage for help, version, four report generators, two batch commands, and comparison.
git diff --check Pass Exit 0 with no output on the clean pre-ledger baseline.
go vet ./... Pass Exit 0 with no diagnostics.

Supplementary baseline commands were git rev-parse HEAD, git rev-list --left-right --count 151c536...HEAD, git status --porcelain=v1, git branch --show-current, go version, go env GOVERSION GOMOD GOWORK GO111MODULE, go list ./..., tracked-file inventory commands, graph index refresh, and graph architecture inspection.

Stage Coverage

Stage Scope Status
1 Establish the baseline and audit ledger Complete
2 Audit architecture and dependency direction Complete
3 Audit report identity and time foundations Complete
4 Audit configuration, secrets, and validation Complete
5 Audit CLI parsing, wiring, and output contracts Complete
6 Audit weather data acquisition and collection Complete
7 Audit forecast and fact derivation Pending
8 Audit module contracts, registry, and source-facing briefing modules Pending
9 Audit derived, planning, formatting, and SPC briefing modules Pending
10 Audit prompt inputs, assets, and neutral execution contracts Pending
11 Audit Promptkit adaptation and secure prompt debugging Pending
12 Audit generated-text validation and catalog contracts Pending
13 Audit render contexts, templates, and Markdown rendering Pending
14 Audit application preparation and prompt preflight Pending
15 Audit single-report generation and atomic output Pending
16 Audit batch orchestration and Distributor notification Pending
17 Audit comparison contracts and transactional publication Pending
18 Audit comparison execution and CLI integration Pending
19 Audit test hermeticity and execution hygiene Pending
20 Audit test risk coverage and ownership Pending
21 Audit test durability, duplication, and maintenance cost Pending
22 Audit cross-cutting efficiency and complexity Pending
23 Audit cross-cutting refactoring and deduplication opportunities Pending
24 Audit documentation coherence and executable contracts Pending
25 Run dynamic robustness and final diagnostic validation Pending
26 Verify, consolidate, and triage findings Pending
27 Produce the remediation roadmap and close the audit Pending

Risk-To-Test Coverage

Stage 1 records where tests exist but does not infer sufficiency from package counts or a passing suite. Assigned stages will replace these pending entries with evidence about meaningful risks, test ownership, gaps, and duplication.

Risk area Current test owner or evidence source Audit stage Coverage assessment
Architecture and dependency direction Assembled app and CLI tests; graph traces 2 Sufficient at the architectural boundary: direct imports are acyclic, external dependency types remain adapter-local, and representative workflow ordering and publication boundaries have focused tests. AUD-001 records unused persistence APIs that do not participate in normal execution.
Report identity, periods, dates, and timezones internal/report, internal/timeutil, plus focused internal/app and internal/forecast callers 3 Partly insufficient. Registry identities, alias policies, batch membership, civil-day boundaries, and elapsed six-hour periods are coherent and exercised. AUD-002 records incorrect wall-clock windows on daylight-saving transition days, and AUD-003 records the missing regression test for Daily run-ID uniqueness across dates.
Configuration, validation, and secrets internal/config, focused internal/cli and Promptkit adapter mapping, maintained examples 4 Partly insufficient. Defaults, precedence, known-field parsing, active URL/duration/template checks, output-path preservation, profile-source rules, secret redaction, and examples are exercised. AUD-004 through AUD-007 record unsupported missing-source keys, blank notification identities, constructed-report override inconsistency, and non-atomic secret environment mutation.
CLI parsing, output, and exit behavior cmd/weatherreporter, internal/cli, representative app-facing CLI tests 5 Partly insufficient. Command/flag ownership, paths, one-executor mapping, stream separation, quiet mode, comparison safe errors, and visible result/exit rules are coherent. AUD-008 records missing signal cancellation, AUD-009 records late generate date validation, and AUD-010 records missing assembled non-comparison CLI protection.
Weather transport and normalized collection internal/adapters/weatherapi, internal/collect, internal/weatherdata 6 Partly insufficient. All eight source requests, required/optional policy, checked-empty products, retryable source statuses, cancellation, normalized provenance, and collection error ownership have focused offline coverage. AUD-011 through AUD-015 record unsupported schemes, unsafe response-body diagnostics, an unenforced body-size limit, structurally invalid required hourly periods, and overbroad warmup retries.
Forecast and fact derivation internal/forecast, internal/facts 7 Pending
Module and briefing contracts internal/module, internal/briefing 8-9 Pending
Prompt inputs, embedded assets, and execution contracts internal/promptinput, internal/promptassets, internal/promptexec 10 Pending
Promptkit boundary and sensitive debug output internal/adapters/promptkit, internal/promptdebug 11 Pending
Generated-text validation internal/generatedtext 12 Pending
Render contexts and templates internal/generatedtext, internal/reporttemplate 13 Pending
Prompt preflight and prepared inputs internal/app 14 Pending
Single-report publication and preservation internal/app, internal/fileutil 15 Pending
Batch partial success and notification internal/app, internal/adapters/distributor 16 Pending
Comparison identity and transactional publication internal/comparison 17 Pending
Comparison concurrency and CLI behavior internal/app, internal/cli 18 Pending
Hermeticity, execution hygiene, portfolio coverage, and durability Repository-wide suite 19-21 Pending
Cross-cutting efficiency and maintainability Graph metrics plus focused tests 22-23 Pending
Documentation and executable-contract coherence Canonical documents, code, schemas, templates, examples 24 Pending
Dynamic robustness and diagnostic checks Repository-wide deterministic checks 25 Pending

Findings

AUD-001: Unused persistence helpers remain after the stateless redesign

  • Stage: 2
  • Status: candidate
  • Severity: low
  • Confidence: high
  • Category: architecture
  • Area: internal/promptinput.Save, internal/adapters/weatherapi.SaveBundle, and internal/fileutil.WriteJSONAtomic
  • Evidence: Graph inbound traces show promptinput.Save has no callers, weatherapi.SaveBundle is called only by TestSaveBundle, and fileutil.WriteJSONAtomic is called only by SaveBundle and its focused test. Repository text search found no documentation or other call sites. Representative generate, batch, comparison, and collection traces do not reach any of these helpers. promptinput.Save and SaveBundle write intermediate prompt input or normalized weather data to arbitrary paths, while normal publication uses fileutil.WriteFileAtomic for selected Markdown and comparison.Publish for selected bundles.
  • Contract at risk: The architecture policy and ADR 0001 define normal execution as an in-memory stateless transformation whose durable files are operator-selected report outputs, comparison bundles, or explicitly requested secure prompt debugging.
  • Impact: These unreachable exported functions do not create state during current workflows, but they retain an unsupported persistence surface and low-value tests that can invite accidental reintroduction of intermediate artifacts or require maintenance despite having no product caller.
  • Recommendation: Remove the two unused domain/adapter save functions, remove WriteJSONAtomic if it then has no production use, and delete or consolidate tests that protect only those retired APIs.
  • Test implications: TestSaveBundle and TestWriteJSONAtomic protect unused persistence mechanisms; promptinput.Save has no focused test. Preserve tests for WriteFileAtomic, normal report publication, comparison publication, and explicit prompt-debug writes.
  • Validation: Graph and text searches show no remaining production references to the retired helpers; go test ./... passes; normal output, comparison, and debug-publication tests remain unchanged and pass.
  • Related findings: none
  • Remediation reference: pending

AUD-002: Clock windows shift on daylight-saving transition days

  • Stage: 3
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/timeutil.ClockWindow and internal/forecast.ResolveDayparts
  • Evidence: ClockWindow obtains the local civil-day start and creates its boundaries with elapsed-duration additions such as day.Start.Add(17 * time.Hour). In America/Chicago, adding 17 elapsed hours to local midnight produces 18:00 on the 2026-03-08 spring transition and 16:00 on the 2026-11-01 fall transition; adding six hours produces 07:00 and 05:00 respectively. Its only production caller is forecast.ResolveDayparts, which uses those boundaries for daily summaries and facts. The focused time test covers only an overnight window in a fixed offset zone, so both transition cases pass unnoticed.
  • Contract at risk: Configured daypart clocks are local wall-clock boundaries. Civil-day and timezone policy requires those clocks to retain their stated local hour across 23-hour and 25-hour days.
  • Impact: On daylight-saving transition dates, observations can enter or leave a configured daypart an hour early or late, shifting derived daily summaries and any downstream facts or report content that consume them.
  • Recommendation: Construct each boundary as a civil date plus local clock in the target location, including explicit handling for 24:00 and overnight windows, rather than adding elapsed hours to midnight.
  • Test implications: Add table-driven America/Chicago coverage for both spring-forward and fall-back dates, asserting the requested wall-clock start and end and the intended overnight date rollover. Retain the existing half-open period assertions.
  • Validation: The new regression test fails against the elapsed-addition implementation and passes when transition-day boundaries retain their configured local clocks; focused timeutil and forecast suites pass.
  • Related findings: none
  • Remediation reference: pending

AUD-003: Daily run-ID uniqueness lacks a focused contract test

  • Stage: 3
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: testing
  • Area: internal/report.Resolved.runID, validStartDateRunIDDisambiguator, and focused report tests
  • Evidence: Daily run IDs append the resolved valid-start date to the shared generation timestamp and report ID. This distinguishes multiple Daily reports planned for different dates in one batch, where GeneratedAt is the same. Focused report tests assert that registry identities are nonempty and app tests use nonempty or fixture run IDs, but no test resolves two Daily dates at one clock instant and asserts distinct IDs. Downstream plannedReportsByRunID rejects duplicate IDs before building a batch notification, and Distributor idempotency also consumes the run ID.
  • Contract at risk: A run ID must identify one resolved report product, while one batch may legitimately contain several Daily products for different valid dates.
  • Impact: An accidental removal or weakening of the date disambiguator can give distinct Daily products the same identity, causing batch notification construction to fail and creating Distributor idempotency collisions even though the existing focused suite remains green.
  • Recommendation: Add a focused report test that resolves two Daily dates with the same injected Now, asserts distinct nonempty run IDs, and verifies that each ID carries the corresponding valid-start date. Prefer this relational contract over freezing unrelated timestamp formatting.
  • Test implications: The regression must fail if the Daily disambiguator is removed while allowing harmless changes to the common run-ID timestamp representation. An app-level assertion may additionally protect the multi-Daily batch consumer, but should not duplicate the report invariant.
  • Validation: Removing the date disambiguator makes the new focused test fail; with the invariant intact, report and multi-Daily batch tests pass and plannedReportsByRunID receives unique keys.
  • Related findings: none
  • Remediation reference: pending

AUD-004: Unsupported missing-source keys are silently accepted

  • Stage: 4
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/config.Validate, MissingSourceConfig.Sources, and internal/adapters/weatherapi.policyFor
  • Evidence: The configuration contract lists seven optional source keys and states that hourly forecast data is always required. Validate checks only that each map key is nonblank and its policy is one of error, warn, or none; it never checks the key against the supported set. The Weather API adapter looks up policies by its own literal source names and otherwise uses the default. Its required-hourly path returns an error before consulting the policy. Consequently a typo such as alert: error, an arbitrary key, or an ineffective hourly: none loads successfully. Focused config tests cover an invalid policy value but no unsupported key.
  • Contract at risk: missing_source.sources accepts only the documented optional source identities, and invalid configuration must fail loading rather than silently changing the effective failure policy.
  • Impact: An operator can believe a source-specific error, warn, or none rule is active while runtime collection ignores it and applies the default; the mistake is visible only when that source is missing or malformed.
  • Recommendation: Give internal/config one authoritative supported-source set and reject every other key, including hourly. Keep adapter source names aligned with that owner without introducing a second independently maintained list.
  • Test implications: Add loaded and constructed configuration cases for every supported key plus representative typo, unknown, blank, and required-hourly keys. Assert behavior through Validate or LoadFile, not private map iteration.
  • Validation: The new unsupported-key cases fail against the current validator and pass when validation rejects keys that cannot affect optional-source policy; focused config and Weather API policy tests remain green.
  • Related findings: none
  • Remediation reference: pending

AUD-005: Single-report notification identities may render blank

  • Stage: 4
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: validateDistributorNotify, RenderDistributorBundleID, and RenderDistributorIdempotencyKey
  • Evidence: Enabled single-report notification validation rejects an empty template and unknown variables, and RenderDistributorPipelineID rejects a whitespace-only result. In contrast, the bundle-ID and idempotency-key renderers return whitespace unchanged and validation does not perform a nonblank result check. Batch renderers consistently reject whitespace-only pipeline, bundle, and idempotency identities. The Distributor adapter checks only equality with the empty string, so whitespace values also pass its local request checks. Focused tests cover the single pipeline case and all three batch cases, but omit the two single-report cases.
  • Contract at risk: Enabled notification requires usable pipeline, bundle, and idempotency identities; configuration validation must reject invalid active templates before report execution.
  • Impact: A whitespace bundle ID or idempotency key can reach Distributor after report publication, risking rejected uploads or collisions between otherwise distinct report deliveries while configuration preflight reports success.
  • Recommendation: Apply the same trimmed-nonempty invariant to all three single-report identity renderers and exercise those renderers during active configuration validation.
  • Test implications: Extend the existing single-report validation table with whitespace-rendered bundle and idempotency identities, and retain the batch table as the parallel contract owner.
  • Validation: Both new cases fail against the current single-report renderers and pass when all active rendered identities are nonblank; focused config, app notification, and Distributor adapter tests pass.
  • Related findings: none
  • Remediation reference: pending

AUD-006: Constructed report overrides depend on inaccessible presence flags

  • Stage: 4
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: ReportConfig, ReportDistributorConfig, Config.ReportModuleOverrides, and Config.ReportDistributorPathOverrides
  • Evidence: YAML unmarshalling sets private deterministicModulesSet and pathTemplatesSet flags to distinguish omission from an explicit list. Both public override accessors skip exported DeterministicModules and PathTemplates values unless the corresponding private flag is true. Code outside package config therefore cannot construct an equivalent override: setting a nonempty exported slice in Config.Reports is silently ignored, and Validate skips its contents as well. Tests named for loaded/constructed consistency reside inside package config and manually set the private flags, so they do not exercise the configuration shape available to app callers. Graph traces show the accessors feed app report resolution and single/batch Distributor path rendering.
  • Contract at risk: Loaded and programmatically constructed configurations with the same exported values should validate and produce the same report module and path overrides.
  • Impact: Repository callers and tests that construct a config.Config can silently run default modules or Distributor paths instead of their requested values, while invalid constructed overrides can pass validation unused.
  • Recommendation: Represent presence through an API available to downstream callers, such as constructors/setters or an optional-list representation that preserves omitted versus explicitly empty YAML without private state that exported values cannot establish.
  • Test implications: Move or add behavior tests from an external config test package or an app boundary so they can use only exported construction APIs; assert both nonempty overrides and explicit-empty rejection.
  • Validation: The external-boundary test fails with direct exported-field construction today and passes when loaded and constructed forms yield the same overrides and validation result.
  • Related findings: none
  • Remediation reference: pending

AUD-007: Failed configuration loads can leave secrets applied

  • Stage: 4
  • Status: candidate
  • Severity: low
  • Confidence: high
  • Category: security
  • Area: internal/config.Load and loadSecrets
  • Evidence: Load calls loadSecrets before Validate. The secret loader validates and applies directory entries one at a time with os.Setenv, with no preflight of all entries and no restoration of prior environment values. A valid secret is therefore installed even when later configuration validation fails, and an earlier secret remains installed when a later invalid entry, unreadable value, or failed environment assignment aborts the directory load. Existing tests cover successful replacement, line-ending trimming, rejection diagnostics, and omission of secret values from errors, but do not assert failure atomicity.
  • Contract at risk: A rejected configuration should not partially apply its secret-bearing environment interaction, and repeatable in-process loads should not inherit state from an unsuccessful attempt.
  • Impact: The normal CLI process exits after a load failure, limiting the operational effect, but embedded runners, repeated actions, and tests can observe or consume credentials from a configuration that was never accepted.
  • Recommendation: Validate non-secret configuration before secret application, preflight and read the complete secret directory before mutation, and either apply environment changes atomically with rollback or return a scoped lookup rather than mutating process-global state incrementally.
  • Test implications: Add cases for a valid secret followed by an invalid entry and for valid secrets paired with a later-invalid configuration, asserting that prior set/unset environment state is restored.
  • Validation: Both failure cases leave the environment exactly as it was before loading; successful overwrite and single-line-ending behavior remain intact.
  • Related findings: none
  • Remediation reference: pending

AUD-008: Process interrupts do not cancel active workflows

  • Stage: 5
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: cmd/weatherreporter.main and CLI cancellation propagation
  • Evidence: main calls cli.Run with context.Background(). Repository code search found no signal.Notify, signal.NotifyContext, os.Interrupt, or termination-signal handling. Runner.Run correctly forwards the supplied context to generate, batch, and comparison workflows, and focused app tests prove that context cancellation prevents pending publication and joins comparison execution. The production executable never supplies a context that an operator interrupt can cancel, so those paths are reachable only by programmatic callers and tests.
  • Contract at risk: Documented cancellation failures must reach workflow cancellation checks so pre-publication destinations are preserved and a bounded failed result can be finalized when available.
  • Impact: SIGINT or SIGTERM terminates the process abruptly instead of canceling in-flight collection, Promptkit work, publication preflight, or notification. The CLI cannot emit its documented canceled result path or allow workflow cleanup/coordination to finish, increasing interruption risk around output and comparison transactions.
  • Recommendation: Derive the root context from signal.NotifyContext for operator interrupt and termination signals, stop signal delivery on return, and pass that context through the existing CLI boundary. Preserve the current nonzero failure exit and bounded error behavior.
  • Test implications: Add a narrow executable-boundary test or a testable signal context seam proving that an interrupt cancels the context passed to CLI. Keep detailed destination-preservation and goroutine-join assertions with their existing app/publication owners.
  • Validation: An interrupt observed during a controlled blocking action cancels its workflow context, returns the expected failed/canceled result when available, and leaves the existing destination intact; ordinary help, version, and successful exits are unchanged.
  • Related findings: AUD-010
  • Remediation reference: pending

AUD-009: Generate validates report dates after constructing its executor

  • Stage: 5
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/cli.Runner.resolveGenerateAction
  • Evidence: Generate validates the command name and syntactic flags, then loads configuration and constructs the Promptkit executor before checking that Daily has --date or parsing a supplied Daily/Today date. Executor construction can open and parse configured profile files/directories and can fail first. The equivalent comparison resolver validates profile selection before configuration and performs report-date checks before constructing its one executor. Focused comparison tests assert that input failures construct no executor, but generate has no corresponding test. The production generate daily diagnostic confirms the missing-date failure is pre-result, but source tracing shows the executor has already been constructed.
  • Contract at risk: Basic command input should be rejected before constructing an external-dependency adapter or performing work unrelated to the invalid request; pre-run errors should identify the user's actionable input failure.
  • Impact: A missing or malformed report date can instead surface a profile source/configuration error, needlessly reads configured Promptkit sources, and follows secret loading from AUD-007 even though the report request can never execute.
  • Recommendation: After flag parsing, reject a missing Daily date immediately; then load configuration for the effective timezone, parse/default the date, resolve paths, and construct the executor only after all request-local validation succeeds.
  • Test implications: Add generate input-failure cases with an injected executor factory and assert zero factory/application calls and no stdout/stderr for a missing Daily date, malformed Daily/Today date, unsupported flag, and unexpected argument. Avoid duplicating timeutil parser cases.
  • Validation: Every generate input failure returns its input diagnostic without constructing an executor or emitting a partial summary; valid requests still construct exactly one executor.
  • Related findings: AUD-007, AUD-010
  • Remediation reference: pending

AUD-010: Assembled non-comparison CLI result paths lack durable tests

  • Stage: 5
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: testing
  • Area: internal/cli.Runner.Run, generate/run command tests, and cmd/weatherreporter
  • Evidence: The CLI suite directly tests generate summary field copying and shared output helpers, but has no assembled Runner.Run generate success, result-bearing failure, quiet failure, or pre-result failure test. Generate calls app.GenerateDetailed directly, unlike the injectable batch and comparison functions, which makes its user-visible wiring difficult to test. Batch has one assembled notification-failure case but no representative ordinary success, report partial failure, quiet failure, or returned-error mapping case. Comparison has comprehensive parser, construction, success, partial failure, cleanup failure, quiet, and pre-execution tests. The command package has no test for executable exit or signal behavior.
  • Contract at risk: Generate and run must translate result presence, action status, quiet mode, stdout JSON, batch stderr lines, returned errors, and the executable's success/failure exit consistently with the CLI reference.
  • Impact: A change can suppress a required failed summary, emit routine output in quiet mode, return success for a failed result, write to the wrong stream, or construct dependencies for invalid input without a focused CLI test failing, even while summary-helper and lower app tests remain green.
  • Recommendation: Add the smallest generate application injection seam symmetrical with batch/comparison, then cover one success, one result-bearing failure, quiet failure, and pre-result failure. Add only representative batch success/report-failure cases and one executable exit/cancellation check; leave workflow details with app tests.
  • Test implications: Prefer structured JSON/status/error assertions and typed errors over complete output snapshots or private call choreography. These tests should own CLI translation, not repeat publication, collection, prompt, or notification mechanics.
  • Validation: Mutations to result/error branching, quiet gating, stream selection, or failure return status break a focused CLI test, while internal app refactors that preserve request/result contracts require no CLI edits.
  • Related findings: AUD-008, AUD-009
  • Remediation reference: pending

AUD-011: Unsupported Weather API URL schemes fail only at transport

  • Stage: 6
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/config.Validate and internal/adapters/weatherapi.New
  • Evidence: Both boundaries accept any parsed absolute URL with a scheme and host. The adapter always constructs an http.Request and executes it with http.Client, so a configured URL such as ftp://weather.example.test passes configuration and adapter construction but fails during warmup with an unsupported-protocol transport error. The integration contract defines an HTTP request boundary, and focused configuration and adapter tests cover malformed or missing URLs but no unsupported absolute scheme.
  • Contract at risk: A syntactically valid configuration must select a transport the Weather API adapter can execute, and intrinsic endpoint incompatibility should fail validation rather than an attempted collection.
  • Impact: An operator can pass configuration validation and Promptkit preflight only to receive a runtime weather-fetch failure for a base URL that can never work. This obscures an actionable configuration defect as external availability and performs avoidable setup before failure.
  • Recommendation: Define http and https as the supported Weather API schemes, document that bound in the configuration and integration owners, and reject every other scheme during config validation and defensively in adapter construction.
  • Test implications: Add relational config and adapter cases that accept local HTTP and HTTPS shapes and reject a representative unsupported absolute URL without making a request. Do not duplicate generic URL-parser cases.
  • Validation: Unsupported schemes fail before Weather API or Promptkit work; ordinary HTTP test servers and configured HTTPS URLs remain accepted.
  • Related findings: none
  • Remediation reference: pending

AUD-012: Weather API failures expose upstream response bodies

  • Stage: 6
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: security
  • Area: internal/adapters/weatherapi.Client.warmupOnce and fetchHTTPOnce
  • Evidence: Both non-2xx branches interpolate the complete body read by their 10 MiB reader into the returned error. That error is wrapped without redaction by fetchHTTP, FetchBundle, and collect.Run, then reaches the application/CLI error path. Focused tests require endpoint and status context but neither require response text nor prove that marker secrets or large provider diagnostics are absent. The integration contract promises endpoint context; it does not make arbitrary upstream bodies part of the diagnostic contract.
  • Contract at risk: Normal errors and summaries must remain bounded and must not expose provider response detail, credentials echoed by an intermediary, internal infrastructure diagnostics, or large HTML/error payloads.
  • Impact: A failing or compromised service can place up to 10 MiB of arbitrary text into normal stderr and wrapped action errors. Besides disclosure, this can overwhelm machine-readable output consumers and make the actionable endpoint/status diagnostic difficult to use.
  • Recommendation: Return the relative endpoint, status code, and a stable classification only. If response text is operationally necessary, admit a small sanitized allowlisted excerpt or capture it only through an explicit secure diagnostic mechanism.
  • Test implications: Serve non-2xx bodies containing a unique secret marker and a large payload; assert that neither appears in the adapter/collection error while endpoint, status, retry identity, and cancellation remain inspectable.
  • Validation: Normal failure output is bounded and marker-free for warmup and source requests, with existing actionable and retry tests still passing.
  • Related findings: AUD-013
  • Remediation reference: pending

AUD-013: The Weather API response limit silently truncates bodies

  • Stage: 6
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/adapters/weatherapi.Client.warmupOnce and fetchHTTPOnce
  • Evidence: Both readers call io.ReadAll(io.LimitReader(resp.Body, 10<<20)) and never determine whether more bytes remain. Most oversized JSON is incidentally rejected as truncated syntax, but a complete JSON envelope followed by enough whitespace or later invalid bytes can be accepted from the first 10 MiB while the remainder is ignored. Warmup accepts every 2xx prefix regardless. No focused test defines behavior at, below, or above the nominal limit.
  • Contract at risk: The documented per-response input bound must distinguish a complete response within the limit from an oversized response; provenance hashes and decoding must describe the complete accepted source payload.
  • Impact: A buggy or hostile service can make Weatherreporter accept a partial response, hash only its prefix, or treat an oversized warmup as healthy. Ordinary oversized products usually fail with a misleading JSON decode error rather than an actionable limit error.
  • Recommendation: Give the transport one shared bounded-body reader that reads at most the maximum plus one byte, rejects excess input explicitly, and is used by both warmup and source attempts. Keep the configured timeout and body closure ownership unchanged.
  • Test implications: Exercise one response at the limit and one over it, including a valid JSON prefix with ignored trailing content; assert a stable size failure and no source decoding or retry for a non-transient size error.
  • Validation: No over-limit warmup or source response is accepted, while a complete at-limit response remains readable and response bodies always close.
  • Related findings: AUD-012
  • Remediation reference: pending

AUD-014: Required hourly periods can lack usable time bounds

  • Stage: 6
  • Status: candidate
  • Severity: medium
  • Confidence: high
  • Category: correctness
  • Area: internal/adapters/weatherapi.bundleBuilder.fetchHourly and internal/weatherdata.ForecastRun
  • Evidence: Required hourly validation rejects a missing/null payload, a JSON type error, and a zero-length periods slice. Go decoding leaves absent issuedAt, startTime, and endTime fields as zero time.Time values, so {"data":{"periods":[{}]}} passes collection with one hourly period. The adapter also does not reject a period whose end is not after its start. Downstream period selection depends on those bounds, and the focused required hourly test covers only explicit null rather than structurally unusable periods.
  • Contract at risk: Hourly is the required normalized forecast product; a present array must contain usable forecast periods rather than merely one decodable object.
  • Impact: Collection can report success and provenance for an hourly product that cannot contribute to any report period. Later derivation may present an empty or incomplete forecast as a successful collection instead of the required-source failure.
  • Recommendation: Validate the smallest required hourly invariant at the normalization boundary: a nonzero issue time if contractually required and, for every period, nonzero bounds with endTime after startTime. Keep meteorological selection and overlap policy in Stage 7 owners.
  • Test implications: Add table-driven adapter cases for missing bounds, zero or reversed duration, and one valid period. Assert direct required-source failure without duplicating downstream forecast selection tests.
  • Validation: Every accepted required hourly period has a usable half-open time range, and existing valid fixtures still normalize unchanged.
  • Related findings: none
  • Remediation reference: pending

AUD-015: Weather API warmup retries permanent HTTP failures

  • Stage: 6
  • Status: candidate
  • Severity: low
  • Confidence: high
  • Category: correctness
  • Area: internal/adapters/weatherapi.Client.warmup and warmupOnce
  • Evidence: Source fetching retries only transport/read failures and the documented 408, 429, 500, 502, 503, and 504 statuses. Warmup uses a separate attempt implementation and retries every error until its budget is exhausted, including permanent 400, 401, 403, and 404 responses. Focused warmup tests cover successful retry of 502 and exhausted 502, while the non-retryable status test covers only a source request.
  • Contract at risk: The Weather API retry policy distinguishes transient failures from permanent request/status failures consistently across HTTP attempts.
  • Impact: Permanent warmup failures cause redundant requests and delay an actionable error by the full retry schedule. The small default budget bounds the effect, so this is primarily avoidable latency and upstream load.
  • Recommendation: Reuse the shared transport attempt classification for warmup while retaining its distinct success rule that a readable 2xx body need not decode as a source envelope.
  • Test implications: Add a warmup 404 case that makes one request, retain the 502 retry case, and assert cancellation interrupts its retry delay. Keep source decoding and optional-source policy tests separate.
  • Validation: Warmup retries only the documented transient classes and fails permanent statuses immediately with endpoint/status context.
  • Related findings: none
  • Remediation reference: pending

Retained Decisions

RET-001: Keep the application package as the explicit composition owner

The package import graph, outbound traces from GenerateDetailed, RunBatchDetailed, and compareDetailed, and their focused tests were inspected. internal/app has deliberately broad outbound dependencies because it sequences project-owned domain, collection, execution, publication, and notification contracts; those dependencies do not point back into app. Splitting that fan-out merely to reduce a graph metric would obscure workflow ownership. Reconsider only if a second composition owner emerges or a coherent workflow can move behind a narrower contract without duplicating ordering policy.

RET-002: Keep external dependencies behind repository-owned contracts

Production import and data-flow inspection found Promptkit imports only in internal/adapters/promptkit and Distributor imports only in internal/adapters/distributor. The Promptkit adapter implements promptexec.Executor; Weather API collection returns weatherdata.Bundle; and Distributor results are translated before application and CLI summaries. This explicit translation prevents dependency types and sensitive diagnostics from becoming application contracts. Reconsider only when an upstream type is intentionally adopted as a public repository contract with corresponding architecture and compatibility changes.

RET-003: Keep publication mechanisms separate by artifact contract

Filesystem-write discovery and call traces were inspected for single reports, comparison bundles, and secure prompt debugging. fileutil.WriteFileAtomic owns one-file replacement, comparison.Publish owns guarded transactional directory replacement and recovery, and promptdebug owns secure explicit diagnostic files. Their shared use of temporary paths and rename operations is mechanical similarity, while their authorization, permissions, commit, and recovery semantics differ materially. Reconsider common abstraction only if multiple artifact kinds acquire the same complete transaction contract.

RET-004: Keep collection as a narrow application-facing seam

The app.Collector contract, collect.Run, the Weather API adapter boundary, and focused collection tests were inspected. Although collect.Run is small, it keeps adapter creation and error context out of orchestration and gives app tests a dependency-neutral deterministic seam. Reconsider if collection gains no additional policy and an equally narrow project-owned adapter contract can replace it without leaking transport construction into app.

RET-005: Keep report definitions explicit and independently reviewable

The four registry definitions were compared across stable ID, prompt and version, template and schema, period resolver, modules, output name, Distributor paths, and run-ID policy. Their similar structure is contract data, while meaningful differences include Daily's required date and dynamic output identity, Today's current local day and Morning membership, Tomorrow's next local day and both batch memberships, and Hourly's rolling elapsed period. Explicit definitions make those differences visible and focused tests assert the exact retained registry surface. Reconsider a shared declarative builder only if the registry grows enough that it can reduce maintenance while preserving independent review of every identity field.

RET-006: Keep command and configuration report lookup policies separate

IDForCommandName accepts only the four exact CLI command names, while IDForConfigKey deliberately trims space, folds case, and normalizes hyphens for configuration keys. Tests also reject retired aliases. Combining the two lookups because their successful mappings are similar would risk broadening the CLI contract or narrowing configuration compatibility. Reconsider only if the accepted-input policies intentionally converge or registry metadata can generate both maps without changing either boundary.

RET-007: Keep civil-day and rolling elapsed-time period policies distinct

Daily, Today, and Tomorrow use CivilDay, whose AddDate boundary correctly produces 23-hour and 25-hour local days. Hourly instead adds six elapsed hours to the injected clock, matching its documented rolling-horizon contract. Batch coverage likewise walks the actual hourly instants within each civil day, and focused app tests exercise daylight-saving dates. Unifying these period calculations would conflate calendar and elapsed-time semantics. Reconsider only if a report's product contract changes which kind of time it represents.

RET-008: Keep configured output paths declarative during loading

Configuration preserves a nonblank output.directory exactly as supplied and does not clean, absolutize, inspect, create, expand, or rebase it. App and publication owners later resolve it against the invocation working directory and perform destination-specific preflight. This keeps configuration loading free of working-directory and filesystem lifecycle policy while still rejecting a meaningless whitespace-only setting. Reconsider only if a new configuration contract requires an intrinsic path property that can be checked without assuming an execution destination.

RET-009: Keep inactive optional integrations free of active-field validation

Distributor templates and delivery settings are validated only when notification is enabled, and batch templates only when both Distributor and batch notification are enabled. Likewise, a blank Promptkit local endpoint means the backend is unregistered and its configured default concurrency is dormant; CLI mapping forwards the limit only with an endpoint, while the adapter rejects an active limit without one. This lets complete defaults and examples describe optional integrations without making unused credentials or templates mandatory. Reconsider only if dormant configuration must become an installation-time lint contract distinct from runtime configuration loading.

RET-010: Keep Promptkit profile source precedence in its adapter boundary

Configuration owns mutual exclusion of profile_file and profile_dir, URL and concurrency shape, and the selected profile ID. The Promptkit adapter owns opening configured sources, complete-definition precedence, embedded and built-in fallbacks, and classification of malformed profile errors before collection. Maintained profile tests exercise that boundary offline. Moving profile parsing into config would duplicate dependency-specific behavior and leak Promptkit mechanics. Reconsider only if Weatherreporter adopts a repository-owned profile format independent of Promptkit.

RET-011: Keep command-specific parsers around one shared flag core

The three action parsers use separate flag.FlagSet values so unsupported flags fail at the command that owns them: generate alone accepts --out, run and compare accept --out-dir, compare alone accepts repeated --profile and --replace, and only Daily/Today generate or compare accept --date. addCommonFlags shares only config, units, timezone, and prompt-debug options. This small explicit split keeps the acceptance matrix reviewable without a general command framework. Reconsider if additional actions make the common registration or post-parse checks materially larger or divergent.

RET-012: Keep action summaries distinct and bounded by CLI ownership

Generate, batch, and comparison have different public result contracts, so separate summary types and constructors are preferable to a generic result envelope. Generate copies selected fields and a compact notification result; batch preserves ordered report outcomes and independent notification status; comparison locks its field order and converts top-level failures to bounded safe categories. Pre-result failures emit no JSON, while result-bearing failures emit a failed summary before returning an error, and one shared output gate implements quiet mode. Reconsider common abstraction only if multiple actions acquire the same complete summary and failure semantics.

RET-013: Keep source-specific normalization explicit

The eight source methods share transport and missing-policy helpers but retain visible source semantics: query parameters differ, hourly alone is required, alerts alone treats explicit null as checked empty data, SPC empty arrays are also checked data, and issue/update timestamps come from different payload fields. Collapsing those methods into a generic descriptor would hide the normalization decisions that need independent review. Reconsider a declarative table only if it can express every source's availability, empty-value, timestamp, and validation policy without callbacks that recreate the current methods indirectly.

Open Questions

No Stage 1 open questions or unexplained baseline failures remain.

Stage 2 routed these investigation leads without treating graph metrics or an unusual edge as findings:

  • internal/config imports internal/briefing to use module definitions while validating report-module options. Stages 4, 8, and 23 should determine whether this remains the smallest single-owner validation path or creates avoidable registry coupling.
  • The graph identifies internal/cli.Run, internal/app.RunBatchDetailed, and internal/app.compareDetailed as relatively complex orchestration paths. Their ownership and top-level ordering are coherent; Stages 5, 16, 18, 22, and 23 should assess their local behavior and maintainability rather than inferring a finding from metrics.

Stage 3 routed these investigation leads to their assigned later stages:

  • LoadLocation accepts numeric offsets through 23:59, while the config reference does not state an offset range. Stage 4 should assess the effective configuration contract and validation ownership before judging that bound.
  • The report-registry document says focused tests cover run IDs and output names, but Stage 3 found no behavioral run-ID assertion and only indirect dynamic output-name coverage. AUD-003 captures the consequential run-ID gap; Stage 24 should reconcile the broader documentation claim with the eventual executable contract.
  • Prompt, template, schema, and module IDs are internally consistent at the report registry. Stages 8 and 10-13 remain responsible for validating the referenced registries, embedded assets, generated-text schemas, and render compatibility rather than duplicating those audits here.

Stage 4 routed these investigation leads to their assigned later stages:

  • Configuration accepts any absolute URL scheme with a host, matching the documented term “absolute URL.” Stages 6, 11, and 16 should assess transport scheme support at the Weather API, Promptkit, and Distributor boundaries before narrowing the configuration contract.
  • weather_api.base_url may be empty in a valid configuration because only collection workflows require it. Stages 5-6 should verify action-specific failure timing and diagnostics rather than making every non-collection configuration require an endpoint.
  • Report config-key normalization accepts trimmed and case-folded spellings in addition to the documented hyphen/underscore equivalence. Stage 24 should decide whether the canonical reference should describe that compatibility or the executable contract should be narrowed.
  • Distributor delivery redaction, runtime template consumption, and notification idempotency remain assigned to Stage 16. Stage 4 accounts only for active configuration and safe identity/path rendering before execution.

Stage 5 routed these investigation leads to their assigned later stages:

  • Generate and batch summaries intentionally carry project-owned app error and warning fields, while comparison applies an additional bounded safe-error translation. Stages 6, 11, and 16 should verify that Weather API, Promptkit, and Distributor producers never place sensitive endpoint, credential, or provider detail in the non-comparison fields before Stage 24 judges the full documented output claim.
  • Generate and comparison repeat the Daily/Today flag-acceptance and date defaulting policy. They currently agree, and the report registry remains the period owner. Stage 23 should assess whether a shared CLI policy descriptor would reduce drift without moving parsing into the domain registry.
  • The static help text includes --date in usage but omits it from the Options list and describes --quiet as suppressing successful output even though it suppresses failed action summaries too. docs/internal/cli.md also still says the CLI dispatches only generation and batch actions despite implemented comparison dispatch. Stage 24 should reconcile these documentation/help statements without expanding Stage 5 into the repository-wide documentation audit.
  • Batch status writes deliberately ignore stderr write failures while JSON encoding errors are returned. Stage 25 should exercise broken-pipe and output writer failures dynamically before deciding whether routine-status I/O must affect the process exit.

Stage 6 routed these investigation leads to their assigned later stages:

  • facts.BuildCollected and facts.Bundle copy source and warning slices but not nested query maps or warning slices. Prepared-report construction later deep-copies its complete inputs. Stage 7 should judge whether the earlier derivation boundary promises mutation isolation or whether immutable workflow ownership makes the shallow copy intentional.
  • AlertRun.Raw preserves the complete alert payload in addition to the raw alert items, but graph-augmented use search found no production consumer of the full duplicate. Stage 23 should assess removal with other unused and duplicate internal surfaces; alert-item schema and parsing remain assigned to Stages 7-9.
  • Source provenance retains the adapter-controlled query parameters in the normalized bundle, while prompt-facing metadata deliberately omits the query map. Stage 10 should confirm that no prompt input or debug artifact widens that boundary, and Stage 24 should judge whether the internal documentation needs an explicit non-secret-query invariant.
  • The adapter accepts URL user information, base query parameters, and fragments because the current absolute-URL check does not constrain them; endpoint construction drops the fragment and carries base query values into requests/provenance. The current configuration and examples use none of these. Stage 24 should clarify the intended base-URL shape after remediation of AUD-011, rather than Stage 6 inventing undocumented authentication or query behavior.

Stage Log

Stage 1: Establish The Baseline And Audit Ledger

  • Status: Complete.
  • Scope reviewed: repository identity, worktree state, Go module and workspace context, graph identity, tracked package/source/test/fixture/asset/document inventory, and required baseline validation.
  • Exclusions: none beyond the stage boundary; no production code, tests, dependencies, examples, or canonical documents were changed.
  • Result: the audit is tied to exact commit e7c7262; the expected-baseline difference is explained; every required baseline command passes; and all later stages remain pending.
  • Findings: none.
  • Retained decisions: none.
  • Open questions: none.

Stage 2: Audit Architecture And Dependency Direction

  • Status: Complete.
  • Scope reviewed: cmd/weatherreporter, production import boundaries for all 24 Go packages, principal call and data-flow paths, external adapter containment, project-owned interfaces, publication boundaries, filesystem writes, ADR 0001, focused orchestration tests, and the architecture policy.
  • Exclusions: Detailed report/time, configuration, CLI, weather, domain, module, prompt, rendering, comparison, and notification correctness remains assigned to Stages 3-18. Cross-cutting complexity, deduplication, and full documentation coherence remain assigned to Stages 22-24.

Boundary Accounting

Normative boundary or invariant Implementation owner and evidence Disposition
Binary entry and CLI ownership cmd/weatherreporter.main calls cli.Run; internal/cli owns action parsing, config loading, executor construction, and bounded result output. Graph traces place app calls below action resolution. Matches policy.
Configuration ownership internal/config owns defaults, YAML loading, normalization, secrets, and validation. CLI loads config and passes effective values down; app does not parse config files. Matches policy; the config-to-briefing validation edge is routed to Stages 4, 8, and 23.
Application orchestration GenerateDetailed, RunBatchDetailed, and compareDetailed sequence preflight, collection, preparation, execution, publication, and notification through repository-owned values and interfaces. No lower package imports app. Matches policy; retained as RET-001.
Report and domain ownership report, timeutil, weatherdata, forecast, facts, module, briefing, promptinput, generatedtext, and reporttemplate form one-way deterministic dependencies below app. Matches policy at package level; local rules remain for Stages 3 and 7-13.
Prompt execution boundary promptexec.Executor is dependency-neutral. CLI constructs the Promptkit adapter once per action, app consumes only promptexec requests/results, and the prepared-report data flow supplies curated serialized module packages. Matches policy; retained as RET-002.
Weather API boundary App calls its Collector interface; the production implementation delegates through collect.Run to adapters/weatherapi.FetchBundle, which returns normalized weatherdata.Bundle. Production HTTP imports occur only in external adapters. Matches policy; retained as RET-004.
Distributor boundary App owns notification timing and repository request/result types; distributorNotifier translates them to adapter-local types and the adapter alone imports the Distributor dependency. Comparison exposes no notifier path. Matches policy.
Prompt preflight and curated inputs Generate inspects the exact prompt/profile before collectWeather; batch inspects all candidates before its one collection; comparison inspects prompt and all profiles before collection, then calls prepareReport once. Data-flow traces reach promptinput.Build and MarshalYAML, not raw bundle serialization into Promptkit. Matches policy at workflow level; detailed checks remain for Stages 10, 11, 14, and 18.
Single output and notification order publishPromptReport checks context, calls fileutil.WriteFileAtomic, records the path, and only then calls notifyReport. Focused tests cover pre-publication preservation and notification ordering. Matches policy; publication split retained as RET-003.
Batch output and notification order RunBatchDetailed validates every planned path before sequential execution, suppresses item notification, continues across item failures, and calls notifyBatch only after the loop. Focused tests cover preflight, partial failure, and notification-after-publication. Matches policy at workflow level; detailed review remains for Stage 16.
Comparison publication App preflights before external work and again before publication. internal/comparison owns bundle validation, guarded replacement, staging, commit, restoration, and cleanup results. The comparison request has no notifier and the trace does not reach Distributor. Matches policy at workflow level; detailed review remains for Stages 17-18.
Stateless execution Normal traces retain preparation and execution values in memory. Production filesystem writes are limited to selected Markdown publication, selected comparison bundles, explicit prompt-debug capture, and the unreachable helpers in AUD-001; no cache, workspace, history, receipt, or resume owner appears in the package graph. Matches normal-runtime policy; AUD-001 records the unused persistence surface.
Dependency cycles and direction The compiler-derived direct-import inventory is acyclic. Entry packages point inward, adapters do not import app/CLI, domain packages do not import orchestration, and no external dependency type appears outside its production adapter. Matches policy.

Commands And Evidence

  • Refreshed graph project home-eric-Workspace-weatherreporter in moderate mode. The graph's branch identity remains production baseline e7c7262; Stage 1's intervening commit changes only this excluded audit document.
  • Used graph architecture views for structure, dependencies, entry points, hotspots, boundaries, layers, and clusters; queried the graph schema and direct import edges.
  • Used graph search, snippets, inbound/outbound call traces, and data-flow traces for main, cli.Run, GenerateDetailed, RunBatchDetailed, compareDetailed, prepareReport, executePreparedProfile, collect.Run, all three external adapters, publishPromptReport, comparison.PlanDestination, comparison.Publish, and Distributor notification.
  • Used compiler-derived go list direct imports to separate production edges from test-only graph edges and confirm the build has no import cycle.
  • Used graph-augmented code search to inventory external dependency imports, HTTP ownership, and production filesystem writes. A bounded text search confirmed the unused persistence helpers have no documentation or hidden non-code callers.
  • Ran go test ./internal/app ./internal/cli ./internal/collect ./internal/comparison ./internal/adapters/... ./internal/fileutil ./internal/promptdebug ./internal/promptexec; all focused packages passed.
  • Findings: AUD-001.
  • Retained decisions: RET-001, RET-002, RET-003, and RET-004.
  • Open questions: the two leads recorded above are routed to their assigned later stages.

Stage 3: Audit Report Identity And Time Foundations

  • Status: Complete.
  • Scope reviewed: all production and focused-test code in internal/report and internal/timeutil; report and timezone contracts in docs/cli.md, docs/config.md, and docs/internal/report-registry.md; and the focused internal/app and internal/forecast callers that consume batch membership, run IDs, civil periods, clock windows, and hourly coverage.
  • Exclusions: Effective configuration validation and CLI date parsing remain assigned to Stages 4-5. Forecast derivation beyond the time boundary remains assigned to Stage 7. Module, prompt, schema, and template compatibility remains assigned to Stages 8 and 10-13. Batch notification and Distributor idempotency behavior remains assigned to Stage 16, and documentation-wide coherence remains assigned to Stage 24.

Contract Accounting

Contract or risk Implementation and caller evidence Disposition
Stable report identities The default registry contains exactly Daily, Today, Tomorrow, and Hourly. Each definition carries the documented prompt 2.0.0, matching template/schema identity, modules, output name, and Distributor paths; focused registry tests assert the retained surface. Matches the registry contract; explicit definitions retained as RET-005. Asset compatibility remains for later owner stages.
Alias resolution Command lookup accepts exact current command names. Config lookup separately normalizes whitespace, case, and hyphens. Both reject retired aliases in focused tests. Matches the distinct boundary contracts; separation retained as RET-006.
Batch membership and ordering Morning explicitly plans Today then Tomorrow before eligible Daily dates; Evening explicitly plans Tomorrow before eligible Daily dates. Focused app tests cover ordering, flags, multiple Daily dates, and no eligible dates. Matches CLI and registry contracts. Detailed partial-success and notification semantics remain for Stage 16.
Report period policy Daily requires an explicit date; Today uses an explicit date or current local date; Tomorrow selects the next local civil day; Hourly spans six elapsed hours from the injected clock. Resolve defaults a missing location to UTC and a zero clock to the system clock. Matches documented selection policy; calendar and elapsed policies retained separately as RET-007.
Civil-day and hourly DST behavior CivilDay uses local midnight plus one calendar day, and batch hourly coverage walks actual instants, correctly accommodating 23-hour and 25-hour dates. Focused batch tests exercise DST civil days. Sufficient for the reviewed policy.
Configured clock windows ClockWindow adds elapsed clock durations to local midnight. The only production path reaches forecast.ResolveDayparts, daily summaries, and facts. Fixed-zone tests do not exercise either DST transition. Incorrect on DST dates; recorded as AUD-002.
Date and timezone parsing ParseLocalDate uses strict YYYY-MM-DD parsing in the selected location. LoadLocation covers IANA names, America/ fallback, documented aliases, fixed US abbreviations, and signed offsets, returning contextual errors for invalid input. Implementation is coherent for Stage 3. Effective config validation and CLI error presentation remain for Stages 4-5.
Clock control All report resolvers accept an injected Now; only zero-valued input falls back to time.Now. Focused report and batch tests use fixed clocks. Sufficient for deterministic period selection.
Daily output and run identity Daily output names derive from the resolved valid-start date, and Daily run IDs append that date so same-clock multi-date products remain distinct. The downstream batch-notification map rejects duplicate IDs. Implementation is coherent, but the identity invariant lacks a focused behavior test; recorded as AUD-003.
Half-open period operations Period.Valid, Contains, Overlaps, and Intersection consistently use [start,end) semantics, and focused tests protect overlap boundaries. Matches the internal period contract.

Commands And Evidence

  • Used graph search, source snippets, and inbound/outbound traces for the four report definitions, registry construction and lookup, command/config mapping, batch mapping and planning, Resolve, every period resolver, CivilDay, ClockWindow, ParseLocalDate, LoadLocation, period overlap operations, Daily output naming, run-ID construction, ResolveDayparts, batch hourly coverage, and plannedReportsByRunID.
  • Compared the implementation with docs/cli.md, docs/config.md, and docs/internal/report-registry.md, then inspected the focused report, time, forecast, and app tests. Bounded text searches were used for string and test assertion discovery not represented by the code graph.
  • Reproduced the clock-window defect with America/Chicago transition dates: 17 elapsed hours after midnight yields 18:00 on 2026-03-08 and 16:00 on 2026-11-01; six elapsed hours yields 07:00 and 05:00 respectively.
  • Ran go test ./internal/report ./internal/timeutil ./internal/forecast ./internal/app; all focused packages passed.
  • Findings: AUD-002 and AUD-003.
  • Retained decisions: RET-005, RET-006, and RET-007.
  • Open questions: the three leads recorded above are routed to their assigned later stages.

Stage 4: Audit Configuration, Secrets, And Validation

  • Status: Complete.
  • Scope reviewed: all production and focused-test code in internal/config; the configuration-to-executor mapping in internal/cli; Promptkit adapter configuration and focused tests; all three maintained examples; docs/config.md; and configuration-related architecture, operations, documentation, and testing rules.
  • Exclusions: CLI parsing and action-specific diagnostics remain assigned to Stage 5. Weather API transport behavior remains assigned to Stage 6. Promptkit source loading and sensitive execution diagnostics remain assigned to Stage 11. Output publication preflight remains assigned to Stages 15 and 17, Distributor delivery remains assigned to Stage 16, and repository-wide documentation coherence remains assigned to Stage 24.

Precedence And Validation Map

Contract or risk Implementation and consumer evidence Disposition
Discovery and precedence Load starts with Defaults, merges the explicit or default YAML path, tolerates only an absent implicit default file, applies nonempty units/timezone overrides, normalizes report modules, loads configured secrets, and validates. Representative CLI coverage proves explicit units/timezone beat file values while unrelated file values remain. Matches the documented defaults → file → CLI order. Environment variables do not override config fields.
YAML schema and migrations mergeFile uses KnownFields(true); custom notification, report, Distributor-path, and module decoders reject their own unknown fields. Retired scriptorium, recent-change, and workspace keys have focused migration/rejection tests. Sufficient for known struct fields and retired top-level contracts. Unsupported keys inside the semantic missing-source map are AUD-004.
Defaults and partial merges Exact documented defaults are built once in Defaults; YAML decoding merges partial nested structs over them, and focused tests cover default literals, omitted optional sections, batch partial configuration, and both maintained config examples. Matches the canonical reference. Exact-default assertions protect user-visible contracts rather than private constants.
Output directory Config accepts empty, relative, and absolute values, preserves them byte-for-byte, and rejects only nonempty whitespace. Graph traces place path resolution, inspection, creation, and publication below app/filesystem owners. Matches policy; retained as RET-008.
Weather and daypart validation Config checks absolute base URLs when present, positive timeout, nonnegative precision, nonempty units/timezone, resolvable timezone, JSON format, nonempty dayparts, names, and parseable clocks. Collection separately requires a base URL for collection actions. Matches the documented split. The Stage 3 DST defect remains AUD-002.
Missing-source policy Policy values and nonblank keys are validated, and the adapter consumes source-specific values by source name with a default fallback. Required hourly failures bypass optional-source policy. Unsupported and ineffective keys pass silently; AUD-004.
Report/module overrides Loaded aliases, duplicate normalized reports, module identities/order, compatibility, duplicate modules/stanzas, typed options, explicit empty lists, and Distributor path templates are validated through report/module registries. Loaded YAML behavior is well covered. Exported constructed values cannot establish private presence state; AUD-006. Detailed module semantics remain for Stages 8-9.
Promptkit settings Config validates mutually exclusive profile sources, positive timeout, optional absolute local endpoint, and nonnegative concurrency. CLI maps a dormant endpoint-less limit to the adapter zero value; adapter construction owns file/directory loading, backend registration, fallbacks, and safe error classification. Matches the documented inactive/active split; retained as RET-009 and RET-010.
Secret directory Empty disables loading. A configured directory accepts only directly contained regular files with environment-variable basenames, rejects symlinks/directories/nonregular/unreadable entries, removes one LF or CRLF, overwrites the named environment value, and reports only directory/file names and operation context. Values are absent from tested diagnostics, but failure application is not atomic; AUD-007.
Distributor configuration Disabled notification skips active-field checks. Enabled notification validates endpoint, token variable name, timeout, failure policy, allowed template variables, batch activation, and report path safety/uniqueness. Templates render from repository-owned metadata rather than environment values. Mostly sufficient. Two single-report rendered identities omit the nonblank invariant; AUD-005.
Filesystem and URL timing Config validates intrinsic string/URL shape. Output destinations are inspected by app/publication owners, Promptkit sources by its adapter, and secret entries by the config loader. No audit command contacted an external service. Matches ownership boundaries for Stage 4; transport-specific scheme support is routed to later stages.
Constructed configuration Validate, report override accessors, and focused tests attempt to provide loaded/constructed consistency without mutating caller-owned option values. General scalar validation is consistent, but report override presence is not externally constructible; AUD-006.
Maintained examples Both YAML configuration examples load through the strict config suite. The endpoint-only weather-light profile is inspected through the real Promptkit adapter with embedded assets and no network or credential. All examples contain synthetic endpoints and no secrets. Matches documentation and testing policy.

Commands And Evidence

  • Used graph search, source snippets, call traces, and graph-augmented test searches for defaults, Load, YAML merging and migration rejection, Validate, secret loading, report/module/path traversal, notification renderers, CLI executor mapping, Promptkit adapter construction, Weather API missing-source policy consumption, and app notification consumers.
  • Compared executable fields, defaults, precedence, templates, paths, profile settings, source keys, and secret behavior with docs/config.md, maintained examples, and the configuration-related operations and architecture rules.
  • Ran the production CLI with examples/minimal-config.yml. It reached offline profile credential preflight and returned missing_credential before weather collection, confirming that the endpoint-less local concurrency default is intentionally dormant rather than rejected by adapter construction.
  • Ran go test ./internal/config ./internal/adapters/promptkit ./internal/cli; all focused packages passed.
  • Findings: AUD-004, AUD-005, AUD-006, and AUD-007.
  • Retained decisions: RET-008, RET-009, and RET-010.
  • Open questions: the four leads recorded above are routed to their assigned later stages.

Stage 5: Audit CLI Parsing, Wiring, And Output Contracts

  • Status: Complete.
  • Scope reviewed: cmd/weatherreporter, all production and representative tests in internal/cli, docs/cli.md, docs/internal/cli.md, immediate config/report/time/executor mapping, and app request/result/error boundaries. Comparison parsing and CLI translation were reviewed, while detailed comparison execution remains excluded.
  • Exclusions: Weather collection, report generation, publication, notification, and comparison execution correctness remain assigned to Stages 6-18. Cross-cutting parser deduplication, test portfolio durability, documentation coherence, and dynamic robustness remain assigned to Stages 21 and 23-25.

Command And Exit Accounting

Command or risk Parsing, result, and exit evidence Disposition
Root help and version No arguments, -h, and --help print static help without config/provider setup. Exact --version prints the injected or build version; extra version arguments fail. Main prints any returned error to stderr and exits 1; nil returns normally. Matches the documented root contract. Help wording drift is routed to Stage 24.
Generate dispatch Requires one of four exact report names. Command-specific flag sets accept only documented flags; Daily requires a date, Today defaults or parses one, and Tomorrow/Hourly reject --date. Config and explicit output/working-directory values map into one app request and one executor. Functional mapping matches, but date validation occurs after executor construction; AUD-009. Assembled result/exit coverage is AUD-010.
Run dispatch Requires exact Morning or Evening batch identity, accepts --out-dir rather than --out, resolves only explicit relative overrides against the absolute invocation directory, preserves configured fallback, and constructs one executor/request. Matches the parser/wiring contract. Representative assembled result coverage is incomplete in AUD-010; detailed batch behavior remains Stage 16.
Compare dispatch Requires a valid report and two distinct nonblank ordered profiles before config/executor work; applies the same report date eligibility; clears the configured default profile; maps exact bundle destination/replace/debug options; constructs one executor. Matches the CLI contract. Focused tests cover parser, construction frequency, pre-execution failure, success/partial/cleanup summaries, quiet behavior, and safe errors. Detailed execution remains Stage 18.
Common flags and ownership addCommonFlags registers config, units, timezone, and prompt debug only. Each action separately registers its output, quiet, date, profile, and replacement flags. Report and batch name lookup delegates to internal/report; config precedence delegates to internal/config. Intentional explicitness retained as RET-011; repeated date policy is routed to Stage 23.
Validation ordering Unknown commands/reports, unsupported flags, extra args, and comparison profile shape fail before configuration or executor creation. Generate performs Daily presence and Daily/Today parsing after config and executor creation. Partly incorrect; AUD-009.
Output paths CLI captures/validates one absolute working directory, preserves configured fallbacks, and resolves only nonempty relative explicit output overrides against that directory. App owns default naming and publication. Matches CLI/config documentation and architecture ownership.
Stdout, stderr, and quiet Result-bearing actions emit indented JSON to stdout. Run emits compact ordered item/notification/batch lines to stderr first. Quiet returns before both routine channels but the returned error still reaches main stderr. Pre-result failures emit no structured output. Matches the documented separation; summary design retained as RET-012. Broken status-writer behavior is routed to Stage 25.
Generate summaries Selected report, prompt, time, profile/model, warnings, validation, output, debug, notification, and error fields are copied; status is failed exactly when a result accompanies an error. Shape matches documentation, but only the constructor is tested; assembled behavior is AUD-010. Producer redaction remains for Stages 6, 11, and 16.
Batch summaries and exit Report counters remain independent of notification failure. Any failed report or failed notification makes summary status/error failed, emits status lines unless quiet, and returns typed BatchError so main exits nonzero. Mapping is coherent and notification failure is assembled-tested. Broader representative translation coverage is AUD-010.
Comparison summaries Field order is fixed by the explicit DTO; profile order is retained; unpublished artifact fields are omitted; errors are mapped to bounded aggregate, cancellation, deadline, Promptkit, destination, cleanup, or application categories. Matches the documented safe CLI contract with strong focused coverage.
Cancellation Runner.Run forwards its context to every app action, and app tests protect cancellation behavior. Production main supplies an uncancelable background context and has no signal bridge. Programmatic propagation is correct; executable cancellation is AUD-008.
Test ownership Parser/resolver tests own flag acceptance, paths, configuration mapping, and one-executor construction; summary tests own selected fields; comparison command tests own assembled translation. Comparison is sufficient. Non-comparison assembled coverage is insufficient in AUD-010; lower workflow details correctly remain in app tests.

Commands And Evidence

  • Used graph inventory, source snippets, inbound/outbound traces, and graph-augmented searches for root dispatch, every action parser/resolver, common flag registration, working/output path resolution, executor mapping, summary/status construction, output routing, error classification, app cancellation consumers, and process signal handling.
  • Compared executable help, accepted flags, summary fields, error/status rules, and action wiring with docs/cli.md, docs/internal/cli.md, and the configuration, architecture, documentation, and testing contracts.
  • Ran go test -cover ./internal/cli; the suite passed at 75.8% statement coverage. Coverage was used only to guide test inspection, not as a finding.
  • Ran go run ./cmd/weatherreporter --help, go run ./cmd/weatherreporter --version, and go run ./cmd/weatherreporter generate daily; help and development-version paths succeeded, while missing Daily date returned the documented pre-result error and nonzero status without contacting an external service.
  • Findings: AUD-008, AUD-009, and AUD-010.
  • Retained decisions: RET-011 and RET-012.
  • Open questions: the four leads recorded above are routed to their assigned later stages.

Stage 6: Audit Weather Data Acquisition And Collection

  • Status: Complete.
  • Scope reviewed: all production code and focused tests in internal/adapters/weatherapi, internal/weatherdata, and internal/collect; all eight adapter fixtures; the Weather API integration, normalized weather-data, and collection internal documents; and immediate app/prompt-facing consumers needed to account for provenance, warnings, and error exposure.
  • Exclusions: Meteorological selection, alert interpretation, forecast/fact derivation, and aliasing policy remain assigned to Stage 7. Prompt-facing curation remains assigned to Stages 8-10, app orchestration to Stages 14-16, cross-cutting cleanup to Stage 23, documentation reconciliation to Stage 24, and adversarial dynamic checks to Stage 25.

Source And Failure Accounting

Source or transport risk Required, empty, malformed, and provenance behavior Disposition
Warmup Calls /conditions/current first with format, units, and precision; requires a readable 2xx body; closes it; and stops before source requests on failure. Context cancels requests and retry waits. Endpoint and cancellation behavior match. Permanent statuses are retried contrary to the shared policy in AUD-015; body safety/limits are AUD-012 and AUD-013.
Observations Optional. Missing/null and malformed data use configured policy. A successful value records observation timestamp as issue time plus endpoint, query, fetch time, and compact-data hash. Matches the source contract.
Current conditions Optional. Missing/null and malformed data use configured policy; successful normalized fields and provenance are retained. Matches the source contract.
Hourly forecast Required regardless of optional-source policy. Missing/null, decode failure, and an empty period list fail collection; format, units, precision, and timezone are sent. Availability mapping matches, but periods with zero/reversed time bounds pass; AUD-014.
Narrative forecast Optional. Missing/null and malformed data use configured policy; issue/update timestamps and forecast periods are normalized. An empty period list remains checked data. Matches the documented source contract. Semantic forecast use remains Stage 7.
Active alerts Optional. An absent member is missing, explicit null is checked empty data with a hash and no warning, and malformed non-null data uses configured policy. Alert items remain raw payloads at this boundary. Matches the explicit alerts exception. Full duplicate raw payload is routed to Stage 23.
Forecast discussion Optional. Missing/null and malformed data use configured policy; issue/update times, key messages, and short/long sections are normalized. Matches the source contract.
Weather story Optional. Uses format only; missing/null and malformed data use configured policy; start/update provenance is retained when available. Matches the source contract.
SPC convective outlooks Optional. Uses format/timezone without units; missing/null uses configured policy; non-null empty outlook/discussion lists are checked data; issue time prefers issuedAt and falls back to asOf; GeoJSON remains raw. Matches the explicit SPC empty-data and provenance contract.
Optional-source policy error aborts with no partial bundle, warn records the same stable warning in source and bundle, and none records a missing source without a warning. Transport/status/envelope failures remain direct request errors. Matches the documented division. Unsupported policy keys remain the Stage 4 finding AUD-004.
Endpoint construction Joins every fixed endpoint to a base path prefix and applies only the required format/units/precision/timezone matrix. Query/path behavior matches for HTTP(S); unsupported absolute schemes survive until runtime in AUD-011. Base URL query/user-info shape is routed to Stage 24.
HTTP attempt lifecycle Requests carry context and configured client timeout; response bodies close after bounded reads; source attempts retry transport/read failures and only the documented transient statuses. Mostly coherent. Unsafe response text and silent truncation are AUD-012/AUD-013; warmup classification divergence is AUD-015.
Normalization and collection ownership weatherdata contains no HTTP/config/filesystem behavior. The adapter translates wire envelopes into project types. collect.Run constructs one adapter, forwards context, and distinguishes setup from fetch errors. Matches architecture; the narrow collection seam remains retained under RET-004. Source-specific normalization remains explicit under RET-013.
Test assets and ownership Adapter tests use httptest.Server for HTTP/query/status/retry/cancellation/policy behavior and eight small checked-in JSON fixtures for translation. weatherdata owns a focused GeoJSON round trip; collect owns three real local composition/error cases. Fixtures are synthetic, credential-free, and total under 5 KiB. Ownership is distinct and offline. Missing high-risk cases are attached to AUD-011 through AUD-015, not inferred from coverage alone.

Commands And Evidence

  • Used graph architecture, symbol search, source snippets, inbound traces, and graph-augmented use searches for adapter construction, all eight source methods, HTTP attempts and retries, body lifecycle, endpoint/query assembly, missing/malformed policy, hashes and timestamps, normalized source consumers, collect.Run, and propagation toward app/CLI boundaries.
  • Compared implementation and fixtures with docs/integrations/weatherapi.md, docs/internal/weather-data.md, docs/internal/collect.md, the architecture policy, and the testing policy. Bounded text inspection was used for documentation, JSON fixtures, literal credential markers, and known test assertions outside graph discovery.
  • Ran go test -coverprofile=/tmp/weatherreporter-stage6-cover.out ./internal/adapters/weatherapi ./internal/weatherdata ./internal/collect; all focused packages passed. Adapter statement coverage was 84.9% and collection coverage 100%; coverage was used only to guide branch inspection.
  • Findings: AUD-011, AUD-012, AUD-013, AUD-014, and AUD-015.
  • Retained decisions: existing RET-004 was revalidated and RET-013 records explicit source normalization.
  • Open questions: the four leads recorded above are routed to their assigned later stages.