Files
weatherreporter/docs/roadmap/audit.md

825 lines
56 KiB
Markdown

# Repository Audit Ledger
Status: In progress; Stages 1-4 complete.
This temporary roadmap document is the evidence ledger for the staged audit
defined by [the audit plan](audit-plan.md). It records audit evidence and
status, not implemented product behavior. Current contracts remain with the
canonical owners identified by the
[documentation policy](../policy/documentation.md).
## 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. 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 | Pending |
| 6 | Audit weather data acquisition and collection | Pending |
| 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 | `internal/cli` | 5 | Pending |
| Weather transport and normalized collection | `internal/adapters/weatherapi`, `internal/collect`, `internal/weatherdata` | 6 | Pending |
| 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
## 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.
## 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 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.