26 KiB
Cleanup Roadmap
Purpose
This roadmap defines the staged cleanup work recommended by docs/roadmap/audit.md. It is written for an LLM coding agent that will implement the stages in order.
The cleanup is intended to simplify, centralize, and clarify implementation logic before the next major release without changing public CLI syntax, managed workspace paths, report IDs, prompt IDs, generated output semantics, or external integration behavior unless a stage explicitly says otherwise.
This file belongs under docs/roadmap/ because it describes planned refactoring work. Non-roadmap documentation must not describe the planned behavior here until the corresponding code is implemented.
Cleanup Principles
- Preserve implemented behavior unless a stage explicitly directs a change.
- Prefer narrow explicit helpers and registries over broad frameworks.
- Keep package boundaries from
docs/policy/architecture.md:internal/configowns config loading, defaults, overrides, secrets, and validation.internal/reportowns report identity, report definitions, valid periods, batches, output names, and comparison declarations.internal/cliowns command parsing and help text.internal/appowns orchestration.internal/briefingowns prompt-facing module builders and module registry.internal/generatedtextowns generated-text validation and render-context construction.internal/reporttemplateowns embedded templates and generated-text schema assets.- external system types stay inside
internal/adapters/*.
- Do not introduce Cobra, a workflow engine, plugin architecture, per-module packages, per-report packages, a manifest/resume system, or a global test framework.
- Keep tests focused near the package that owns the behavior.
- Run
gofmt -won changed Go files. - Update implemented docs only in the stage that changes implemented behavior or package contracts.
Decisions Locked
- Generated-text/template resolution should use one explicit catalog rather than app-owned switches plus separate template/schema maps.
- Report definitions remain authoritative for report IDs, prompt IDs, generation mode, template ID, generated-text schema ID, valid-period resolver, module composition, artifact group, and output naming.
internal/reportshould become the canonical home for report public-name and alias policy used by CLI, config, and app request resolution.- App orchestration should keep a direct, readable workflow. Extract only narrow helpers for repeated policies such as final report completion.
- Config report-module normalization should be a load-time mutation. Validation should not perform duplicate mutating normalization.
ReportModuleOverridesshould not silently ignore invalid report keys in any path an application caller can reach.- Weather API source lifecycle cleanup should be explicit and source-aware. Do not use reflection or a generic ingestion framework.
- Test cleanup should use package-local helpers only.
- State remains artifact-oriented. Do not add a manifest, checkpoint, resume, or run-index subsystem in this cleanup sequence.
Stage 1: Generated-Text And Template Catalog
Goal: make generated-text report asset resolution one explicit catalog so validators, schemas, templates, and render-context builders cannot drift independently.
Scope
Create a narrow catalog for generated-text-template reports. The catalog should connect:
- generated-text schema ID;
- embedded JSON schema asset;
- generated-text validator;
- report template ID;
- embedded Markdown template asset;
- render-context builder.
Implementation Guidance
- Prefer placing the primary catalog in
internal/generatedtextif it owns validator and render-context function wiring. - Keep embedded asset reading in
internal/reporttemplate; do not move embedded template or schema files. - Add an exported or package-internal lookup API with a small app-facing surface, for example:
generatedtext.LookupDefinition(def report.Definition);- or
generatedtext.Lookup(schemaID, templateID string).
- The returned handler should expose methods or fields for:
Validate(data []byte) (any, []byte, error);BuildRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, facts app/report facts input, generated any) (any, error);- schema/template IDs needed by
internal/reporttemplate.
- Avoid importing
internal/appintointernal/generatedtext. If app-ownedReportFactsis currently needed, pass its constituentfacts.CollectedFactsandfacts.DerivedFacts. - Remove
validateGeneratedTextandbuildRenderContextswitches frominternal/appafter the catalog owns this dispatch. - Keep
internal/reporttemplate.Template,Schema, andRenderas small asset helpers unless the catalog cleanly replaces their maps. - If
reporttemplatekeeps template/schema maps, add tests tying those maps to the generated-text catalog. Prefer one source of truth if this is straightforward.
Files To Inspect
internal/app/app.gointernal/generatedtext/hourly.gointernal/generatedtext/tomorrow.gointernal/generatedtext/render_context.gointernal/reporttemplate/reporttemplate.gointernal/report/*_report.gointernal/report/definition.gointernal/reporttemplate/templates/*.md.tmplinternal/reporttemplate/schemas/*.schema.json
Acceptance Criteria
- Adding a new generated-text-template report requires registering one generated-text catalog entry and adding the report definition/assets, not editing app-level switches.
internal/appno longer switches on concrete generated-text schema IDs or template IDs.- Unsupported schema/template combinations return actionable errors that include the report ID and the unsupported ID.
- Existing hourly and tomorrow report behavior is unchanged.
- No generated-text schema, template, prompt ID, or report ID changes are introduced.
Tests
Add or update:
- Generated-text catalog completeness test proving every report with
GenerationGeneratedTextTemplatehas:- a generated-text catalog entry;
- an embedded schema asset;
- an embedded template asset;
- a validator;
- a render-context builder.
- Negative test for unsupported generated-text schema/template IDs.
- Existing hourly and tomorrow validation tests.
- Existing reporttemplate render tests.
- App workflow tests for hourly and tomorrow reports.
Validation commands:
go test ./internal/generatedtext ./internal/reporttemplate ./internal/report ./internal/app
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 2: Final Report Finalization Helper
Goal: centralize repeated metadata, output copy, notification, and result-population logic after a managed Markdown report exists.
Scope
Extract a narrow app helper that runs after either generation mode has produced the managed Markdown report file. This helper should not own Weather API fetching, facts, modules, Scriptorium calls, generated-text validation, or template rendering.
Implementation Guidance
- Add an unexported helper in
internal/app, for examplefinalizeRenderedReport. - Inputs should include the values already available in generation flow:
- context;
- loaded config;
- store;
- resolved report;
- RunID;
- metadata;
- managed report path;
- optional output path/output directory request;
- report result being assembled, or enough fields to return one.
- The helper should centralize:
- optional
--outcopy; - optional
--out-dircopy; - final metadata updates related to report paths/output copy/notification;
- final metadata save;
- distributor notification invocation when enabled;
- notification debug artifact save behavior already implemented;
- notification failure propagation.
- optional
- Preserve current ordering:
- managed report is written before finalization;
- metadata is saved after final report path is known;
- notification happens only after successful report generation and final metadata save.
- Preserve current behavior that distributor uses the managed report path, never optional output copies.
- Keep notification request construction in app unless a later stage creates a smaller named helper for template values.
Files To Inspect
internal/app/app.gointernal/state/metadata.gointernal/state/filesystem.gointernal/adapters/distributorinternal/fileutil- app tests covering generated reports, output copies, and notification artifacts.
Acceptance Criteria
- Direct Markdown report generation and generated-text-template generation both use the same finalization helper.
- Managed report paths, optional output copy behavior, metadata JSON shape, notification artifacts, and batch behavior are unchanged.
- Single-report notification failure still returns an error.
- Batch generation still continues independent reports and returns aggregate failure when any report fails.
- The helper is small enough to read without becoming a workflow engine.
Tests
Add or update:
- App test proving direct Markdown and generated-text-template reports both save final metadata consistently.
- App test proving
--outand--out-dirstill copy from the managed report path. - App test proving notification receives the managed report path.
- App test proving notification failure fails a single report.
- Batch test proving notification failure marks only that report failed and the batch exits nonzero.
Validation commands:
go test ./internal/app ./internal/state ./internal/adapters/distributor
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 3: Report Public Name And Alias Resolution
Goal: make internal/report the canonical source for public report names, config keys, aliases, and batch command membership.
Scope
Move report-name resolution policy out of scattered CLI/app/config helpers and into report-owned helpers.
Implementation Guidance
- Add report-owned helpers in
internal/report, for example:IDForCommandName(name string) (ID, error);IDForConfigKey(key string) (ID, error);BatchForCommandName(name string) ([]ID, error);CommandNames() []stringif helpful for CLI help tests.
- Preserve current accepted names and aliases:
- CLI report names currently accepted by
generate; - config aliases currently accepted by
reports.*; - batch names currently accepted by
run.
- CLI report names currently accepted by
- Keep user-facing error messages concise and actionable. It is acceptable for exact wording to change if tests are updated and the error remains clear.
- Update
internal/clito use report-owned command-name resolution or to call app request helpers that use it. - Update
internal/appto remove privatereportIDForCommandandreportBatchForCommandmappings if report-owned helpers can replace them. - Update
internal/configto remove privatereportIDForConfigKeyin favor of report-owned config-key resolution. - Avoid creating new exported app request types solely for this cleanup.
Files To Inspect
internal/report/definition.gointernal/report/registry.gointernal/report/period.gointernal/report/*_report.gointernal/cli/root.gointernal/app/app.gointernal/config/reports.godocs/cli.mddocs/config.md
Acceptance Criteria
- Report identity, command name, config key, and batch membership policy are discoverable from
internal/report. - CLI/app/config no longer maintain independent report ID switch statements for the same names.
- All existing public command names and config aliases continue to work unless explicitly documented as removed in this stage. This cleanup should not remove aliases.
- Distributor path variables that use report ID or artifact group are unchanged.
Tests
Add or update:
- Report tests for command-name-to-ID mapping.
- Report tests for config-key-to-ID mapping and aliases.
- Report tests for batch names and report order.
- CLI parser tests for every supported
generatereport name. - Config tests for report override aliases and unknown keys.
- App tests for run batch command mapping if not covered through CLI.
Validation commands:
go test ./internal/report ./internal/cli ./internal/config ./internal/app
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 4: Config Report Module Normalization
Goal: remove duplicate mutating normalization from config validation and make report module override extraction explicit and safe.
Scope
Separate load-time config mutation from validation. Ensure report module overrides cannot silently ignore invalid report keys in app-facing paths.
Implementation Guidance
- Keep configuration loading order:
- built-in defaults;
- config file;
- CLI overrides;
- report module normalization;
- secrets directory loading;
- validation.
Loadshould continue returning a fully normalized and validatedConfig.- Refactor
Validateso it does not perform duplicate mutating normalization. - Options:
- Preferred: split
normalizeReportModulesinto a mutating load-time normalizer and a non-mutating validator used byValidate. - Acceptable: make
Validaterequire normalized config and document/test that public callers should useLoadfor full processing.
- Preferred: split
- Do not remove
config.Validateunless all callers and tests can clearly useLoad. - Update
ReportModuleOverridesso invalid report keys are not silently ignored in app paths.- Preferred: change it to
ReportModuleOverrides() (map[report.ID][]module.ConfigItem, error)and update callers. - Acceptable: keep the signature only if
Configgains an internal validated/normalized marker and the method clearly cannot be reached with invalid keys.
- Preferred: change it to
- Preserve module option strict YAML decoding and composition validation.
- Preserve current config file syntax.
Files To Inspect
internal/config/load.gointernal/config/validate.gointernal/config/reports.gointernal/config/config.gointernal/config/config_test.gointernal/app/app.goexamples/config.ymldocs/config.md
Acceptance Criteria
- Report module normalization happens once on the normal
Loadpath. - Validation no longer performs duplicate mutating normalization.
- App report registry construction handles report override errors explicitly.
- Unknown report keys in report module overrides cannot be silently skipped.
- Existing valid example config still loads.
- Existing CLI overrides still take precedence over config and defaults.
Tests
Add or update:
- Config tests for normal load with typed module options.
- Config tests for direct validation of duplicate aliases and unknown report keys.
- Config tests proving
ReportModuleOverridesreturns or surfaces an error for invalid keys if called on invalid config. - App test proving report registry construction fails clearly on invalid overrides when supplied programmatically.
- Existing example config load test.
Validation commands:
go test ./internal/config ./internal/app ./internal/cli
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 5: Generated-Text Internal Helper Cleanup
Goal: reduce low-risk duplication inside internal/generatedtext after the catalog is in place.
Scope
Add small helpers for repeated snapshot stanza extraction and strict JSON validation mechanics while preserving report-specific context structs and validation rules.
Implementation Guidance
Snapshot lookup:
- Add an unexported snapshot lookup/cache type, for example
moduleSnapshotLookup. - It should wrap
module.Snapshotand provide typed methods for repeated module stanzas. - Keep report-specific context structs:
HourlyRenderContext;TomorrowRenderContext;- future report contexts.
- Do not replace typed context structs with a generic
map[string]any. - Preserve nil pointer behavior for omitted optional modules.
Strict JSON helper:
- Add a small helper for:
- single JSON object decode;
DisallowUnknownFields;- detection of trailing JSON tokens;
- canonical re-marshal of validated output.
- Keep report-specific required-field checks in
hourly.go,tomorrow.go, and future report validators. - Keep generated-text schema files unchanged unless tests show they are out of sync with validator behavior.
Files To Inspect
internal/generatedtext/render_context.gointernal/generatedtext/hourly.gointernal/generatedtext/tomorrow.gointernal/generatedtext/*_test.gointernal/moduleinternal/briefing
Acceptance Criteria
- Repeated optional stanza extraction is reduced without changing render-context JSON shape.
- Strict JSON decode behavior remains the same for hourly and tomorrow generated text.
- Unknown fields, missing required fields, trailing JSON, and invalid JSON errors remain actionable.
- Existing templates render unchanged.
Tests
Add or update:
- Generated-text validation tests for unknown fields, trailing JSON, missing required fields, and canonical output.
- Render-context tests proving key module pointers are populated when present and nil when omitted.
- Render-context tests proving extraction errors name the module ID.
Validation commands:
go test ./internal/generatedtext ./internal/reporttemplate ./internal/app
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 6: Weather API Source Lifecycle Helper
Goal: reduce repeated Weather API source fetch boilerplate while keeping source-specific semantics explicit.
Scope
Introduce a small helper or source spec for shared source lifecycle behavior in internal/adapters/weatherapi.
Implementation Guidance
- Centralize only common lifecycle mechanics:
- endpoint and query construction;
- envelope fetch;
- absent/malformed/null data handling according to source policy;
- source metadata construction;
- warning/error conversion;
- source hash handling.
- Keep source-specific decode functions explicit.
- Preserve these source-specific behaviors:
- hourly forecast remains required for normal report generation;
- alerts
data:nullmeans checked successfully with no active alerts; - optional non-alert source
data:nullfollows missing-source policy; - weather story uses its current endpoint/query behavior;
- SPC convective outlooks use the current endpoint constant and overlap filtering downstream;
- current source warning and provenance JSON shapes remain unchanged.
- Do not use reflection, generics-heavy helpers, or a broad source framework.
- If the helper makes the code less readable, stop and keep the explicit source functions.
Files To Inspect
internal/adapters/weatherapi/client.gointernal/adapters/weatherapi/*_test.gointernal/weatherdatainternal/forecastdocs/integrations/weatherapi.mddocs/internal/weather-data.md
Acceptance Criteria
- Source fetch functions are shorter but still readable and source-aware.
- Existing source warning behavior is unchanged.
- Existing source metadata fields and hashes are unchanged.
- No Weather API transport details leak outside
internal/adapters/weatherapi. - No prompt/data package schema changes are introduced.
Tests
Add or update:
- Fixture-server test proving all expected endpoints are still requested.
- Required hourly
data:nullfailure test. - Alerts
data:nullno-active-alerts test. - Optional non-alert missing-source policy tests for
error,warn, andnonewhere currently covered. - Weather story metadata test.
- SPC source test if current coverage depends on source count or warning count.
Validation commands:
go test ./internal/adapters/weatherapi ./internal/weatherdata ./internal/forecast
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt, but it should be skipped if the implementing agent cannot keep the helper narrow and explicit.
Stage 7: Package-Local Test Helper Cleanup
Goal: reduce noisy repeated test setup without creating a cross-package test framework.
Scope
Add package-local helpers only where tests already repeat substantial setup.
Implementation Guidance
- In
internal/apptests, consider helpers for:- temporary config/workspace construction;
- fake renderer setup;
- fake notifier setup;
- Weather API test server setup;
- artifact path/glob assertions.
- In
internal/clitests, consider a command invocation helper that captures stdout/stderr and exit status. - In
internal/adapters/weatherapitests, keep fixture server helpers local to the adapter package. - In
internal/adapters/distributortests, keep fake upload/status clients local to the adapter package. - Do not create
internal/testutilor a global test helper package in this stage. - Do not change test behavior or remove meaningful assertions while reducing setup.
Files To Inspect
internal/app/*_test.gointernal/cli/*_test.gointernal/adapters/weatherapi/*_test.gointernal/adapters/distributor/*_test.gointernal/generatedtext/*_test.gointernal/reporttemplate/*_test.go
Acceptance Criteria
- Test setup duplication is reduced in packages that have obvious repetition.
- Tests remain readable without hiding important workflow details.
- No production code changes are required for this stage unless an existing test-only seam is missing and justified.
- No global test helper package is introduced.
Tests
This stage changes tests only unless a small test seam is needed. Run:
go test ./internal/app ./internal/cli ./internal/adapters/weatherapi ./internal/adapters/distributor
go test ./...
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Stage 8: Documentation And Dead-Code Sweep
Goal: align implemented documentation with cleanup changes and remove stale symbols left behind by prior stages.
Scope
Update docs only for implemented cleanup changes. Remove stale code, stale tests, and stale roadmap references that no longer describe future work.
Implementation Guidance
- Update non-roadmap docs only where package contracts or contributor workflow changed:
docs/policy/development.mdif package responsibilities or validation commands changed;docs/internal/app-orchestration.mdif finalization ordering is documented;docs/internal/report-registry.mdif report name/catalog helpers are documented;docs/internal/generated-text.mdor equivalent if generated-text catalog behavior is documented;docs/internal/weather-data.mdif Weather API source lifecycle behavior changed.
- Do not document deferred or unimplemented cleanup outside
docs/roadmap/. - Keep
docs/roadmap/future.mdfor deferred ideas only if they remain future work. - Remove stale private helpers after all call sites are gone.
- Run stale-symbol searches for removed helpers and old dispatch names.
Files To Inspect
docs/policy/development.mddocs/internal/docs/config.mddocs/cli.mddocs/operations.mddocs/roadmap/- production code touched by earlier stages
Acceptance Criteria
- Non-roadmap docs describe implemented behavior only.
- No stale references remain to removed private app switches or old helper names.
- Roadmap docs do not duplicate implemented docs except to identify deferred future work.
go test ./..., CLI help, and whitespace checks pass.
Suggested Stale-Symbol Checks
Adjust names to match the actual implementation:
rg -n "validateGeneratedText|buildRenderContext|reportIDForCommand|reportBatchForCommand|reportIDForConfigKey" internal docs examples
rg -n "ReportModuleOverrides" internal docs examples
Validation commands:
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Prompt size: This stage is suitable for one implementation prompt.
Deferred Refactors
Do not include these in the cleanup sequence:
- Generic workflow engine.
- Plugin architecture.
- Cobra migration.
- Per-module or per-report Go packages.
- Manifest, checkpoint, resume, progress, or run-index system.
- Reflection-heavy Weather API ingestion framework.
- Global test helper package.
- Generic adapter error wrapper.
- Config-only report definitions.
- Broad state-storage redesign around workspace scanning.
These may be revisited only if concrete new requirements make the current explicit structure materially expensive.
Global Validation Checklist
Run after each implementation stage unless the stage documents a narrower test set:
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Additional checks after the full cleanup sequence:
go test ./internal/generatedtext ./internal/reporttemplate ./internal/app
go test ./internal/report ./internal/cli ./internal/config
go test ./internal/adapters/weatherapi ./internal/adapters/distributor ./internal/adapters/scriptorium
go test ./internal/briefing ./internal/module ./internal/facts ./internal/forecast ./internal/promptinput ./internal/changes ./internal/state
rg -n "validateGeneratedText|buildRenderContext|reportIDForCommand|reportBatchForCommand|reportIDForConfigKey" internal docs examples
Manual review items:
- Public CLI syntax remains stable.
- Managed workspace paths remain stable.
- Distributor bundle paths remain stable.
- Report IDs, prompt IDs, template IDs, and generated-text schema IDs remain stable unless a stage explicitly changed them.
- No secret values appear in errors, logs, metadata, notification artifacts, docs, examples, or tests.
- Non-roadmap documentation describes only implemented behavior.
Open Questions
No open questions block implementation of this cleanup roadmap.
The only discretionary item is Stage 6. Recommended approach: implement a narrow Weather API source lifecycle helper only if it remains explicit and improves readability. Viable alternative: skip Stage 6 and keep the current source-specific methods until the next Weather API source is added. The alternative is acceptable because current source functions are readable and tested, and the risk is mostly future maintenance cost rather than current behavior drift.