Persist module snapshots for generated reports

This commit is contained in:
2026-06-09 20:58:35 +00:00
parent 40639309b1
commit 2483c2362d
11 changed files with 236 additions and 82 deletions

View File

@@ -28,16 +28,16 @@ weatherreporter run morning [--config PATH] [--units VALUE] [--tz NAME] [--out-d
weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH]
weatherreporter inspect reports [--config PATH] [--limit N] weatherreporter inspect reports [--config PATH] [--limit N]
weatherreporter inspect metadata [--config PATH] RUN_ID weatherreporter inspect metadata [--config PATH] RUN_ID
weatherreporter inspect briefing [--config PATH] RUN_ID weatherreporter inspect modules [--config PATH] RUN_ID
weatherreporter inspect data-package [--config PATH] RUN_ID weatherreporter inspect data-package [--config PATH] RUN_ID
weatherreporter inspect prior [--config PATH] RUN_ID weatherreporter inspect prior [--config PATH] RUN_ID
weatherreporter inspect sources [--config PATH] RUN_ID weatherreporter inspect sources [--config PATH] RUN_ID
``` ```
`generate` commands write briefing, data package, preflight, report, and `generate` commands write briefing, module snapshot, data package, preflight,
metadata artifacts under the configured workspace. `--out` writes an extra report, and metadata artifacts under the configured workspace. `--out` writes
Markdown copy for the operator; distributor notification uses the managed an extra Markdown copy for the operator; distributor notification uses the
report path, not the extra copy. `generate storm` requires explicit managed report path, not the extra copy. `generate storm` requires explicit
event-window bounds with `--start` and `--end`. event-window bounds with `--start` and `--end`.
`run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook `run morning` generates Daily Today and the 3-Day Outlook, plus Weekend Outlook
@@ -88,14 +88,15 @@ weatherreporter run evening --out-dir ./reports
```sh ```sh
weatherreporter inspect reports --limit 10 weatherreporter inspect reports --limit 10
weatherreporter inspect metadata 20260529T100000.000000000Z_daily_today weatherreporter inspect metadata 20260529T100000.000000000Z_daily_today
weatherreporter inspect briefing 20260529T100000.000000000Z_daily_today weatherreporter inspect modules 20260529T100000.000000000Z_daily_today
weatherreporter inspect data-package 20260529T100000.000000000Z_daily_today weatherreporter inspect data-package 20260529T100000.000000000Z_daily_today
weatherreporter inspect prior 20260529T100000.000000000Z_daily_today weatherreporter inspect prior 20260529T100000.000000000Z_daily_today
weatherreporter inspect sources 20260529T100000.000000000Z_daily_today weatherreporter inspect sources 20260529T100000.000000000Z_daily_today
``` ```
`inspect reports` lists recent generated runs with artifact paths and source `inspect reports` lists recent generated runs with artifact paths and source
warning counts. The other inspect commands require a RunID. `inspect prior` warning counts. The other inspect commands require a RunID. `inspect modules`
returns the persisted ordered module snapshot for a run. `inspect prior`
returns the prior comparable snapshot metadata selected from stored metadata, or returns the prior comparable snapshot metadata selected from stored metadata, or
`null` when none exists. `inspect sources` shows source provenance and source `null` when none exists. `inspect sources` shows source provenance and source
warnings without dumping full weather payloads. warnings without dumping full weather payloads.

View File

@@ -52,18 +52,22 @@ workspace/
daily/ daily/
YYYY-MM-DD/ YYYY-MM-DD/
<run_id>.briefing.json <run_id>.briefing.json
<run_id>.modules.json
<run_id>.metadata.json <run_id>.metadata.json
three-day/ three-day/
YYYY-MM-DD/ YYYY-MM-DD/
<run_id>.briefing.json <run_id>.briefing.json
<run_id>.modules.json
<run_id>.metadata.json <run_id>.metadata.json
weekend/ weekend/
YYYY-MM-DD/ YYYY-MM-DD/
<run_id>.briefing.json <run_id>.briefing.json
<run_id>.modules.json
<run_id>.metadata.json <run_id>.metadata.json
storm/ storm/
YYYY-MM-DD/ YYYY-MM-DD/
<run_id>.briefing.json <run_id>.briefing.json
<run_id>.modules.json
<run_id>.metadata.json <run_id>.metadata.json
data-packages/ data-packages/
daily/ daily/
@@ -164,11 +168,11 @@ report generation has a distinct retry identity. The default bundle path uses
the valid-period start date, artifact group, and RunID. Distributor owns the valid-period start date, artifact group, and RunID. Distributor owns
destination merge, retention, and derived snapshot behavior such as `latest`. destination merge, retention, and derived snapshot behavior such as `latest`.
Notification happens after final metadata save. Weather API, briefing, Notification happens after final metadata save. Weather API, briefing, module
data-package, render preflight, Scriptorium run, and metadata-save failures do snapshot, data-package, render preflight, Scriptorium run, and metadata-save
not trigger notification. A notification failure fails that report. In a batch, failures do not trigger notification. A notification failure fails that report.
other reports continue, the failed report includes notification fields in the In a batch, other reports continue, the failed report includes notification
JSON summary, and the batch returns nonzero. fields in the JSON summary, and the batch returns nonzero.
Each notification attempt writes a debug artifact under `notifications/`. The Each notification attempt writes a debug artifact under `notifications/`. The
artifact records the rendered pipeline ID, bundle ID, idempotency key, managed artifact records the rendered pipeline ID, bundle ID, idempotency key, managed
@@ -195,7 +199,7 @@ They do not fetch weather data or run `scriptorium`.
```text ```text
weatherreporter inspect reports --limit 10 weatherreporter inspect reports --limit 10
weatherreporter inspect metadata RUN_ID weatherreporter inspect metadata RUN_ID
weatherreporter inspect briefing RUN_ID weatherreporter inspect modules RUN_ID
weatherreporter inspect data-package RUN_ID weatherreporter inspect data-package RUN_ID
weatherreporter inspect prior RUN_ID weatherreporter inspect prior RUN_ID
weatherreporter inspect sources RUN_ID weatherreporter inspect sources RUN_ID
@@ -203,11 +207,11 @@ weatherreporter inspect sources RUN_ID
Use `inspect reports` to find recent RunIDs and artifact paths. Use Use `inspect reports` to find recent RunIDs and artifact paths. Use
`inspect metadata` to see the artifact links recorded for a run. Use `inspect metadata` to see the artifact links recorded for a run. Use
`inspect briefing` and `inspect data-package` to review the exact structured `inspect modules` to review the persisted ordered module snapshot, and
inputs used for rendering. Use `inspect prior` to see the prior comparable `inspect data-package` to review the current structured prompt package used for
snapshot selected for Recent Changes, or `null` when none exists. Use rendering. Use `inspect prior` to see the prior comparable snapshot selected
`inspect sources` to review source provenance and warnings without dumping full for Recent Changes, or `null` when none exists. Use `inspect sources` to review
weather payloads. source provenance and warnings without dumping full weather payloads.
## Recent Changes ## Recent Changes
@@ -241,7 +245,7 @@ For a bad report, start with:
```text ```text
weatherreporter inspect metadata RUN_ID weatherreporter inspect metadata RUN_ID
weatherreporter inspect sources RUN_ID weatherreporter inspect sources RUN_ID
weatherreporter inspect briefing RUN_ID weatherreporter inspect modules RUN_ID
weatherreporter inspect data-package RUN_ID weatherreporter inspect data-package RUN_ID
weatherreporter inspect prior RUN_ID weatherreporter inspect prior RUN_ID
``` ```

View File

@@ -17,6 +17,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/facts" "gitea.maximumdirect.net/eric/weatherreporter/internal/facts"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/forecast" "gitea.maximumdirect.net/eric/weatherreporter/internal/forecast"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state" "gitea.maximumdirect.net/eric/weatherreporter/internal/state"
@@ -73,6 +74,11 @@ type BriefingRequest struct {
OutputPath string OutputPath string
} }
type ModuleSnapshotRequest struct {
Config config.Config
Resolved report.Resolved
}
type ReportRequest struct { type ReportRequest struct {
Config config.Config Config config.Config
Resolved report.Resolved Resolved report.Resolved
@@ -88,21 +94,23 @@ type BriefingResult struct {
} }
type ReportResult struct { type ReportResult struct {
Briefing briefing.Package Briefing briefing.Package
BriefingPath string ModuleSnapshot module.Snapshot
DataPackage promptinput.Package BriefingPath string
DataPackagePath string ModuleSnapshotPath string
PreflightPath string DataPackage promptinput.Package
ReportPath string DataPackagePath string
OutputPath string PreflightPath string
NotificationPath string ReportPath string
Metadata state.Metadata OutputPath string
MetadataPath string NotificationPath string
PriorSnapshot *state.PriorSnapshot Metadata state.Metadata
RecentChanges []changes.Change MetadataPath string
RenderResult *scriptorium.RenderResult PriorSnapshot *state.PriorSnapshot
RunResult *scriptorium.RunResult RecentChanges []changes.Change
Notification *NotificationResult RenderResult *scriptorium.RenderResult
RunResult *scriptorium.RunResult
Notification *NotificationResult
} }
type BatchResult struct { type BatchResult struct {
@@ -493,11 +501,22 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
if err != nil { if err != nil {
return nil, err return nil, err
} }
moduleSnapshot, err := BuildModuleSnapshot(ModuleSnapshotRequest{
Config: req.Config,
Resolved: req.Resolved,
}, bundle)
if err != nil {
return nil, err
}
briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage) briefingPath, err := store.SaveBriefing(ctx, req.Resolved, briefingPackage)
if err != nil { if err != nil {
return nil, err return nil, err
} }
moduleSnapshotPath, err := store.SaveModuleSnapshot(ctx, req.Resolved, moduleSnapshot)
if err != nil {
return nil, err
}
recentChanges, err := recentChanges(ctx, store, priorSnapshot, briefingPackage, req.Config.RecentChange) recentChanges, err := recentChanges(ctx, store, priorSnapshot, briefingPackage, req.Config.RecentChange)
if err != nil { if err != nil {
@@ -537,6 +556,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
} }
metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{ metadata := state.BuildMetadata(req.Resolved, briefingPackage, state.ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: dataPackagePath, DataPackage: dataPackagePath,
Preflight: preflightPath, Preflight: preflightPath,
@@ -590,21 +610,23 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
} }
return &ReportResult{ return &ReportResult{
Briefing: briefingPackage, Briefing: briefingPackage,
BriefingPath: briefingPath, ModuleSnapshot: moduleSnapshot,
DataPackage: dataPackage, BriefingPath: briefingPath,
DataPackagePath: dataPackagePath, ModuleSnapshotPath: moduleSnapshotPath,
PreflightPath: preflightPath, DataPackage: dataPackage,
ReportPath: reportPath, DataPackagePath: dataPackagePath,
OutputPath: outputPath, PreflightPath: preflightPath,
NotificationPath: notificationPath, ReportPath: reportPath,
Metadata: metadata, OutputPath: outputPath,
MetadataPath: metadataPath, NotificationPath: notificationPath,
PriorSnapshot: priorSnapshot, Metadata: metadata,
RecentChanges: recentChanges, MetadataPath: metadataPath,
RenderResult: renderResult, PriorSnapshot: priorSnapshot,
RunResult: runResult, RecentChanges: recentChanges,
Notification: notification, RenderResult: renderResult,
RunResult: runResult,
Notification: notification,
}, nil }, nil
} }
@@ -807,20 +829,7 @@ func distributorUploadFiles(sourcePath string, bundlePaths []string) []distribut
func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) { func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Package, error) {
collected := facts.BuildCollected(bundle) collected := facts.BuildCollected(bundle)
dayparts := make([]forecast.DaypartDefinition, 0, len(req.Config.Dayparts)) derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
for _, daypart := range req.Config.Dayparts {
dayparts = append(dayparts, forecast.DaypartDefinition{
Name: daypart.Name,
Start: daypart.Start,
End: daypart.End,
})
}
derived, err := facts.BuildDerived(facts.BuildDerivedRequest{
Resolved: req.Resolved,
Timezone: req.Config.WeatherAPI.Timezone,
Dayparts: dayparts,
Collected: collected,
})
if err != nil { if err != nil {
return briefing.Package{}, err return briefing.Package{}, err
} }
@@ -846,6 +855,62 @@ func BuildBriefing(req BriefingRequest, bundle *weatherdata.Bundle) (briefing.Pa
} }
} }
func BuildModuleSnapshot(req ModuleSnapshotRequest, bundle *weatherdata.Bundle) (module.Snapshot, error) {
collected := facts.BuildCollected(bundle)
derived, err := buildDerivedFacts(req.Config, req.Resolved, collected)
if err != nil {
return module.Snapshot{}, err
}
registry, err := briefing.DefaultModuleRegistry()
if err != nil {
return module.Snapshot{}, err
}
moduleContext := briefing.ModuleContext{
Resolved: req.Resolved,
Collected: collected,
Derived: derived,
Units: req.Config.WeatherAPI.Units,
Timezone: req.Config.WeatherAPI.Timezone,
Location: briefingLocation(req.Config),
}
var outputs []module.Output
for _, item := range req.Resolved.Definition.Modules {
definition, err := registry.Lookup(item.ID)
if err != nil {
return module.Snapshot{}, err
}
if definition.Builder == nil {
continue
}
output, err := registry.BuildModule(moduleContext, item)
if err != nil {
return module.Snapshot{}, err
}
if output == nil {
continue
}
outputs = append(outputs, *output)
}
return module.NewSnapshot(outputs)
}
func buildDerivedFacts(cfg config.Config, resolved report.Resolved, collected facts.CollectedFacts) (facts.DerivedFacts, error) {
dayparts := make([]forecast.DaypartDefinition, 0, len(cfg.Dayparts))
for _, daypart := range cfg.Dayparts {
dayparts = append(dayparts, forecast.DaypartDefinition{
Name: daypart.Name,
Start: daypart.Start,
End: daypart.End,
})
}
return facts.BuildDerived(facts.BuildDerivedRequest{
Resolved: resolved,
Timezone: cfg.WeatherAPI.Timezone,
Dayparts: dayparts,
Collected: collected,
})
}
func briefingLocation(cfg config.Config) *briefing.LocationContext { func briefingLocation(cfg config.Config) *briefing.LocationContext {
location := briefing.LocationContext{ location := briefing.LocationContext{
ID: cfg.Location.ID, ID: cfg.Location.ID,

View File

@@ -194,7 +194,14 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if renderer.runRequest.OutputPath != result.ReportPath { if renderer.runRequest.OutputPath != result.ReportPath {
t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath) t.Fatalf("run OutputPath = %q, want managed report path %q", renderer.runRequest.OutputPath, result.ReportPath)
} }
assertPathsExist(t, result.BriefingPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath) assertPathsExist(t, result.BriefingPath, result.ModuleSnapshotPath, result.DataPackagePath, result.PreflightPath, result.ReportPath, result.MetadataPath, outputPath)
snapshotData, err := os.ReadFile(result.ModuleSnapshotPath)
if err != nil {
t.Fatalf("read module snapshot: %v", err)
}
if !strings.Contains(string(snapshotData), module.SnapshotSchemaVersion) || !strings.Contains(string(snapshotData), `"metadata"`) || !strings.Contains(string(snapshotData), `"derived_daily_summary"`) {
t.Fatalf("module snapshot missing expected stanzas:\n%s", string(snapshotData))
}
data, err := os.ReadFile(result.DataPackagePath) data, err := os.ReadFile(result.DataPackagePath)
if err != nil { if err != nil {
t.Fatalf("read data package: %v", err) t.Fatalf("read data package: %v", err)
@@ -238,7 +245,7 @@ func TestGenerateReportWritesReportAndPreflight(t *testing.T) {
if result.Metadata.RunID != resolved.Metadata().RunID { if result.Metadata.RunID != resolved.Metadata().RunID {
t.Fatalf("metadata RunID = %q, want %q", result.Metadata.RunID, resolved.Metadata().RunID) t.Fatalf("metadata RunID = %q, want %q", result.Metadata.RunID, resolved.Metadata().RunID)
} }
if result.Metadata.BriefingPath != result.BriefingPath || result.Metadata.DataPackagePath != result.DataPackagePath { if result.Metadata.BriefingPath != result.BriefingPath || result.Metadata.ModuleSnapshotPath != result.ModuleSnapshotPath || result.Metadata.DataPackagePath != result.DataPackagePath {
t.Fatalf("metadata does not link artifact paths: %#v", result.Metadata) t.Fatalf("metadata does not link artifact paths: %#v", result.Metadata)
} }
if result.Metadata.RenderedReportPath != result.ReportPath { if result.Metadata.RenderedReportPath != result.ReportPath {
@@ -647,6 +654,7 @@ func TestGenerateReportIncludesRecentChangesFromPriorSnapshot(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath, Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata, Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage, DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight, Preflight: priorPaths.Preflight,
@@ -762,6 +770,7 @@ func TestTomorrowReportCanCompareAgainstPriorDailySnapshot(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath, Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata, Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage, DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight, Preflight: priorPaths.Preflight,
@@ -829,6 +838,7 @@ func TestGenerateThreeDayReportWritesReportAndRecentChanges(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath, Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata, Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage, DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight, Preflight: priorPaths.Preflight,
@@ -905,6 +915,7 @@ func TestGenerateWeekendReportWritesReportAndRecentChanges(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), state.BuildMetadata(priorResolved, priorBriefing, state.ArtifactPaths{
Briefing: priorBriefingPath, Briefing: priorBriefingPath,
ModuleSnapshot: priorPaths.ModuleSnapshot,
Metadata: priorPaths.Metadata, Metadata: priorPaths.Metadata,
DataPackage: priorPaths.DataPackage, DataPackage: priorPaths.DataPackage,
Preflight: priorPaths.Preflight, Preflight: priorPaths.Preflight,
@@ -1045,15 +1056,15 @@ func TestInspectGeneratedReportArtifacts(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("InspectMetadata() error = %v", err) t.Fatalf("InspectMetadata() error = %v", err)
} }
if metadata.BriefingPath != result.BriefingPath || metadata.DataPackagePath != result.DataPackagePath { if metadata.BriefingPath != result.BriefingPath || metadata.ModuleSnapshotPath != result.ModuleSnapshotPath || metadata.DataPackagePath != result.DataPackagePath {
t.Fatalf("metadata paths = %#v, want generated artifact paths", metadata) t.Fatalf("metadata paths = %#v, want generated artifact paths", metadata)
} }
briefingPackage, err := InspectBriefing(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID}) moduleSnapshot, err := InspectModules(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID})
if err != nil { if err != nil {
t.Fatalf("InspectBriefing() error = %v", err) t.Fatalf("InspectModules() error = %v", err)
} }
if briefingPackage.Metadata.RunID != result.Metadata.RunID { if moduleSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(moduleSnapshot.Outputs) == 0 {
t.Fatalf("briefing RunID = %q, want %q", briefingPackage.Metadata.RunID, result.Metadata.RunID) t.Fatalf("module snapshot = %#v, want persisted outputs", moduleSnapshot)
} }
dataPackage, err := InspectDataPackage(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID}) dataPackage, err := InspectDataPackage(context.Background(), InspectRunRequest{Config: cfg, RunID: result.Metadata.RunID})
if err != nil { if err != nil {

View File

@@ -6,6 +6,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/state" "gitea.maximumdirect.net/eric/weatherreporter/internal/state"
@@ -44,12 +45,12 @@ func InspectMetadata(ctx context.Context, req InspectRunRequest) (state.Metadata
return inspection.metadata, err return inspection.metadata, err
} }
func InspectBriefing(ctx context.Context, req InspectRunRequest) (briefing.Package, error) { func InspectModules(ctx context.Context, req InspectRunRequest) (module.Snapshot, error) {
inspection, err := inspectRun(ctx, req) inspection, err := inspectRun(ctx, req)
if err != nil { if err != nil {
return briefing.Package{}, err return module.Snapshot{}, err
} }
return inspection.store.LoadBriefing(ctx, inspection.metadata.BriefingPath) return inspection.store.LoadModuleSnapshot(ctx, inspection.metadata.ModuleSnapshotPath)
} }
func InspectDataPackage(ctx context.Context, req InspectRunRequest) (promptinput.Package, error) { func InspectDataPackage(ctx context.Context, req InspectRunRequest) (promptinput.Package, error) {

View File

@@ -26,7 +26,7 @@ Usage:
weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH] weatherreporter run evening [--config PATH] [--units VALUE] [--tz NAME] [--out-dir PATH]
weatherreporter inspect reports [--config PATH] [--limit N] weatherreporter inspect reports [--config PATH] [--limit N]
weatherreporter inspect metadata [--config PATH] RUN_ID weatherreporter inspect metadata [--config PATH] RUN_ID
weatherreporter inspect briefing [--config PATH] RUN_ID weatherreporter inspect modules [--config PATH] RUN_ID
weatherreporter inspect data-package [--config PATH] RUN_ID weatherreporter inspect data-package [--config PATH] RUN_ID
weatherreporter inspect prior [--config PATH] RUN_ID weatherreporter inspect prior [--config PATH] RUN_ID
weatherreporter inspect sources [--config PATH] RUN_ID weatherreporter inspect sources [--config PATH] RUN_ID
@@ -118,8 +118,8 @@ var inspectRunCommands = []inspectRunCommand{
{Name: "metadata", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) { {Name: "metadata", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) {
return app.InspectMetadata(ctx, req) return app.InspectMetadata(ctx, req)
}}, }},
{Name: "briefing", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) { {Name: "modules", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) {
return app.InspectBriefing(ctx, req) return app.InspectModules(ctx, req)
}}, }},
{Name: "data-package", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) { {Name: "data-package", Inspect: func(ctx context.Context, req app.InspectRunRequest) (any, error) {
return app.InspectDataPackage(ctx, req) return app.InspectDataPackage(ctx, req)

View File

@@ -28,6 +28,10 @@ func TestRunHelpLongFlag(t *testing.T) {
if !strings.Contains(stdout.String(), "generate daily") { if !strings.Contains(stdout.String(), "generate daily") {
t.Fatalf("help output missing generate command:\n%s", stdout.String()) t.Fatalf("help output missing generate command:\n%s", stdout.String())
} }
removedInspectCommand := "inspect " + "briefing"
if !strings.Contains(stdout.String(), "inspect modules") || strings.Contains(stdout.String(), removedInspectCommand) {
t.Fatalf("help output has wrong inspect commands:\n%s", stdout.String())
}
} }
func TestRunHelpShortFlag(t *testing.T) { func TestRunHelpShortFlag(t *testing.T) {
@@ -729,7 +733,8 @@ func TestRunInspectGeneratedArtifacts(t *testing.T) {
t.Fatalf("inspect reports output missing run:\n%s", stdout.String()) t.Fatalf("inspect reports output missing run:\n%s", stdout.String())
} }
for _, command := range []string{"metadata", "briefing", "data-package", "sources"} { var sourcesOutput string
for _, command := range []string{"metadata", "modules", "data-package", "sources"} {
stdout.Reset() stdout.Reset()
err = runner.Run(context.Background(), []string{"inspect", command, "--config", configPath, runID}, &stdout, &stderr) err = runner.Run(context.Background(), []string{"inspect", command, "--config", configPath, runID}, &stdout, &stderr)
if err != nil { if err != nil {
@@ -738,9 +743,12 @@ func TestRunInspectGeneratedArtifacts(t *testing.T) {
if !strings.Contains(stdout.String(), runID) { if !strings.Contains(stdout.String(), runID) {
t.Fatalf("inspect %s output missing run id:\n%s", command, stdout.String()) t.Fatalf("inspect %s output missing run id:\n%s", command, stdout.String())
} }
if command == "sources" {
sourcesOutput = stdout.String()
}
} }
if !strings.Contains(stdout.String(), `"warnings"`) { if !strings.Contains(sourcesOutput, `"warnings"`) {
t.Fatalf("inspect sources output missing warnings:\n%s", stdout.String()) t.Fatalf("inspect sources output missing warnings:\n%s", sourcesOutput)
} }
} }
@@ -772,7 +780,7 @@ func TestRunInspectRunCommandsParseRunIDAndConfig(t *testing.T) {
t.Fatalf("write config: %v", err) t.Fatalf("write config: %v", err)
} }
runner := Runner{Clock: fixedClock()} runner := Runner{Clock: fixedClock()}
commands := []string{"metadata", "briefing", "data-package", "prior", "sources"} commands := []string{"metadata", "modules", "data-package", "prior", "sources"}
for _, command := range commands { for _, command := range commands {
t.Run(command+" requires run id", func(t *testing.T) { t.Run(command+" requires run id", func(t *testing.T) {

View File

@@ -13,6 +13,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/fileutil"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
) )
@@ -28,6 +29,7 @@ type FilesystemStore struct {
type ArtifactPaths struct { type ArtifactPaths struct {
Briefing string `json:"briefing"` Briefing string `json:"briefing"`
ModuleSnapshot string `json:"moduleSnapshot"`
Metadata string `json:"metadata"` Metadata string `json:"metadata"`
DataPackage string `json:"dataPackage"` DataPackage string `json:"dataPackage"`
Preflight string `json:"preflight"` Preflight string `json:"preflight"`
@@ -91,6 +93,7 @@ func (s *FilesystemStore) Paths(resolved report.Resolved) (ArtifactPaths, error)
filenameBase := metadata.RunID filenameBase := metadata.RunID
return ArtifactPaths{ return ArtifactPaths{
Briefing: s.join(s.snapshotsDir, group, validDate, filenameBase+".briefing.json"), Briefing: s.join(s.snapshotsDir, group, validDate, filenameBase+".briefing.json"),
ModuleSnapshot: s.join(s.snapshotsDir, group, validDate, filenameBase+".modules.json"),
Metadata: s.join(s.snapshotsDir, group, validDate, filenameBase+".metadata.json"), Metadata: s.join(s.snapshotsDir, group, validDate, filenameBase+".metadata.json"),
DataPackage: s.join(s.dataPackagesDir, group, validDate, filenameBase+".data_package.json"), DataPackage: s.join(s.dataPackagesDir, group, validDate, filenameBase+".data_package.json"),
Preflight: s.join(s.preflightDir, group, validDate, filenameBase+".render.json"), Preflight: s.join(s.preflightDir, group, validDate, filenameBase+".render.json"),
@@ -110,6 +113,20 @@ func (s *FilesystemStore) SaveBriefing(_ context.Context, resolved report.Resolv
return paths.Briefing, nil return paths.Briefing, nil
} }
func (s *FilesystemStore) SaveModuleSnapshot(_ context.Context, resolved report.Resolved, snapshot module.Snapshot) (string, error) {
paths, err := s.Paths(resolved)
if err != nil {
return "", err
}
if err := snapshot.Validate(); err != nil {
return "", err
}
if err := fileutil.WriteJSONAtomic(paths.ModuleSnapshot, snapshot); err != nil {
return "", err
}
return paths.ModuleSnapshot, nil
}
func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Resolved, pkg promptinput.Package) (string, error) { func (s *FilesystemStore) SaveDataPackage(_ context.Context, resolved report.Resolved, pkg promptinput.Package) (string, error) {
paths, err := s.Paths(resolved) paths, err := s.Paths(resolved)
if err != nil { if err != nil {
@@ -167,6 +184,9 @@ func (s *FilesystemStore) SaveMetadata(_ context.Context, metadata Metadata) (st
if metadata.BriefingPath == "" { if metadata.BriefingPath == "" {
return "", fmt.Errorf("metadata briefing path is required") return "", fmt.Errorf("metadata briefing path is required")
} }
if metadata.ModuleSnapshotPath == "" {
return "", fmt.Errorf("metadata module snapshot path is required")
}
if metadata.DataPackagePath == "" { if metadata.DataPackagePath == "" {
return "", fmt.Errorf("metadata data package path is required") return "", fmt.Errorf("metadata data package path is required")
} }
@@ -308,6 +328,20 @@ func (s *FilesystemStore) LoadDataPackage(_ context.Context, path string) (promp
return pkg, nil return pkg, nil
} }
func (s *FilesystemStore) LoadModuleSnapshot(_ context.Context, path string) (module.Snapshot, error) {
if path == "" {
return module.Snapshot{}, fmt.Errorf("module snapshot path is required")
}
var snapshot module.Snapshot
if err := readJSON(path, &snapshot); err != nil {
return module.Snapshot{}, err
}
if err := snapshot.Validate(); err != nil {
return module.Snapshot{}, err
}
return snapshot, nil
}
func (s *FilesystemStore) reportRecord(path string) (ReportRecord, error) { func (s *FilesystemStore) reportRecord(path string) (ReportRecord, error) {
var metadata Metadata var metadata Metadata
if err := readJSON(path, &metadata); err != nil { if err := readJSON(path, &metadata); err != nil {

View File

@@ -11,6 +11,7 @@ import (
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config" "gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
"gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil" "gitea.maximumdirect.net/eric/weatherreporter/internal/timeutil"
@@ -27,6 +28,7 @@ func TestPathsUseRunIDAndWorkspace(t *testing.T) {
for _, want := range []string{ for _, want := range []string{
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.briefing.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.briefing.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.modules.json"),
filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"), filepath.Join("snapshots", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.metadata.json"),
filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.json"), filepath.Join("data-packages", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.data_package.json"),
filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"), filepath.Join("preflight", "daily", "2026-05-29", "20260529T100000.000000000Z_daily_today.render.json"),
@@ -56,6 +58,14 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("SaveDataPackage() error = %v", err) t.Fatalf("SaveDataPackage() error = %v", err)
} }
snapshot, err := module.NewSnapshot([]module.Output{{ID: module.Metadata, StanzaName: "metadata", Value: map[string]string{"run_id": resolved.Metadata().RunID}}})
if err != nil {
t.Fatalf("NewSnapshot() error = %v", err)
}
moduleSnapshotPath, err := store.SaveModuleSnapshot(context.Background(), resolved, snapshot)
if err != nil {
t.Fatalf("SaveModuleSnapshot() error = %v", err)
}
preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`}) preflightPath, err := store.SavePreflight(context.Background(), resolved, PreflightArtifact{Stdout: `{"ok":true}`})
if err != nil { if err != nil {
t.Fatalf("SavePreflight() error = %v", err) t.Fatalf("SavePreflight() error = %v", err)
@@ -112,6 +122,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
} }
metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{ metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: moduleSnapshotPath,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: dataPackagePath, DataPackage: dataPackagePath,
Preflight: preflightPath, Preflight: preflightPath,
@@ -122,7 +133,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
t.Fatalf("SaveMetadata() error = %v", err) t.Fatalf("SaveMetadata() error = %v", err)
} }
for _, path := range []string{briefingPath, dataPackagePath, preflightPath, notificationPath, renderedReportPath, metadataPath} { for _, path := range []string{briefingPath, moduleSnapshotPath, dataPackagePath, preflightPath, notificationPath, renderedReportPath, metadataPath} {
if _, err := os.Stat(path); err != nil { if _, err := os.Stat(path); err != nil {
t.Fatalf("expected artifact %q: %v", path, err) t.Fatalf("expected artifact %q: %v", path, err)
} }
@@ -134,6 +145,13 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if loadedBriefing.Metadata.RunID != resolved.Metadata().RunID { if loadedBriefing.Metadata.RunID != resolved.Metadata().RunID {
t.Fatalf("loaded briefing RunID = %q, want %q", loadedBriefing.Metadata.RunID, resolved.Metadata().RunID) t.Fatalf("loaded briefing RunID = %q, want %q", loadedBriefing.Metadata.RunID, resolved.Metadata().RunID)
} }
loadedSnapshot, err := store.LoadModuleSnapshot(context.Background(), moduleSnapshotPath)
if err != nil {
t.Fatalf("LoadModuleSnapshot() error = %v", err)
}
if loadedSnapshot.SchemaVersion != module.SnapshotSchemaVersion || len(loadedSnapshot.Outputs) != 1 {
t.Fatalf("loaded module snapshot = %#v, want one metadata output", loadedSnapshot)
}
var decoded Metadata var decoded Metadata
data, err := os.ReadFile(metadataPath) data, err := os.ReadFile(metadataPath)
if err != nil { if err != nil {
@@ -145,7 +163,7 @@ func TestSaveArtifactsAndMetadataRoundTrip(t *testing.T) {
if decoded.RunID != resolved.Metadata().RunID { if decoded.RunID != resolved.Metadata().RunID {
t.Fatalf("RunID = %q, want %q", decoded.RunID, resolved.Metadata().RunID) t.Fatalf("RunID = %q, want %q", decoded.RunID, resolved.Metadata().RunID)
} }
if decoded.BriefingPath != briefingPath || decoded.DataPackagePath != dataPackagePath || decoded.PreflightPath != preflightPath { if decoded.BriefingPath != briefingPath || decoded.ModuleSnapshotPath != moduleSnapshotPath || decoded.DataPackagePath != dataPackagePath || decoded.PreflightPath != preflightPath {
t.Fatalf("metadata paths = %#v, want saved artifact paths", decoded) t.Fatalf("metadata paths = %#v, want saved artifact paths", decoded)
} }
if decoded.RenderedReportPath != renderedReportPath { if decoded.RenderedReportPath != renderedReportPath {
@@ -173,6 +191,7 @@ func TestSaveMetadataUsesExplicitMetadataPath(t *testing.T) {
metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{ metadata := BuildMetadata(resolved, briefingPackage, ArtifactPaths{
Briefing: otherBriefingPath, Briefing: otherBriefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -208,6 +227,7 @@ func TestFindPriorSnapshot(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -247,6 +267,7 @@ func TestFindPriorSnapshotUsesValidDate(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), BuildMetadata(previousDate, briefingPackage, ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), BuildMetadata(previousDate, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -280,6 +301,7 @@ func TestFindPriorSnapshotSupportsThreeDay(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -316,6 +338,7 @@ func TestFindPriorSnapshotSupportsWeekend(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -352,6 +375,7 @@ func TestFindPriorSnapshotSupportsNarrowedWeekendPeriod(t *testing.T) {
} }
_, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{ _, err = store.SaveMetadata(context.Background(), BuildMetadata(first, briefingPackage, ArtifactPaths{
Briefing: briefingPath, Briefing: briefingPath,
ModuleSnapshot: paths.ModuleSnapshot,
Metadata: paths.Metadata, Metadata: paths.Metadata,
DataPackage: paths.DataPackage, DataPackage: paths.DataPackage,
Preflight: paths.Preflight, Preflight: paths.Preflight,
@@ -521,6 +545,7 @@ func pathsString(paths ArtifactPaths) string {
return strings.Join([]string{ return strings.Join([]string{
paths.Briefing, paths.Briefing,
paths.Metadata, paths.Metadata,
paths.ModuleSnapshot,
paths.DataPackage, paths.DataPackage,
paths.Preflight, paths.Preflight,
paths.Notification, paths.Notification,

View File

@@ -27,6 +27,7 @@ type Metadata struct {
Sources []briefing.SourceMetadata `json:"sources,omitempty"` Sources []briefing.SourceMetadata `json:"sources,omitempty"`
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"` SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
BriefingPath string `json:"briefingPath"` BriefingPath string `json:"briefingPath"`
ModuleSnapshotPath string `json:"moduleSnapshotPath"`
DataPackagePath string `json:"dataPackagePath"` DataPackagePath string `json:"dataPackagePath"`
PreflightPath string `json:"preflightPath"` PreflightPath string `json:"preflightPath"`
NotificationPath string `json:"notificationPath,omitempty"` NotificationPath string `json:"notificationPath,omitempty"`
@@ -51,6 +52,7 @@ func BuildMetadata(resolved report.Resolved, briefingPackage briefing.Package, p
Sources: briefingPackage.Metadata.Sources, Sources: briefingPackage.Metadata.Sources,
SourceWarnings: briefingPackage.Metadata.SourceWarnings, SourceWarnings: briefingPackage.Metadata.SourceWarnings,
BriefingPath: paths.Briefing, BriefingPath: paths.Briefing,
ModuleSnapshotPath: paths.ModuleSnapshot,
DataPackagePath: paths.DataPackage, DataPackagePath: paths.DataPackage,
PreflightPath: paths.Preflight, PreflightPath: paths.Preflight,
RenderedReportPath: paths.RenderedReport, RenderedReportPath: paths.RenderedReport,

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"gitea.maximumdirect.net/eric/weatherreporter/internal/briefing" "gitea.maximumdirect.net/eric/weatherreporter/internal/briefing"
"gitea.maximumdirect.net/eric/weatherreporter/internal/module"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput" "gitea.maximumdirect.net/eric/weatherreporter/internal/promptinput"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report" "gitea.maximumdirect.net/eric/weatherreporter/internal/report"
) )
@@ -14,6 +15,7 @@ import (
type Store interface { type Store interface {
Paths(report.Resolved) (ArtifactPaths, error) Paths(report.Resolved) (ArtifactPaths, error)
SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error) SaveBriefing(context.Context, report.Resolved, briefing.Package) (string, error)
SaveModuleSnapshot(context.Context, report.Resolved, module.Snapshot) (string, error)
SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error) SaveDataPackage(context.Context, report.Resolved, promptinput.Package) (string, error)
SavePreflight(context.Context, report.Resolved, PreflightArtifact) (string, error) SavePreflight(context.Context, report.Resolved, PreflightArtifact) (string, error)
SaveDistributorNotification(context.Context, report.Resolved, DistributorNotificationArtifact) (string, error) SaveDistributorNotification(context.Context, report.Resolved, DistributorNotificationArtifact) (string, error)
@@ -21,6 +23,7 @@ type Store interface {
SaveMetadata(context.Context, Metadata) (string, error) SaveMetadata(context.Context, Metadata) (string, error)
FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error) FindPriorSnapshot(context.Context, report.Resolved) (*PriorSnapshot, error)
LoadBriefing(context.Context, string) (briefing.Package, error) LoadBriefing(context.Context, string) (briefing.Package, error)
LoadModuleSnapshot(context.Context, string) (module.Snapshot, error)
} }
type PriorSnapshot struct { type PriorSnapshot struct {