16 KiB
Batch Distributor Upload Implementation Roadmap
Purpose
Implement the batch distributor upload behavior defined in
docs/roadmap/batch-distributor.md.
This plan is written for an LLM coding agent. Implement the stages in order. Preserve existing public CLI syntax, report generation behavior, managed report paths, and single-report distributor notification behavior unless a stage explicitly changes batch notification behavior.
Source Roadmap
Use docs/roadmap/batch-distributor.md as the authoritative source for:
- user intent;
- locked policy decisions;
- target batch JSON shape;
- batch notification artifact layout;
- configuration shape;
- app, state, and adapter boundaries;
- deferred work.
Do not move aspirational batch distributor behavior into non-roadmap docs until the corresponding code is implemented.
Locked Decisions
generate <report>keeps per-report distributor notification.run morningandrun eveninguse one batch-level distributor upload when distributor notification is enabled and batch notification is enabled.notify.distributor.batch.enabled=falsedisables distributor notification for run commands; it does not fall back to per-report batch uploads.- Batch upload is all-or-nothing: if any report fails, skip the distributor upload for the whole batch.
- A batch notification failure makes the batch command return a nonzero aggregate error, but it does not mark individual report generation as failed.
- The initial implementation uses one distributor pipeline for the whole batch.
- The batch upload source files are managed Markdown report paths only.
--out-dircopies are never uploaded. - Reuse the existing distributor adapter; do not import distributor package
types outside
internal/adapters/distributor. - Do not introduce batch manifest/resume behavior, distributor-specific CLI flags, a workflow engine, plugin system, or broad CLI redesign.
Stage 1: Batch Notify Config And Template Rendering
Goal: add batch-specific distributor notification configuration and rendering without changing runtime behavior yet.
Implementation guidance:
- Add a nested batch config under
notify.distributor, for example:type DistributorBatchNotifyConfig struct { Enabled bool `yaml:"enabled"` PipelineIDTemplate string `yaml:"pipeline_id_template"` BundleIDTemplate string `yaml:"bundle_id_template"` IdempotencyKeyTemplate string `yaml:"idempotency_key_template"` } - Add it to
config.DistributorNotifyConfigasBatch. - Defaults:
batch.enabled: truebatch.pipeline_id_template: "weatherreporter"batch.bundle_id_template: "weatherreporter.{location_id}.{batch}"batch.idempotency_key_template: "{bundle_id}.{batch_run_id}"
- Preserve existing report-level distributor defaults and validation.
- When
notify.distributor.enabled=false, batch values may remain defaulted but must not require endpoint/token availability. - When
notify.distributor.enabled=trueandbatch.enabled=true, validate:- non-empty batch pipeline template;
- non-empty batch bundle template;
- non-empty batch idempotency template;
- known template variables only;
- rendered values are non-empty for a representative validation input.
- Supported batch template variables:
location_idbatchbatch_run_idbatch_started_datebundle_idfor idempotency templates only
- Add config rendering helpers, for example:
RenderDistributorBatchBundleIDRenderDistributorBatchPipelineIDRenderDistributorBatchIdempotencyKey
- Keep report-level render helpers unchanged.
- Update
examples/config.ymlonly after the config fields are implemented.
Tests:
- Defaults include enabled batch config and expected templates.
- Disabled distributor config accepts default batch settings.
- Enabled distributor config rejects malformed batch templates.
- Unknown batch template variables are rejected.
- Batch idempotency templates can reference
{bundle_id}. - Example config loads and includes the batch block if updated in this stage.
Run:
go test ./internal/config
Stage 2: Batch Notification State Artifacts
Goal: add batch-level notification artifact paths and persistence.
Implementation guidance:
- Add a state-owned batch notification schema version, for example:
weatherreporter.batch_distributor_notification.v1. - Add a
BatchDistributorNotificationArtifacttype with fields matchingdocs/roadmap/batch-distributor.md:- schema version;
- batch kind;
- batch run ID;
- attempted time;
- endpoint;
- pipeline ID;
- bundle ID;
- idempotency key;
- bundle created timestamp;
- included report records;
- status;
- upload result;
- run status;
- status error;
- error.
- Add included report record fields:
- report ID;
- RunID;
- source path;
- bundle paths.
- Add state path support for:
notifications/batches/<batch>/<local-date>/<batch_run_id>.distributor.json - Use the batch start date in the effective Weather API/report timezone for the local date directory.
- Add a save method such as
SaveBatchDistributorNotification. - Use existing atomic JSON write helpers.
- Do not change existing per-report metadata JSON shape in this stage.
Tests:
- Batch notification paths use the configured workspace root.
- Morning and evening paths include batch kind, local date, and batch run ID.
- Saved artifact round-trips with included reports, upload result, run status, raw report JSON, and error fields.
- Empty batch run ID or batch kind fails with actionable errors.
Run:
go test ./internal/state
Stage 3: Batch Notification App Types And Identity
Goal: add app-owned batch notification request/result types and deterministic batch run ID helpers.
Implementation guidance:
- Add app-level types, for example:
type BatchNotificationResult struct { Status string Reason string RunID string PipelineID string BundleID string IdempotencyKey string Path string IncludedReports []BatchNotificationReport Error string } - Add JSON tags matching the target
BatchResult.notificationshape. - Add
Notification *BatchNotificationResulttoBatchResultwithjson:"notification,omitempty". - Add
BatchNotificationReportwith report ID, RunID, source path, and bundle paths. - Add an unexported batch run ID helper:
using the same UTC nanosecond timestamp layout as report RunIDs.
<started_at_utc_timestamp>_<batch> - Add unexported batch distributor template values and rendering glue in
internal/appor call the newinternal/confighelpers directly with a narrow value struct. - Do not call distributor in this stage.
Tests:
- Batch run ID for morning/evening uses UTC timestamp plus batch name.
- Batch result JSON omits
notificationwhen nil. - Batch result JSON includes notification fields when populated.
Run:
go test ./internal/app
Stage 4: Batch Bundle Request Construction
Goal: build a validated multi-file distributor notification request from a successful batch result.
Implementation guidance:
- Add an unexported app helper that accepts:
- config;
- batch kind;
- batch run ID;
- batch started time;
- successful batch report results;
- the matching resolved/planned reports if needed for valid-period template values.
- Render batch
pipeline_id,bundle_id, andidempotency_keyfromnotify.distributor.batch. - For each included report, render existing report-level
report_path_templatesusing that report's normal distributor template values. - Build one app notification request with multiple file mappings.
- If the current
NotificationRequestis too report-specific, introduce a separate unexported batch request type and convert it todistributoradapter.UploadRequestat the notifier boundary. - Validate before upload:
- no reports included;
- missing source path;
- missing or invalid bundle path;
- duplicate rendered bundle paths across the batch;
- missing batch pipeline, bundle ID, or idempotency key.
- Error context should include report ID, RunID, source path, and bundle path where useful.
- Do not include token values in errors.
Tests:
- Evening batch with Tomorrow plus two Daily reports renders one request with all managed report paths.
- Every included report uses its own normal rendered report path templates.
- Duplicate bundle paths fail before upload.
- Missing report path fails before upload.
- Batch ID, bundle ID, pipeline ID, and idempotency key match configured templates.
Run:
go test ./internal/app ./internal/config
Stage 5: Suppress Per-Report Batch Notification
Goal: make run commands stop notifying once per report while preserving single-report notification.
Implementation guidance:
- Add an explicit app-layer mechanism to suppress per-report notification when
GenerateReportis called fromRunBatchDetailed. - Prefer a private field on
ReportRequestor a private generation helper over changing public CLI behavior. - Ensure
GenerateReportused bygenerate <report>still notifies exactly as it does today. - Suppress per-report notification for all
run morningandrun eveningreport generation, regardless of whether batch notification is enabled, disabled, skipped, or fails later. - Ensure report metadata and report artifacts remain saved normally when per-report notification is suppressed.
- Do not remove the existing per-report notification implementation because single-report generation still uses it.
Tests:
generate tomorrowwith distributor enabled calls the notifier once.run eveningwith distributor enabled does not call the notifier once per generated report before the batch notification stage is wired in.- Per-report notification artifacts are not written for batch-generated reports when batch notification is enabled.
- Existing single-report notification artifact tests still pass.
Run:
go test ./internal/app ./internal/cli
Stage 6: Batch Notification Orchestration
Goal: upload one distributor bundle after a fully successful batch and persist a batch notification debug artifact.
Implementation guidance:
- In
RunBatchDetailed, keep current report generation behavior:- collect once;
- plan reports;
- continue generating later reports after individual failures;
- record per-report success/failure.
- After report generation finishes:
- if distributor is disabled, leave
BatchResult.Notificationnil; - if distributor is enabled but
notify.distributor.batch.enabled=false, set batch notification status to disabled or omit the notification object, and do not call distributor for the batch; - if any report failed, set
BatchResult.Notificationto statusskippedwith reasonone or more reports failed, and do not call distributor; - if all reports succeeded, build and send one batch notification request.
- if distributor is disabled, leave
- Use the existing distributor adapter with multiple files.
- Save the batch notification artifact for attempted, succeeded, failed, and status-error outcomes.
- Persist distributor accepted run ID, upload status, terminal run status, raw run report JSON, status lookup error, and redacted failure error.
- Batch notification failure should:
- set top-level batch notification status to
failed; - increment or otherwise reflect aggregate batch failure consistently with
existing
BatchErrorbehavior; - not mark individual report generation items as failed;
- return
BatchErrorfromRunBatchand an error from CLI execution.
- set top-level batch notification status to
Tests:
- All-success morning batch calls notifier exactly once with all report files.
- All-success evening batch calls notifier exactly once with all report files.
- Report failure skips notification and sets top-level notification status
skipped. - Batch notification upload failure returns aggregate batch error and keeps report item statuses succeeded.
- Batch notification run-status failure records raw status report JSON in the debug artifact.
- Disabled distributor produces no notification object and no notifier call.
- Batch notification disabled produces no batch notifier call and no per-report notifier calls from run commands.
Run:
go test ./internal/app ./internal/state ./internal/adapters/distributor
Stage 7: CLI JSON And Stderr Output
Goal: expose one batch notification result clearly without repeating it on every report item.
Implementation guidance:
- Extend CLI JSON output naturally through
BatchResult.Notification. - Update stderr batch logging:
- keep existing compact per-report lines;
- add one
batchNotification ...line when notification is attempted, skipped, or failed; - do not repeat the same batch notification error on every report line.
- Keep command syntax unchanged.
- Do not add distributor-specific CLI flags.
- Ensure secret values and bearer tokens cannot appear in JSON or stderr.
Tests:
- Batch JSON includes top-level notification fields for success.
- Batch JSON includes top-level skipped notification when a report failed.
- Batch JSON omits notification when distributor is disabled.
- Stderr includes one batch notification line on success/failure/skipped.
- Stderr does not repeat batch notification errors per report.
- Existing help output remains unchanged.
Run:
go test ./internal/cli ./internal/app
go run ./cmd/weatherreporter --help
Stage 8: Documentation And Examples
Goal: move implemented behavior out of roadmap-only status after the code is in place.
Implementation guidance:
- Update
docs/config.mdwith:notify.distributor.batch.enabled;- batch pipeline, bundle ID, and idempotency templates;
- supported batch template variables;
- relationship between report path templates and batch bundle file mappings.
- Update
examples/config.ymlwith the batch block and no secrets. - Update
docs/operations.mdwith:- batch upload ordering;
- skip-all notification policy;
- batch artifact path;
- top-level batch notification JSON behavior.
- Update
docs/troubleshooting.mdwith:- skipped batch upload;
- batch upload failure;
- duplicate bundle path validation;
- distributor source conflicts.
- Update
docs/internal/app-orchestration.mdwith batch notification workflow. - Update
docs/internal/distributor-adapter.mdto clarify that multi-file upload is supported by the adapter and batch orchestration lives in app. - Update
docs/internal/state.mdwith batch notification artifact paths and JSON shape. - Keep future extensions only under
docs/roadmap/.
Tests/checks:
- Config examples load.
- Non-roadmap docs describe only implemented behavior.
Run:
go test ./internal/config
git diff --check
Stage 9: Final Validation
Run the full validation set:
go test ./internal/app ./internal/config ./internal/state ./internal/cli ./internal/adapters/distributor
go test ./...
go run ./cmd/weatherreporter --help
git diff --check
Manual checks:
weatherreporter generate tomorrowstill uses per-report notification.weatherreporter run morningsends at most one distributor upload.weatherreporter run eveningsends at most one distributor upload.- A failed report in a batch skips the batch upload.
- Batch upload source paths are managed report paths, not
--out-dircopies. - Batch upload bundle paths are unique.
- No token values appear in errors, batch JSON, notification artifacts, docs, or examples.
Deferred Work
- Batch-level durable resume or retry queues.
- Batch-level distributor status inspection command.
- Multiple distributor pipelines within one batch.
- Report-type-specific distributor routing inside batch uploads.
- Uploading data packages, metadata, render contexts, or notification artifacts in the batch bundle.
- Distributor-specific CLI flags.
- Batch manifest or progress system.
- Changing distributor destination merge semantics from weatherreporter.
Open Questions
None. The roadmap decisions are sufficient for implementation:
- failed report means skip all batch upload;
- initial batch upload uses one pipeline;
- batch debug artifacts live under
notifications/batches/...; - batch JSON uses one top-level
notificationobject; - single-report generation keeps per-report notification.