Route report projections through prepared identity

This commit is contained in:
2026-08-13 02:00:18 +00:00
parent e2dd8d0e29
commit 360c665a3e
15 changed files with 163 additions and 253 deletions

View File

@@ -66,6 +66,7 @@ type BatchRequest struct {
type ModuleSnapshotRequest struct {
Config config.Config
Resolved report.Resolved
Identity briefing.PreparedIdentity
}
type ReportFacts struct {
@@ -729,7 +730,12 @@ func BuildModuleSnapshotFromFacts(req ModuleSnapshotRequest, reportFacts ReportF
if err != nil {
return module.Snapshot{}, err
}
identity := req.Identity
if identity.ReportID == "" {
identity = briefing.BuildPreparedIdentity(briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected))
}
moduleContext := briefing.ModuleContext{
Identity: identity,
Resolved: req.Resolved,
Collected: reportFacts.Collected,
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{
RunID: metadata.RunID,
ReportID: metadata.ReportID,
Variant: metadata.Variant,
PromptID: metadata.PromptID,
GeneratedAt: metadata.GeneratedAt,
Timezone: metadata.Timezone,
ValidPeriod: metadata.ValidPeriod,
SourceWarnings: metadata.SourceWarnings,
RunID: identity.RunID,
ReportID: identity.ReportID,
Variant: identity.Variant,
PromptID: identity.PromptID,
GeneratedAt: identity.GeneratedAt,
Timezone: identity.Timezone,
ValidPeriod: identity.ValidPeriod,
SourceWarnings: identity.SourceWarnings,
}
}

View File

@@ -17,14 +17,13 @@ import (
// preparedReport contains the immutable deterministic inputs shared by prompt
// executions for one resolved report.
type preparedReport struct {
resolved report.Resolved
reportFacts ReportFacts
moduleSnapshot module.Snapshot
identity briefing.PreparedIdentity
briefingMetadata briefing.Metadata
sourceWarnings []weatherdata.SourceWarning
dataPackage []byte
handler generatedtext.Handler
resolved report.Resolved
reportFacts ReportFacts
moduleSnapshot module.Snapshot
identity briefing.PreparedIdentity
sourceWarnings []weatherdata.SourceWarning
dataPackage []byte
handler generatedtext.Handler
}
type prepareReportRequest struct {
@@ -57,12 +56,11 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
}
buildContext := briefingBuildContext(req.Config, req.Resolved, reportFacts.Collected)
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 {
return preparedReport{}, &preparationError{operation: "build module snapshot", err: err}
}
metadata := briefing.BuildMetadata(identity, buildContext.Bundle)
dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(metadata), Modules: moduleSnapshot})
dataPackage, err := promptinput.Build(promptinput.BuildRequest{Metadata: promptMetadata(identity), Modules: moduleSnapshot})
if err != nil {
return preparedReport{}, &preparationError{operation: "build data package", err: err}
}
@@ -87,19 +85,14 @@ func prepareReport(req prepareReportRequest) (preparedReport, error) {
if err != nil {
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{
resolved: cloneResolved(req.Resolved),
reportFacts: clonedFacts,
moduleSnapshot: clonedSnapshot,
identity: clonedIdentity,
briefingMetadata: clonedMetadata,
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
dataPackage: append([]byte(nil), serializedDataPackage...),
handler: handler,
resolved: cloneResolved(req.Resolved),
reportFacts: clonedFacts,
moduleSnapshot: clonedSnapshot,
identity: clonedIdentity,
sourceWarnings: append([]weatherdata.SourceWarning(nil), clonedIdentity.SourceWarnings...),
dataPackage: append([]byte(nil), serializedDataPackage...),
handler: handler,
}
return prepared, nil
}
@@ -127,20 +120,20 @@ func (p preparedReport) sourceWarningsCopy() []weatherdata.SourceWarning {
return append([]weatherdata.SourceWarning(nil), p.sourceWarnings...)
}
func (p preparedReport) renderInputs() (briefing.Metadata, module.Snapshot, ReportFacts, error) {
metadata, err := clonePreparedValue(p.briefingMetadata)
func (p preparedReport) renderInputs() (briefing.PreparedIdentity, module.Snapshot, ReportFacts, error) {
identity, err := clonePreparedValue(p.identity)
if err != nil {
return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
}
snapshot, err := clonePreparedValue(p.moduleSnapshot)
if err != nil {
return briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, err
return briefing.PreparedIdentity{}, module.Snapshot{}, ReportFacts{}, err
}
reportFacts, err := clonePreparedValue(p.reportFacts)
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) {

View File

@@ -31,19 +31,18 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
if err != nil {
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) {
t.Fatalf("prepared package/identity/metadata are not deterministic: %q/%#v/%#v", prepared.dataPackage, prepared.identity, prepared.briefingMetadata)
if len(prepared.dataPackage) == 0 || !bytes.Equal(prepared.dataPackage, repeated.dataPackage) || !reflect.DeepEqual(prepared.identity, repeated.identity) {
t.Fatalf("prepared package and identity are not deterministic: %q/%#v", prepared.dataPackage, prepared.identity)
}
originalDataPackage := append([]byte(nil), prepared.dataPackage...)
originalIdentity := prepared.identity
originalMetadata := prepared.briefingMetadata
originalWarnings := append([]weatherdata.SourceWarning(nil), prepared.sourceWarnings...)
metadata, snapshot, reportFacts, err := prepared.renderInputs()
identity, snapshot, reportFacts, err := prepared.renderInputs()
if err != nil {
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
reportFacts.Collected.Hourly.Periods[0].TextDescription = "consumer mutation"
bundle.Hourly.Periods[0].TextDescription = "mutated after preparation"
@@ -55,7 +54,7 @@ func TestPrepareReportBuildsImmutableDeterministicInputs(t *testing.T) {
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)
}
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()
bundle := generationBundle(t)
resolved, err := ResolveGenerate(GenerateRequest{
@@ -82,12 +81,14 @@ func TestPrepareReportSharesOneMetadataIdentity(t *testing.T) {
}
identity := prepared.identity
metadata := prepared.briefingMetadata
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) {
t.Fatalf("briefing metadata does not match prepared identity: %#v/%#v", metadata, identity)
renderIdentity, _, _, err := prepared.renderInputs()
if err != nil {
t.Fatalf("renderInputs() error = %v", err)
}
prompt := promptMetadata(metadata)
if !reflect.DeepEqual(renderIdentity, identity) {
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) {
t.Fatalf("prompt metadata does not match prepared identity: %#v/%#v", prompt, identity)
}

View File

@@ -115,11 +115,11 @@ func executePreparedProfile(ctx context.Context, req profileExecutionRequest) (p
if err != nil {
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 {
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 {
return outcome, nil, &profileExecutionError{operation: "build render context", err: err}
}