Route report projections through prepared identity
This commit is contained in:
@@ -77,13 +77,13 @@ are unavailable. Empty alert and outlook runs can still produce checked-empty
|
|||||||
modules. SPC discussion is omitted unless a retained categorical outlook meets
|
modules. SPC discussion is omitted unless a retained categorical outlook meets
|
||||||
the package's severity criterion and matching discussion text exists.
|
the package's severity criterion and matching discussion text exists.
|
||||||
|
|
||||||
Effective units, timezone, and location context arrive in `ModuleContext` from
|
`ModuleContext` carries the effective units, timezone, location context, and
|
||||||
configuration and resolved report metadata. Report preparation also creates one
|
prepared identity. Report preparation creates that one `PreparedIdentity` for
|
||||||
`PreparedIdentity` for the shared report identity, timing, configuration
|
the shared report identity, timing, configuration context, and source warnings
|
||||||
context, and source warnings. Briefing metadata derives its matching fields
|
before module construction. The metadata module projects its matching fields
|
||||||
from that value; module projections retain their prompt-safe shape. Field
|
from that value and retains its prompt-safe shape. Field defaults are owned by
|
||||||
defaults are owned by [configuration](../config.md), and prompt-package layout
|
[configuration](../config.md), and prompt-package layout is owned by [prompt
|
||||||
is owned by [prompt input](prompt-input.md).
|
input](prompt-input.md).
|
||||||
|
|
||||||
## Verification and invariants
|
## Verification and invariants
|
||||||
|
|
||||||
|
|||||||
@@ -6,15 +6,14 @@ and profile comparison; it is not a durable artifact.
|
|||||||
|
|
||||||
Preparation first establishes one `PreparedIdentity` for the report run, report
|
Preparation first establishes one `PreparedIdentity` for the report run, report
|
||||||
and prompt IDs, variant, generation time, units, timezone, valid period,
|
and prompt IDs, variant, generation time, units, timezone, valid period,
|
||||||
location, and source warnings. It then builds report facts, the configured
|
location, and source warnings. It passes that identity to the configured module
|
||||||
module snapshot, briefing metadata, the curated prompt-input package,
|
snapshot, curated prompt-input package, serialized YAML, generated-text render
|
||||||
serialized YAML, and the generated-text definition. The current metadata
|
context, and generated-text definition. Each boundary projects only the fields
|
||||||
projections remain equivalent to that identity while each retains its own
|
it needs from that prepared authority.
|
||||||
boundary-specific shape.
|
|
||||||
|
|
||||||
Preparation deep-copies mutable facts, snapshots, identity, metadata, and
|
Preparation deep-copies mutable facts, snapshots, identity, and data-package
|
||||||
data-package bytes before returning them. Consumers receive independent copies
|
bytes before returning them. Consumers receive independent copies so one
|
||||||
so one execution cannot change another's input or rendering context.
|
execution cannot change another's input or rendering context.
|
||||||
|
|
||||||
Single-report generation executes one prepared profile and publishes its
|
Single-report generation executes one prepared profile and publishes its
|
||||||
Markdown. Comparison prepares once, gives every selected profile the same YAML
|
Markdown. Comparison prepares once, gives every selected profile the same YAML
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
## Package Construction
|
## Package Construction
|
||||||
|
|
||||||
`Build` produces `weatherreporter.data_package.v4`. It copies the run ID; report ID, variant, prompt ID, generation time, timezone, local current date, and valid period; ordered briefing stanzas; and prompt-safe source-warning summaries. Warning summaries include only source, code, severity, message, and completeness impact; raw transport and provenance fields such as endpoints never cross into the provider input. Prompt input contains no historical comparison section.
|
`Build` produces `weatherreporter.data_package.v4`. Its metadata projection comes from the prepared report identity and copies the run ID; report ID, variant, prompt ID, generation time, timezone, local current date, and valid period; ordered briefing stanzas; and prompt-safe source-warning summaries. Warning summaries include only source, code, severity, message, and completeness impact; raw transport and provenance fields such as endpoints never cross into the provider input. Prompt input contains no historical comparison section.
|
||||||
|
|
||||||
Briefing is a flat ordered set of stanza values. `Build` uses each output's `DataPackageValue`, so curated prompt exports take precedence and rich values are used only as a fallback. Prompt exports are selected by the [briefing registry](briefing.md), while the rich-versus-prompt contract is in [module internals](module.md).
|
Briefing is a flat ordered set of stanza values. `Build` uses each output's `DataPackageValue`, so curated prompt exports take precedence and rich values are used only as a fallback. Prompt exports are selected by the [briefing registry](briefing.md), while the rich-versus-prompt contract is in [module internals](module.md).
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ type BatchRequest struct {
|
|||||||
type ModuleSnapshotRequest struct {
|
type ModuleSnapshotRequest struct {
|
||||||
Config config.Config
|
Config config.Config
|
||||||
Resolved report.Resolved
|
Resolved report.Resolved
|
||||||
|
Identity briefing.PreparedIdentity
|
||||||
}
|
}
|
||||||
|
|
||||||
type ReportFacts struct {
|
type ReportFacts struct {
|
||||||
@@ -729,7 +730,12 @@ func BuildModuleSnapshotFromFacts(req ModuleSnapshotRequest, reportFacts ReportF
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return module.Snapshot{}, err
|
return module.Snapshot{}, err
|
||||||
}
|
}
|
||||||
|
identity := req.Identity
|
||||||
|
if identity.ReportID == "" {
|
||||||
|
identity = briefing.BuildPreparedIdentity(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
|
||||||
|
}
|
||||||
moduleContext := briefing.ModuleContext{
|
moduleContext := briefing.ModuleContext{
|
||||||
|
Identity: identity,
|
||||||
Resolved: req.Resolved,
|
Resolved: req.Resolved,
|
||||||
Collected: reportFacts.Collected,
|
Collected: reportFacts.Collected,
|
||||||
Derived: reportFacts.Derived,
|
Derived: reportFacts.Derived,
|
||||||
@@ -761,16 +767,16 @@ func briefingBuildContext(cfg config.Config, resolved report.Resolved, collected
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func promptMetadata(metadata briefing.Metadata) promptinput.Metadata {
|
func promptMetadata(identity briefing.PreparedIdentity) promptinput.Metadata {
|
||||||
return promptinput.Metadata{
|
return promptinput.Metadata{
|
||||||
RunID: metadata.RunID,
|
RunID: identity.RunID,
|
||||||
ReportID: metadata.ReportID,
|
ReportID: identity.ReportID,
|
||||||
Variant: metadata.Variant,
|
Variant: identity.Variant,
|
||||||
PromptID: metadata.PromptID,
|
PromptID: identity.PromptID,
|
||||||
GeneratedAt: metadata.GeneratedAt,
|
GeneratedAt: identity.GeneratedAt,
|
||||||
Timezone: metadata.Timezone,
|
Timezone: identity.Timezone,
|
||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: identity.ValidPeriod,
|
||||||
SourceWarnings: metadata.SourceWarnings,
|
SourceWarnings: identity.SourceWarnings,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ type preparedReport struct {
|
|||||||
reportFacts ReportFacts
|
reportFacts ReportFacts
|
||||||
moduleSnapshot module.Snapshot
|
moduleSnapshot module.Snapshot
|
||||||
identity briefing.PreparedIdentity
|
identity briefing.PreparedIdentity
|
||||||
briefingMetadata briefing.Metadata
|
|
||||||
sourceWarnings []weatherdata.SourceWarning
|
sourceWarnings []weatherdata.SourceWarning
|
||||||
dataPackage []byte
|
dataPackage []byte
|
||||||
handler generatedtext.Handler
|
handler generatedtext.Handler
|
||||||
@@ -57,12 +56,11 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
|||||||
}
|
}
|
||||||
buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
|
buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
|
||||||
identity := briefing.BuildPreparedIdentity(buildContext)
|
identity := briefing.BuildPreparedIdentity(buildContext)
|
||||||
moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved}, reportFacts)
|
moduleSnapshot, err := BuildModuleSnapshotFromFacts(ModuleSnapshotRequest{Config: req.Config, Resolved: req.Resolved, Identity: identity}, reportFacts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedReport{}, &preparationError{operation: "build module snapshot", err: err}
|
return preparedReport{}, &preparationError{operation: "build module snapshot", err: err}
|
||||||
}
|
}
|
||||||
metadata := briefing.BuildMetadata(identity, buildContext.Bundle)
|
dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(identity), Modules: moduleSnapshot})
|
||||||
dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(metadata), Modules: moduleSnapshot})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedReport{}, &preparationError{operation: "build data package", err: err}
|
return preparedReport{}, &preparationError{operation: "build data package", err: err}
|
||||||
}
|
}
|
||||||
@@ -87,16 +85,11 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return preparedReport{}, &preparationError{operation: "copy prepared identity", err: err}
|
return preparedReport{}, &preparationError{operation: "copy prepared identity", err: err}
|
||||||
}
|
}
|
||||||
clonedMetadata, err := clonePreparedValue(metadata)
|
|
||||||
if err != nil {
|
|
||||||
return preparedReport{}, &preparationError{operation: "copy prepared briefing metadata", err: err}
|
|
||||||
}
|
|
||||||
prepared := preparedReport{
|
prepared := preparedReport{
|
||||||
resolved: cloneResolved(req.Resolved),
|
resolved: cloneResolved(req.Resolved),
|
||||||
reportFacts: clonedFacts,
|
reportFacts: clonedFacts,
|
||||||
moduleSnapshot: clonedSnapshot,
|
moduleSnapshot: clonedSnapshot,
|
||||||
identity: clonedIdentity,
|
identity: clonedIdentity,
|
||||||
briefingMetadata: clonedMetadata,
|
|
||||||
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
|
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
|
||||||
dataPackage: append([]byte(nil), serializedDataPackage...),
|
dataPackage: append([]byte(nil), serializedDataPackage...),
|
||||||
handler: handler,
|
handler: handler,
|
||||||
@@ -127,20 +120,20 @@ func (p preparedReport) sourceWarningsCopy() []weatherdata.SourceWarning {
|
|||||||
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
|
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p preparedReport) renderInputs() (briefing.Metadata, module.Snapshot, ReportFacts, error) {
|
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, ReportFacts, error) {
|
||||||
metadata, err := clonePreparedValue(p.briefingMetadata)
|
identity, err := clonePreparedValue(p.identity)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err
|
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||||
}
|
}
|
||||||
snapshot, err := clonePreparedValue(p.moduleSnapshot)
|
snapshot, err := clonePreparedValue(p.moduleSnapshot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err
|
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||||
}
|
}
|
||||||
reportFacts, err := clonePreparedValue(p.reportFacts)
|
reportFacts, err := clonePreparedValue(p.reportFacts)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err
|
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
|
||||||
}
|
}
|
||||||
return metadata, snapshot, reportFacts, nil
|
return identity, snapshot, reportFacts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func clonePreparedValue[T any](value T) (T, error) {
|
func clonePreparedValue[T any](value T) (T, error) {
|
||||||
|
|||||||
@@ -31,19 +31,18 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("second prepareReport() error = %v", err)
|
t.Fatalf("second prepareReport() error = %v", err)
|
||||||
}
|
}
|
||||||
if len(prepared.dataPackage) == 0 || !bytes.Equal(prepared.dataPackage, repeated.dataPackage) || !reflect.DeepEqual(prepared.identity, repeated.identity) || !reflect.DeepEqual(prepared.briefingMetadata, repeated.briefingMetadata) {
|
if len(prepared.dataPackage) == 0 || !bytes.Equal(prepared.dataPackage, repeated.dataPackage) || !reflect.DeepEqual(prepared.identity, repeated.identity) {
|
||||||
t.Fatalf("prepared package/identity/metadata are not deterministic: %q/%#v/%#v", prepared.dataPackage, prepared.identity, prepared.briefingMetadata)
|
t.Fatalf("prepared package and identity are not deterministic: %q/%#v", prepared.dataPackage, prepared.identity)
|
||||||
}
|
}
|
||||||
|
|
||||||
originalDataPackage := append([]byte(nil), prepared.dataPackage...)
|
originalDataPackage := append([]byte(nil), prepared.dataPackage...)
|
||||||
originalIdentity := prepared.identity
|
originalIdentity := prepared.identity
|
||||||
originalMetadata := prepared.briefingMetadata
|
|
||||||
originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...)
|
originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...)
|
||||||
metadata, snapshot, reportFacts, err := prepared.renderInputs()
|
identity, snapshot, reportFacts, err := prepared.renderInputs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("renderInputs() error = %v", err)
|
t.Fatalf("renderInputs() error = %v", err)
|
||||||
}
|
}
|
||||||
metadata.SourceWarnings = append(metadata.SourceWarnings, weatherdata.SourceWarning{Source: "test", Message: "consumer mutation"})
|
identity.SourceWarnings = append(identity.SourceWarnings, weatherdata.SourceWarning{Source: "test", Message: "consumer mutation"})
|
||||||
snapshot.Outputs = nil
|
snapshot.Outputs = nil
|
||||||
reportFacts.Collected.Hourly.Periods[0].TextDescription = "consumer mutation"
|
reportFacts.Collected.Hourly.Periods[0].TextDescription = "consumer mutation"
|
||||||
bundle.Hourly.Periods[0].TextDescription = "mutated after preparation"
|
bundle.Hourly.Periods[0].TextDescription = "mutated after preparation"
|
||||||
@@ -55,7 +54,7 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
|||||||
bundle.Sources[0].Query["mutated"] = "true"
|
bundle.Sources[0].Query["mutated"] = "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.briefingMetadata, originalMetadata) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) {
|
if !bytes.Equal(prepared.dataPackage, originalDataPackage) || !reflect.DeepEqual(prepared.identity, originalIdentity) || !reflect.DeepEqual(prepared.sourceWarnings, originalWarnings) {
|
||||||
t.Fatalf("prepared values changed after caller mutation: %#v", prepared)
|
t.Fatalf("prepared values changed after caller mutation: %#v", prepared)
|
||||||
}
|
}
|
||||||
if prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "mutated after preparation" {
|
if prepared.reportFacts.Collected.Hourly.Periods[0].TextDescription == "mutated after preparation" {
|
||||||
@@ -66,7 +65,7 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPrepareReportSharesOneMetadataIdentity(t *testing.T) {
|
func TestPrepareReportProjectsPreparedIdentity(t *testing.T) {
|
||||||
cfg := generationConfig()
|
cfg := generationConfig()
|
||||||
bundle := generationBundle(t)
|
bundle := generationBundle(t)
|
||||||
resolved, err := ResolveGenerate(GenerateRequest{
|
resolved, err := ResolveGenerate(GenerateRequest{
|
||||||
@@ -82,12 +81,14 @@ func TestPrepareReportSharesOneMetadataIdentity(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
identity := prepared.identity
|
identity := prepared.identity
|
||||||
metadata := prepared.briefingMetadata
|
renderIdentity, _, _, err := prepared.renderInputs()
|
||||||
if metadata.RunID != identity.RunID || metadata.ReportID != identity.ReportID || metadata.Variant != identity.Variant || metadata.PromptID != identity.PromptID || !metadata.GeneratedAt.Equal(identity.GeneratedAt) || metadata.Units != identity.Units || metadata.Timezone != identity.Timezone || metadata.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(metadata.Location, identity.Location) || !reflect.DeepEqual(metadata.SourceWarnings, identity.SourceWarnings) {
|
if err != nil {
|
||||||
t.Fatalf("briefing metadata does not match prepared identity: %#v/%#v", metadata, identity)
|
t.Fatalf("renderInputs() error = %v", err)
|
||||||
}
|
}
|
||||||
|
if !reflect.DeepEqual(renderIdentity, identity) {
|
||||||
prompt := promptMetadata(metadata)
|
t.Fatalf("render identity = %#v, want %#v", renderIdentity, identity)
|
||||||
|
}
|
||||||
|
prompt := promptMetadata(identity)
|
||||||
if prompt.RunID != identity.RunID || prompt.ReportID != identity.ReportID || prompt.Variant != identity.Variant || prompt.PromptID != identity.PromptID || !prompt.GeneratedAt.Equal(identity.GeneratedAt) || prompt.Timezone != identity.Timezone || prompt.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(prompt.SourceWarnings, identity.SourceWarnings) {
|
if prompt.RunID != identity.RunID || prompt.ReportID != identity.ReportID || prompt.Variant != identity.Variant || prompt.PromptID != identity.PromptID || !prompt.GeneratedAt.Equal(identity.GeneratedAt) || prompt.Timezone != identity.Timezone || prompt.ValidPeriod != identity.ValidPeriod || !reflect.DeepEqual(prompt.SourceWarnings, identity.SourceWarnings) {
|
||||||
t.Fatalf("prompt metadata does not match prepared identity: %#v/%#v", prompt, identity)
|
t.Fatalf("prompt metadata does not match prepared identity: %#v/%#v", prompt, identity)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -115,11 +115,11 @@ func executePreparedProfile(ctx context.Context, req profileExecutionRequest) (p
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return outcome, nil, &profileExecutionError{operation: "validate generated text", err: err}
|
return outcome, nil, &profileExecutionError{operation: "validate generated text", err: err}
|
||||||
}
|
}
|
||||||
metadata, snapshot, reportFacts, err := req.Prepared.renderInputs()
|
identity, snapshot, reportFacts, err := req.Prepared.renderInputs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return outcome, nil, &profileExecutionError{operation: "copy prepared render inputs", err: err}
|
return outcome, nil, &profileExecutionError{operation: "copy prepared render inputs", err: err}
|
||||||
}
|
}
|
||||||
renderContext, err := req.Prepared.handler.BuildRenderContext(metadata, snapshot, reportFacts.Collected, reportFacts.Derived, generatedText)
|
renderContext, err := req.Prepared.handler.BuildRenderContext(identity, snapshot, reportFacts.Collected, reportFacts.Derived, generatedText)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return outcome, nil, &profileExecutionError{operation: "build render context", err: err}
|
return outcome, nil, &profileExecutionError{operation: "build render context", err: err}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -600,7 +600,7 @@ func testModuleContext() ModuleContext {
|
|||||||
hourlyHumidity := 66.0
|
hourlyHumidity := 66.0
|
||||||
hourlyWindMph := 14.0
|
hourlyWindMph := 14.0
|
||||||
updatedAt := mustParseModuleTime("2026-05-29T07:30:00-05:00")
|
updatedAt := mustParseModuleTime("2026-05-29T07:30:00-05:00")
|
||||||
return ModuleContext{
|
ctx := ModuleContext{
|
||||||
Resolved: resolved,
|
Resolved: resolved,
|
||||||
Collected: facts.CollectedFacts{
|
Collected: facts.CollectedFacts{
|
||||||
Current: &weatherdata.Current{
|
Current: &weatherdata.Current{
|
||||||
@@ -728,6 +728,14 @@ func testModuleContext() ModuleContext {
|
|||||||
Timezone: "America/Chicago",
|
Timezone: "America/Chicago",
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
ctx.Identity = BuildPreparedIdentity(BuildContext{
|
||||||
|
Resolved: ctx.Resolved,
|
||||||
|
Bundle: ctx.Collected.Bundle(),
|
||||||
|
Units: ctx.Units,
|
||||||
|
Timezone: ctx.Timezone,
|
||||||
|
Location: ctx.Location,
|
||||||
|
})
|
||||||
|
return ctx
|
||||||
}
|
}
|
||||||
|
|
||||||
func moduleValue[T any](t *testing.T, output *module.Output) T {
|
func moduleValue[T any](t *testing.T, output *module.Output) T {
|
||||||
|
|||||||
@@ -31,18 +31,17 @@ type SourceWarningSummary struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildMetadataModule(ctx ModuleContext, _ any) (*module.Output, error) {
|
func buildMetadataModule(ctx ModuleContext, _ any) (*module.Output, error) {
|
||||||
metadata := ctx.Resolved.Metadata()
|
|
||||||
value := MetadataModule{
|
value := MetadataModule{
|
||||||
RunID: metadata.RunID,
|
RunID: ctx.Identity.RunID,
|
||||||
ReportID: metadata.ReportID,
|
ReportID: ctx.Identity.ReportID,
|
||||||
Variant: variantForReport(metadata.ReportID),
|
Variant: ctx.Identity.Variant,
|
||||||
PromptID: metadata.PromptID,
|
PromptID: ctx.Identity.PromptID,
|
||||||
GeneratedAt: metadata.GeneratedAt,
|
GeneratedAt: ctx.Identity.GeneratedAt,
|
||||||
Units: ctx.Units,
|
Units: ctx.Identity.Units,
|
||||||
Timezone: ctx.Timezone,
|
Timezone: ctx.Identity.Timezone,
|
||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: ctx.Identity.ValidPeriod,
|
||||||
Location: copyLocation(ctx.Location),
|
Location: copyLocation(ctx.Identity.Location),
|
||||||
SourceWarnings: sourceWarningSummaries(ctx.Collected.SourceWarnings),
|
SourceWarnings: sourceWarningSummaries(ctx.Identity.SourceWarnings),
|
||||||
}
|
}
|
||||||
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil
|
return &module.Output{ID: module.Metadata, StanzaName: "metadata", Value: value}, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type ModuleContext struct {
|
type ModuleContext struct {
|
||||||
|
Identity PreparedIdentity
|
||||||
Resolved report.Resolved
|
Resolved report.Resolved
|
||||||
Collected facts.CollectedFacts
|
Collected facts.CollectedFacts
|
||||||
Derived facts.DerivedFacts
|
Derived facts.DerivedFacts
|
||||||
|
|||||||
@@ -9,23 +9,6 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Metadata struct {
|
|
||||||
RunID string `json:"runId"`
|
|
||||||
ReportID report.ID `json:"reportId"`
|
|
||||||
Variant string `json:"variant,omitempty"`
|
|
||||||
PromptID string `json:"promptId"`
|
|
||||||
GeneratedAt time.Time `json:"generatedAt"`
|
|
||||||
Units string `json:"units"`
|
|
||||||
Timezone string `json:"timezone"`
|
|
||||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
|
||||||
Location *LocationContext `json:"location,omitempty"`
|
|
||||||
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
|
||||||
SourceLocation string `json:"sourceLocation,omitempty"`
|
|
||||||
Sources []SourceMetadata `json:"sources,omitempty"`
|
|
||||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
|
||||||
Alerts *AlertStatus `json:"alerts,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PreparedIdentity is the single prepared authority for report identity,
|
// PreparedIdentity is the single prepared authority for report identity,
|
||||||
// timing, configuration context, and source warnings.
|
// timing, configuration context, and source warnings.
|
||||||
type PreparedIdentity struct {
|
type PreparedIdentity struct {
|
||||||
@@ -38,6 +21,8 @@ type PreparedIdentity struct {
|
|||||||
Timezone string `json:"timezone"`
|
Timezone string `json:"timezone"`
|
||||||
ValidPeriod timeutil.Period `json:"validPeriod"`
|
ValidPeriod timeutil.Period `json:"validPeriod"`
|
||||||
Location *LocationContext `json:"location,omitempty"`
|
Location *LocationContext `json:"location,omitempty"`
|
||||||
|
SourceLocationID string `json:"sourceLocationId,omitempty"`
|
||||||
|
SourceLocation string `json:"sourceLocation,omitempty"`
|
||||||
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
SourceWarnings []weatherdata.SourceWarning `json:"sourceWarnings,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,24 +33,6 @@ type LocationContext struct {
|
|||||||
Timezone string `json:"timezone,omitempty"`
|
Timezone string `json:"timezone,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SourceMetadata struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Endpoint string `json:"endpoint,omitempty"`
|
|
||||||
FetchedAt time.Time `json:"fetchedAt"`
|
|
||||||
IssuedAt *time.Time `json:"issuedAt,omitempty"`
|
|
||||||
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
|
|
||||||
DataSHA256 string `json:"dataSha256,omitempty"`
|
|
||||||
Missing bool `json:"missing,omitempty"`
|
|
||||||
Warnings []weatherdata.SourceWarning `json:"warnings,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AlertStatus struct {
|
|
||||||
Checked bool `json:"checked"`
|
|
||||||
ActiveCount int `json:"activeCount"`
|
|
||||||
RelevantCount int `json:"relevantCount"`
|
|
||||||
Missing bool `json:"missing,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BuildContext struct {
|
type BuildContext struct {
|
||||||
Resolved report.Resolved
|
Resolved report.Resolved
|
||||||
Bundle *weatherdata.Bundle
|
Bundle *weatherdata.Bundle
|
||||||
@@ -76,6 +43,7 @@ type BuildContext struct {
|
|||||||
|
|
||||||
func BuildPreparedIdentity(ctx BuildContext) PreparedIdentity {
|
func BuildPreparedIdentity(ctx BuildContext) PreparedIdentity {
|
||||||
metadata := ctx.Resolved.Metadata()
|
metadata := ctx.Resolved.Metadata()
|
||||||
|
sourceLocationID, sourceLocation := sourceLocation(ctx.Bundle)
|
||||||
return PreparedIdentity{
|
return PreparedIdentity{
|
||||||
RunID: metadata.RunID,
|
RunID: metadata.RunID,
|
||||||
ReportID: metadata.ReportID,
|
ReportID: metadata.ReportID,
|
||||||
@@ -86,27 +54,9 @@ func BuildPreparedIdentity(ctx BuildContext) PreparedIdentity {
|
|||||||
Timezone: ctx.Timezone,
|
Timezone: ctx.Timezone,
|
||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: metadata.ValidPeriod,
|
||||||
Location: copyLocation(ctx.Location),
|
Location: copyLocation(ctx.Location),
|
||||||
SourceWarnings: append([]weatherdata.SourceWarning(nil), sourceWarnings(ctx.Bundle)...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func BuildMetadata(identity PreparedIdentity, bundle *weatherdata.Bundle) Metadata {
|
|
||||||
sourceLocationID, sourceLocation := sourceLocation(bundle)
|
|
||||||
return Metadata{
|
|
||||||
RunID: identity.RunID,
|
|
||||||
ReportID: identity.ReportID,
|
|
||||||
Variant: identity.Variant,
|
|
||||||
PromptID: identity.PromptID,
|
|
||||||
GeneratedAt: identity.GeneratedAt,
|
|
||||||
Units: identity.Units,
|
|
||||||
Timezone: identity.Timezone,
|
|
||||||
ValidPeriod: identity.ValidPeriod,
|
|
||||||
Location: copyLocation(identity.Location),
|
|
||||||
SourceLocationID: sourceLocationID,
|
SourceLocationID: sourceLocationID,
|
||||||
SourceLocation: sourceLocation,
|
SourceLocation: sourceLocation,
|
||||||
Sources: sourceMetadata(bundle),
|
SourceWarnings: append([]weatherdata.SourceWarning(nil), sourceWarnings(ctx.Bundle)...),
|
||||||
SourceWarnings: append([]weatherdata.SourceWarning(nil), identity.SourceWarnings...),
|
|
||||||
Alerts: alertStatus(bundle),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,26 +115,6 @@ func sourceLocation(bundle *weatherdata.Bundle) (string, string) {
|
|||||||
return "", ""
|
return "", ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func sourceMetadata(bundle *weatherdata.Bundle) []SourceMetadata {
|
|
||||||
if bundle == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := make([]SourceMetadata, 0, len(bundle.Sources))
|
|
||||||
for _, source := range bundle.Sources {
|
|
||||||
out = append(out, SourceMetadata{
|
|
||||||
Name: source.Name,
|
|
||||||
Endpoint: source.Endpoint,
|
|
||||||
FetchedAt: source.FetchedAt,
|
|
||||||
IssuedAt: source.IssuedAt,
|
|
||||||
UpdatedAt: source.UpdatedAt,
|
|
||||||
DataSHA256: source.DataSHA256,
|
|
||||||
Missing: source.Missing,
|
|
||||||
Warnings: source.Warnings,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func sourceWarnings(bundle *weatherdata.Bundle) []weatherdata.SourceWarning {
|
func sourceWarnings(bundle *weatherdata.Bundle) []weatherdata.SourceWarning {
|
||||||
if bundle == nil {
|
if bundle == nil {
|
||||||
return nil
|
return nil
|
||||||
@@ -192,27 +122,6 @@ func sourceWarnings(bundle *weatherdata.Bundle) []weatherdata.SourceWarning {
|
|||||||
return bundle.Warnings
|
return bundle.Warnings
|
||||||
}
|
}
|
||||||
|
|
||||||
func alertStatus(bundle *weatherdata.Bundle) *AlertStatus {
|
|
||||||
if bundle == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
status := &AlertStatus{}
|
|
||||||
if bundle.Alerts != nil {
|
|
||||||
status.Checked = true
|
|
||||||
status.ActiveCount = len(bundle.Alerts.Alerts)
|
|
||||||
}
|
|
||||||
for _, source := range bundle.Sources {
|
|
||||||
if source.Name == "alerts" && source.Missing {
|
|
||||||
status.Missing = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !status.Checked && !status.Missing {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return status
|
|
||||||
}
|
|
||||||
|
|
||||||
func variantForReport(id report.ID) string {
|
func variantForReport(id report.ID) string {
|
||||||
switch id {
|
switch id {
|
||||||
case report.Daily, report.Today:
|
case report.Daily, report.Today:
|
||||||
|
|||||||
@@ -3,9 +3,11 @@ package briefing
|
|||||||
import (
|
import (
|
||||||
"reflect"
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/weatherdata"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestPreparedIdentityBuildsEquivalentMetadata(t *testing.T) {
|
func TestPreparedIdentityCopiesMutableFields(t *testing.T) {
|
||||||
moduleContext := testModuleContext()
|
moduleContext := testModuleContext()
|
||||||
context := BuildContext{
|
context := BuildContext{
|
||||||
Resolved: moduleContext.Resolved,
|
Resolved: moduleContext.Resolved,
|
||||||
@@ -15,18 +17,10 @@ func TestPreparedIdentityBuildsEquivalentMetadata(t *testing.T) {
|
|||||||
Location: moduleContext.Location,
|
Location: moduleContext.Location,
|
||||||
}
|
}
|
||||||
identity := BuildPreparedIdentity(context)
|
identity := BuildPreparedIdentity(context)
|
||||||
metadata := BuildMetadata(identity, context.Bundle)
|
originalWarnings := append([]weatherdata.SourceWarning(nil), moduleContext.Collected.SourceWarnings...)
|
||||||
|
identity.Location.Name = "consumer mutation"
|
||||||
if metadata.RunID != identity.RunID || metadata.ReportID != identity.ReportID || metadata.Variant != identity.Variant || metadata.PromptID != identity.PromptID || !metadata.GeneratedAt.Equal(identity.GeneratedAt) || metadata.Units != identity.Units || metadata.Timezone != identity.Timezone || metadata.ValidPeriod != identity.ValidPeriod {
|
identity.SourceWarnings = append(identity.SourceWarnings, moduleContext.Collected.SourceWarnings[0])
|
||||||
t.Fatalf("metadata identity = %#v, want %#v", metadata, identity)
|
if moduleContext.Location.Name == "consumer mutation" || !reflect.DeepEqual(moduleContext.Collected.SourceWarnings, originalWarnings) {
|
||||||
}
|
t.Fatalf("prepared identity changed its source context: %#v", identity)
|
||||||
if !reflect.DeepEqual(metadata.Location, identity.Location) || !reflect.DeepEqual(metadata.SourceWarnings, identity.SourceWarnings) {
|
|
||||||
t.Fatalf("metadata location/warnings = %#v/%#v, want %#v/%#v", metadata.Location, metadata.SourceWarnings, identity.Location, identity.SourceWarnings)
|
|
||||||
}
|
|
||||||
|
|
||||||
metadata.Location.Name = "consumer mutation"
|
|
||||||
metadata.SourceWarnings = append(metadata.SourceWarnings, moduleContext.Collected.SourceWarnings[0])
|
|
||||||
if identity.Location.Name == "consumer mutation" || len(identity.SourceWarnings) != len(moduleContext.Collected.SourceWarnings) {
|
|
||||||
t.Fatalf("identity changed through metadata mutation: %#v", identity)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ const (
|
|||||||
|
|
||||||
type validator func([]byte) (any, []byte, error)
|
type validator func([]byte) (any, []byte, error)
|
||||||
|
|
||||||
type renderContextBuilder func(report.ID, string, briefing.Metadata, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error)
|
type renderContextBuilder func(report.ID, string, briefing.PreparedIdentity, module.Snapshot, facts.CollectedFacts, facts.DerivedFacts, any) (any, error)
|
||||||
|
|
||||||
type catalogEntry struct {
|
type catalogEntry struct {
|
||||||
reportID report.ID
|
reportID report.ID
|
||||||
@@ -140,11 +140,11 @@ func (h Handler) Validate(data []byte) (any, []byte, error) {
|
|||||||
return h.validate(data)
|
return h.validate(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) BuildRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
func (h Handler) BuildRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||||
if h.renderContextBuilder == nil {
|
if h.renderContextBuilder == nil {
|
||||||
return nil, fmt.Errorf("render-context builder is not registered for template %q on report %q", h.templateID, h.reportID)
|
return nil, fmt.Errorf("render-context builder is not registered for template %q on report %q", h.templateID, h.reportID)
|
||||||
}
|
}
|
||||||
return h.renderContextBuilder(h.reportID, h.templateID, metadata, snapshot, collected, derived, generated)
|
return h.renderContextBuilder(h.reportID, h.templateID, identity, snapshot, collected, derived, generated)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h Handler) Render(data any) ([]byte, error) {
|
func (h Handler) Render(data any) ([]byte, error) {
|
||||||
@@ -171,34 +171,34 @@ func validateTomorrow(data []byte) (any, []byte, error) {
|
|||||||
return ValidateTomorrow(data)
|
return ValidateTomorrow(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildHourlyContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
func buildHourlyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||||
hourly, ok := generated.(Hourly)
|
hourly, ok := generated.(Hourly)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", templateID, reportID)
|
return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", templateID, reportID)
|
||||||
}
|
}
|
||||||
return BuildHourlyRenderContext(metadata, snapshot, hourly, collected, derived)
|
return BuildHourlyRenderContext(identity, snapshot, hourly, collected, derived)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDailyContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
func buildDailyContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||||
daily, ok := generated.(Daily)
|
daily, ok := generated.(Daily)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("report template %q requires daily generated text for report %q", templateID, reportID)
|
return nil, fmt.Errorf("report template %q requires daily generated text for report %q", templateID, reportID)
|
||||||
}
|
}
|
||||||
return BuildDailyRenderContext(metadata, snapshot, daily, collected, derived)
|
return BuildDailyRenderContext(identity, snapshot, daily, collected, derived)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTodayContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
func buildTodayContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||||
today, ok := generated.(Today)
|
today, ok := generated.(Today)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("report template %q requires today generated text for report %q", templateID, reportID)
|
return nil, fmt.Errorf("report template %q requires today generated text for report %q", templateID, reportID)
|
||||||
}
|
}
|
||||||
return BuildTodayRenderContext(metadata, snapshot, today, collected, derived)
|
return BuildTodayRenderContext(identity, snapshot, today, collected, derived)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildTomorrowContext(reportID report.ID, templateID string, metadata briefing.Metadata, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
func buildTomorrowContext(reportID report.ID, templateID string, identity briefing.PreparedIdentity, snapshot module.Snapshot, collected facts.CollectedFacts, derived facts.DerivedFacts, generated any) (any, error) {
|
||||||
tomorrow, ok := generated.(Tomorrow)
|
tomorrow, ok := generated.(Tomorrow)
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", templateID, reportID)
|
return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", templateID, reportID)
|
||||||
}
|
}
|
||||||
return BuildTomorrowRenderContext(metadata, snapshot, tomorrow, collected, derived)
|
return BuildTomorrowRenderContext(identity, snapshot, tomorrow, collected, derived)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -188,15 +188,15 @@ type dayStyleTemplateModules struct {
|
|||||||
WeatherStory *briefing.WeatherStoryModule
|
WeatherStory *briefing.WeatherStoryModule
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Hourly, collected facts.CollectedFacts, derived facts.DerivedFacts) (HourlyRenderContext, error) {
|
func BuildHourlyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Hourly, collected facts.CollectedFacts, derived facts.DerivedFacts) (HourlyRenderContext, error) {
|
||||||
location, err := timeutil.LoadLocation(metadata.Timezone)
|
location, err := timeutil.LoadLocation(identity.Timezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err)
|
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: %w", err)
|
||||||
}
|
}
|
||||||
if metadata.GeneratedAt.IsZero() {
|
if identity.GeneratedAt.IsZero() {
|
||||||
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: generatedAt is required")
|
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: generatedAt is required")
|
||||||
}
|
}
|
||||||
if !metadata.ValidPeriod.IsValid() {
|
if !identity.ValidPeriod.IsValid() {
|
||||||
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: valid period is required")
|
return HourlyRenderContext{}, fmt.Errorf("build hourly render context: valid period is required")
|
||||||
}
|
}
|
||||||
modules, err := hourlyTemplateModules(snapshot)
|
modules, err := hourlyTemplateModules(snapshot)
|
||||||
@@ -206,12 +206,12 @@ func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapsh
|
|||||||
return HourlyRenderContext{
|
return HourlyRenderContext{
|
||||||
Report: HourlyReportContext{
|
Report: HourlyReportContext{
|
||||||
Title: "Hourly Report",
|
Title: "Hourly Report",
|
||||||
LocationName: locationName(metadata),
|
LocationName: locationName(identity),
|
||||||
GeneratedAt: metadata.GeneratedAt,
|
GeneratedAt: identity.GeneratedAt,
|
||||||
GeneratedAtLabel: generatedAtLabel(metadata.GeneratedAt, location),
|
GeneratedAtLabel: generatedAtLabel(identity.GeneratedAt, location),
|
||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: identity.ValidPeriod,
|
||||||
ValidPeriodLabel: periodLabel(metadata.ValidPeriod, location),
|
ValidPeriodLabel: periodLabel(identity.ValidPeriod, location),
|
||||||
Timezone: metadata.Timezone,
|
Timezone: identity.Timezone,
|
||||||
},
|
},
|
||||||
GeneratedText: generated,
|
GeneratedText: generated,
|
||||||
Modules: modules,
|
Modules: modules,
|
||||||
@@ -220,8 +220,8 @@ func BuildHourlyRenderContext(metadata briefing.Metadata, snapshot module.Snapsh
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildDailyRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Daily, collected facts.CollectedFacts, derived facts.DerivedFacts) (DailyRenderContext, error) {
|
func BuildDailyRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Daily, collected facts.CollectedFacts, derived facts.DerivedFacts) (DailyRenderContext, error) {
|
||||||
reportContext, err := buildDayStyleReportContext(metadata, "daily", func(dayName string) string {
|
reportContext, err := buildDayStyleReportContext(identity, "daily", func(dayName string) string {
|
||||||
return dayName + "'s Weather"
|
return dayName + "'s Weather"
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -240,8 +240,8 @@ func BuildDailyRenderContext(metadata briefing.Metadata, snapshot module.Snapsho
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildTodayRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Today, collected facts.CollectedFacts, derived facts.DerivedFacts) (TodayRenderContext, error) {
|
func BuildTodayRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Today, collected facts.CollectedFacts, derived facts.DerivedFacts) (TodayRenderContext, error) {
|
||||||
reportContext, err := buildDayStyleReportContext(metadata, "today", func(string) string {
|
reportContext, err := buildDayStyleReportContext(identity, "today", func(string) string {
|
||||||
return "Today's Weather"
|
return "Today's Weather"
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -260,8 +260,8 @@ func BuildTodayRenderContext(metadata briefing.Metadata, snapshot module.Snapsho
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildTomorrowRenderContext(metadata briefing.Metadata, snapshot module.Snapshot, generated Tomorrow, collected facts.CollectedFacts, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
|
func BuildTomorrowRenderContext(identity briefing.PreparedIdentity, snapshot module.Snapshot, generated Tomorrow, collected facts.CollectedFacts, derived facts.DerivedFacts) (TomorrowRenderContext, error) {
|
||||||
reportContext, err := buildDayStyleReportContext(metadata, "tomorrow", func(dayName string) string {
|
reportContext, err := buildDayStyleReportContext(identity, "tomorrow", func(dayName string) string {
|
||||||
return dayName + "'s Weather"
|
return dayName + "'s Weather"
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -280,29 +280,29 @@ func BuildTomorrowRenderContext(metadata briefing.Metadata, snapshot module.Snap
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildDayStyleReportContext(metadata briefing.Metadata, name string, title func(string) string) (dayStyleReportContext, error) {
|
func buildDayStyleReportContext(identity briefing.PreparedIdentity, name string, title func(string) string) (dayStyleReportContext, error) {
|
||||||
location, err := timeutil.LoadLocation(metadata.Timezone)
|
location, err := timeutil.LoadLocation(identity.Timezone)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return dayStyleReportContext{}, fmt.Errorf("build %s render context: %w", name, err)
|
return dayStyleReportContext{}, fmt.Errorf("build %s render context: %w", name, err)
|
||||||
}
|
}
|
||||||
if metadata.GeneratedAt.IsZero() {
|
if identity.GeneratedAt.IsZero() {
|
||||||
return dayStyleReportContext{}, fmt.Errorf("build %s render context: generatedAt is required", name)
|
return dayStyleReportContext{}, fmt.Errorf("build %s render context: generatedAt is required", name)
|
||||||
}
|
}
|
||||||
if !metadata.ValidPeriod.IsValid() {
|
if !identity.ValidPeriod.IsValid() {
|
||||||
return dayStyleReportContext{}, fmt.Errorf("build %s render context: valid period is required", name)
|
return dayStyleReportContext{}, fmt.Errorf("build %s render context: valid period is required", name)
|
||||||
}
|
}
|
||||||
|
|
||||||
forecastDate := metadata.ValidPeriod.Start.In(location)
|
forecastDate := identity.ValidPeriod.Start.In(location)
|
||||||
forecastDayName := forecastDate.Format("Monday")
|
forecastDayName := forecastDate.Format("Monday")
|
||||||
return dayStyleReportContext{
|
return dayStyleReportContext{
|
||||||
Title: title(forecastDayName),
|
Title: title(forecastDayName),
|
||||||
ForecastDate: forecastDate,
|
ForecastDate: forecastDate,
|
||||||
ForecastDateLabel: forecastDate.Format("Monday, January 2, 2006"),
|
ForecastDateLabel: forecastDate.Format("Monday, January 2, 2006"),
|
||||||
ForecastDayName: forecastDayName,
|
ForecastDayName: forecastDayName,
|
||||||
GeneratedAt: metadata.GeneratedAt,
|
GeneratedAt: identity.GeneratedAt,
|
||||||
GeneratedAtLabel: generatedAtLabel(metadata.GeneratedAt, location),
|
GeneratedAtLabel: generatedAtLabel(identity.GeneratedAt, location),
|
||||||
ValidPeriod: metadata.ValidPeriod,
|
ValidPeriod: identity.ValidPeriod,
|
||||||
Timezone: metadata.Timezone,
|
Timezone: identity.Timezone,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -706,20 +706,20 @@ func daypartModuleKeyCandidates(daypart forecast.DaypartSummary) []string {
|
|||||||
return []string{key, daypart.Period.Start.Format(timeutil.DateLayout) + "_" + key}
|
return []string{key, daypart.Period.Start.Format(timeutil.DateLayout) + "_" + key}
|
||||||
}
|
}
|
||||||
|
|
||||||
func locationName(metadata briefing.Metadata) string {
|
func locationName(identity briefing.PreparedIdentity) string {
|
||||||
if metadata.Location != nil {
|
if identity.Location != nil {
|
||||||
if metadata.Location.Name != "" && metadata.Location.Region != "" {
|
if identity.Location.Name != "" && identity.Location.Region != "" {
|
||||||
return metadata.Location.Name + ", " + metadata.Location.Region
|
return identity.Location.Name + ", " + identity.Location.Region
|
||||||
}
|
}
|
||||||
if metadata.Location.Name != "" {
|
if identity.Location.Name != "" {
|
||||||
return metadata.Location.Name
|
return identity.Location.Name
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if metadata.SourceLocation != "" {
|
if identity.SourceLocation != "" {
|
||||||
return metadata.SourceLocation
|
return identity.SourceLocation
|
||||||
}
|
}
|
||||||
if metadata.SourceLocationID != "" {
|
if identity.SourceLocationID != "" {
|
||||||
return metadata.SourceLocationID
|
return identity.SourceLocationID
|
||||||
}
|
}
|
||||||
return "Unknown location"
|
return "Unknown location"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -741,9 +741,9 @@ func TestBuildTomorrowRenderContextAllowsOmittedOptionalModules(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testMetadata() briefing.Metadata {
|
func testMetadata() briefing.PreparedIdentity {
|
||||||
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
generatedAt := time.Date(2026, 5, 29, 13, 30, 0, 0, time.UTC)
|
||||||
return briefing.Metadata{
|
return briefing.PreparedIdentity{
|
||||||
RunID: "run-1",
|
RunID: "run-1",
|
||||||
ReportID: report.Hourly,
|
ReportID: report.Hourly,
|
||||||
PromptID: "weather.hourly_generated_text",
|
PromptID: "weather.hourly_generated_text",
|
||||||
@@ -761,9 +761,9 @@ func testMetadata() briefing.Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTomorrowMetadata() briefing.Metadata {
|
func testTomorrowMetadata() briefing.PreparedIdentity {
|
||||||
generatedAt := time.Date(2026, 6, 14, 14, 14, 0, 0, time.UTC)
|
generatedAt := time.Date(2026, 6, 14, 14, 14, 0, 0, time.UTC)
|
||||||
return briefing.Metadata{
|
return briefing.PreparedIdentity{
|
||||||
RunID: "run-tomorrow",
|
RunID: "run-tomorrow",
|
||||||
ReportID: report.Tomorrow,
|
ReportID: report.Tomorrow,
|
||||||
PromptID: "weather.tomorrow_generated_text",
|
PromptID: "weather.tomorrow_generated_text",
|
||||||
@@ -781,9 +781,9 @@ func testTomorrowMetadata() briefing.Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDailyMetadata() briefing.Metadata {
|
func testDailyMetadata() briefing.PreparedIdentity {
|
||||||
generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC)
|
generatedAt := time.Date(2026, 6, 13, 14, 14, 0, 0, time.UTC)
|
||||||
return briefing.Metadata{
|
return briefing.PreparedIdentity{
|
||||||
RunID: "run-daily",
|
RunID: "run-daily",
|
||||||
ReportID: report.Daily,
|
ReportID: report.Daily,
|
||||||
PromptID: "weather.daily_generated_text",
|
PromptID: "weather.daily_generated_text",
|
||||||
@@ -801,9 +801,9 @@ func testDailyMetadata() briefing.Metadata {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTodayMetadata() briefing.Metadata {
|
func testTodayMetadata() briefing.PreparedIdentity {
|
||||||
generatedAt := time.Date(2026, 6, 15, 12, 14, 0, 0, time.UTC)
|
generatedAt := time.Date(2026, 6, 15, 12, 14, 0, 0, time.UTC)
|
||||||
return briefing.Metadata{
|
return briefing.PreparedIdentity{
|
||||||
RunID: "run-today",
|
RunID: "run-today",
|
||||||
ReportID: report.Today,
|
ReportID: report.Today,
|
||||||
PromptID: "weather.today_generated_text",
|
PromptID: "weather.today_generated_text",
|
||||||
|
|||||||
Reference in New Issue
Block a user