Files
weatherreporter/docs/internal/state.md

113 lines
5.7 KiB
Markdown

# State Internals
The `internal/state` package owns filesystem-backed run state: safe path
derivation, metadata persistence, prior-report lookup, and read-only report
inspection. It does not decide which reports to generate or deliver. For the
operator-facing layout and retention procedures, see the
[operations guide](../operations.md).
## Store construction and artifact paths
`NewFilesystemStore` requires a workspace root and rejects absolute or
escaping values for every configured state directory. `Paths` then validates a
run ID and artifact group before deriving all paths from the report's valid
start date (`YYYY-MM-DD`). This keeps a run's artifacts together while making
the paths safe to use below the configured workspace.
| Artifact | Derived location |
| --- | --- |
| Module snapshot | `snapshots/<group>/<date>/modules.<run-id>.json` |
| Metadata | `snapshots/<group>/<date>/metadata.<run-id>.json` |
| Data package | `data-packages/<group>/<date>/data_package.<run-id>.yaml` |
| Prompt preparation | `preflight/<group>/<date>/prompt_preparation.<run-id>.json` |
| Prompt execution | `snapshots/<group>/<date>/prompt_execution.<run-id>.json` |
| Render preflight | `preflight/<group>/<date>/render.<run-id>.json` |
| Notification record | `notifications/<group>/<date>/distributor.<run-id>.json` |
| Managed report | `reports/<group>/<date>/report.<run-id>.md` |
| Generated text | `snapshots/<group>/<date>/generated_text.<run-id>.json` |
| Generated-text source and result | `snapshots/<group>/<date>/generated_text_raw.<run-id>.json` and `generated_text_result.<run-id>.json` |
| Generated-text render context | `snapshots/<group>/<date>/render_context.<run-id>.json` |
The configured notification root separates notification artifacts from report
artifacts; single-report notification paths use the report's valid date. Report
producers create parent directories as needed and write the report body; state
is responsible for the surrounding paths and saved run artifacts.
Batch Distributor notifications are derived separately as
`notifications/batches/<batch>/<local-date>/distributor.<batch-run-id>.json`.
Their date is calculated from the batch start in its configured location, and
the batch identity and run ID receive the same path-segment validation as
single-report artifact identifiers.
## Metadata and durable writes
`Metadata` is the durable inventory for a run. It records its schema version,
run identity, generated and valid timestamps, artifact group, source content
and provenance, and the module snapshot, data-package, prompt preparation,
prompt execution, report, generated-artifact, and notification locations when
present. New prompt records use `weatherreporter.metadata.v2`; historic
`weatherreporter.metadata.v1` records remain readable and retain their legacy
JSON field names when inspected.
`BuildMetadataFromBriefingMetadata` establishes legacy common fields, while
`BuildPromptMetadataFromBriefingMetadata` establishes the V2 record. The
application adds locations only after the corresponding artifacts are
produced. `SaveMetadata` requires the run ID, module snapshot, data package,
metadata path, and the matching preparation reference for its schema. The
package also saves module snapshots, data packages, prompt preparation and
execution records, legacy preflight records, generated-text artifacts, render
contexts, and notifications. JSON writes use atomic replacement, so readers do
not observe a partially written state file.
## Explicit prompt debug storage
`PromptDebugWriter` is a separate, opt-in boundary for content-rich prompt
diagnostics. It is constructed with an explicit absolute operator root, rather
than a workspace-derived path. A blank root produces a disabled writer that
does not access the filesystem.
Enabled debug captures are grouped as
`<root>/<report-id>/<valid-date>/<run-id>/` and contain `preparation.json` and
`execution.json`. The writer rejects symlinks, unsafe path segments, path
escape, and non-directory roots; it creates its directories with `0700` and
writes files atomically with `0600`. Normal state discovery and inspection do
not read this root. Its wire records map only approved project-owned fields;
credentials and dependency objects are not persisted.
The data package itself follows the shared
[prompt-input contract](prompt-input.md). Report text, templates, and external
delivery payloads remain owned by their respective packages and integration
references.
## Prior reports and inspection
`FindPriorSnapshot` searches metadata rather than guessing from filenames. It
only considers an earlier compatible report in the same artifact group and
supports the comparison strategies defined by the report request:
- `same_valid_date` finds an earlier generated report for the same valid day.
The newest eligible metadata record wins; the current run is excluded.
Unreadable or malformed candidate metadata is ignored so a damaged historical
record does not block a new run.
`ListReports` walks saved metadata, returns results ordered newest-first by
generation time, and treats a missing snapshots directory as an empty history.
`LoadMetadataByRunID` builds on that inspection path. These APIs are read-only;
repairing or pruning stored state is an operational concern.
## Boundaries and verification
The package rejects unsafe path components and incomplete metadata before
writing. Callers must provide a valid report request, artifact group, and
store configuration. Its focused tests cover path derivation, atomic
persistence, metadata validation, comparison eligibility, and report listing:
```sh
go test ./internal/state
```
See [application orchestration](app-orchestration.md) for the order in which
these artifacts are created and [report templates](../templates.md) for the
user-facing report contract.