Route Tomorrow through generated text rendering

This commit is contained in:
2026-06-14 23:44:31 +00:00
parent ddda42453f
commit 0b41773017
5 changed files with 134 additions and 28 deletions

View File

@@ -566,6 +566,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
metadata: metadata,
metadataPath: metadataPath,
preflightPath: preflightPath,
priorSnapshot: priorSnapshot,
recentChanges: recentChanges,
renderResult: renderResult,
renderer: renderer,
@@ -646,6 +647,7 @@ type generatedReportRequest struct {
metadata state.Metadata
metadataPath string
preflightPath string
priorSnapshot *state.PriorSnapshot
recentChanges []changes.Change
renderResult *scriptorium.RenderResult
renderer Renderer
@@ -755,6 +757,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
NotificationPath: notificationPath,
Metadata: req.metadata,
MetadataPath: metadataPath,
PriorSnapshot: req.priorSnapshot,
RecentChanges: req.recentChanges,
RenderResult: req.renderResult,
StructuredRunResult: structuredResult,
@@ -1129,6 +1132,12 @@ func buildRenderContext(definition report.Definition, metadata briefing.Metadata
return nil, fmt.Errorf("report template %q requires hourly generated text for report %q", definition.TemplateID, definition.ID)
}
return generatedtext.BuildHourlyRenderContext(metadata, snapshot, hourly, reportFacts.Collected, reportFacts.Derived)
case "tomorrow":
tomorrow, ok := generated.(generatedtext.Tomorrow)
if !ok {
return nil, fmt.Errorf("report template %q requires tomorrow generated text for report %q", definition.TemplateID, definition.ID)
}
return generatedtext.BuildTomorrowRenderContext(metadata, snapshot, tomorrow, reportFacts.Collected, reportFacts.Derived)
default:
return nil, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
}

View File

@@ -813,7 +813,6 @@ func TestGeneratedTextValidationDispatchSupportsKnownSchemas(t *testing.T) {
}
tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow)
tomorrowDefinition.GeneratedTextSchemaID = "tomorrow"
tomorrow, normalized, err := validateGeneratedText(tomorrowDefinition, []byte(`{
"summary": " Storms become more likely tomorrow. ",
"forecast_discussion": [" A front will keep showers in the forecast. ", ""]
@@ -842,6 +841,18 @@ func TestBuildRenderContextRejectsMismatchedGeneratedText(t *testing.T) {
if !strings.Contains(err.Error(), `requires hourly generated text`) {
t.Fatalf("buildRenderContext() error = %v, want hourly generated text requirement", err)
}
tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow)
_, err = buildRenderContext(tomorrowDefinition, briefing.Metadata{}, module.Snapshot{}, ReportFacts{}, generatedtext.Hourly{
Summary: "Storm chances increase.",
ForecastDiscussion: "A front will keep showers in the forecast.",
})
if err == nil {
t.Fatal("buildRenderContext() error = nil, want tomorrow type mismatch")
}
if !strings.Contains(err.Error(), `requires tomorrow generated text`) {
t.Fatalf("buildRenderContext() error = %v, want tomorrow generated text requirement", err)
}
}
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
@@ -1275,9 +1286,9 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
t.Fatalf("ResolveGenerate() error = %v", err)
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: "# Tomorrow Planning Brief\n",
renderResult: &scriptorium.RenderResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
structuredRunBody: validTomorrowGeneratedTextJSON(),
}
result, err := GenerateReport(context.Background(), ReportRequest{
@@ -1288,10 +1299,22 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
if err != nil {
t.Fatalf("GenerateReport() error = %v", err)
}
if renderer.runCalls != 0 {
t.Fatalf("markdown run calls = %d, want none", renderer.runCalls)
}
if renderer.structuredRunCalls != 1 {
t.Fatalf("structured run calls = %d, want 1", renderer.structuredRunCalls)
}
if renderer.structuredRunRequest.OutputPath != result.GeneratedTextRawPath {
t.Fatalf("structured run OutputPath = %q, want %q", renderer.structuredRunRequest.OutputPath, result.GeneratedTextRawPath)
}
if result.Metadata.ReportID != report.Tomorrow || result.Metadata.Variant != "tomorrow" {
t.Fatalf("metadata report/variant = %q/%q, want tomorrow", result.Metadata.ReportID, result.Metadata.Variant)
}
if result.Metadata.GeneratedTextSchemaID != "tomorrow" || result.Metadata.GeneratedTextPath != result.GeneratedTextPath || result.Metadata.RenderContextPath != result.RenderContextPath || result.Metadata.RenderedReportPath != result.ReportPath {
t.Fatalf("metadata generated-text links = %#v, want tomorrow generated-text artifacts", result.Metadata)
}
dailySummary, ok, err := module.StanzaValue[map[string]any](result.ModuleSnapshot, "derived_daily_summary")
if err != nil {
t.Fatalf("decode daily summary: %v", err)
@@ -1305,6 +1328,25 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
if !strings.Contains(filepath.Base(result.ReportPath), "tomorrow") {
t.Fatalf("ReportPath = %q, want managed tomorrow report path", result.ReportPath)
}
assertPathsExist(t, result.GeneratedTextRawPath, result.GeneratedTextResultPath, result.GeneratedTextPath, result.RenderContextPath, result.ReportPath)
renderContext, err := os.ReadFile(result.RenderContextPath)
if err != nil {
t.Fatalf("read render context: %v", err)
}
for _, want := range []string{`"Title": "Saturday's Weather"`, `"GeneratedText": {`, `"forecast_discussion": [`, `"Dayparts": [`} {
if !strings.Contains(string(renderContext), want) {
t.Fatalf("render context missing %q:\n%s", want, string(renderContext))
}
}
reportData, err := os.ReadFile(result.ReportPath)
if err != nil {
t.Fatalf("read report: %v", err)
}
for _, want := range []string{"# Saturday's Weather", "## Daypart Forecast", "## Forecast Discussion", "Tomorrow starts with showers before improving."} {
if !strings.Contains(string(reportData), want) {
t.Fatalf("tomorrow report missing %q:\n%s", want, string(reportData))
}
}
}
func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) {
@@ -1334,9 +1376,9 @@ func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) {
t.Fatalf("ResolveGenerate(current) error = %v", err)
}
renderer := &recordingRenderer{
renderResult: &scriptorium.RenderResult{ExitCode: 0},
runResult: &scriptorium.RunResult{ExitCode: 0},
runBody: "# Tomorrow Planning Brief\n",
renderResult: &scriptorium.RenderResult{ExitCode: 0},
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
structuredRunBody: validTomorrowGeneratedTextJSON(),
}
result, err := GenerateReport(context.Background(), ReportRequest{
@@ -1671,7 +1713,7 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
name: "tomorrow",
kind: ReportTomorrow,
wantID: report.Tomorrow,
wantPrompt: "weather.daily_report",
wantPrompt: "weather.tomorrow_generated_text",
wantStart: "2026-05-30T00:00:00-05:00",
wantEnd: "2026-05-31T00:00:00-05:00",
requestDate: time.Time{},
@@ -2109,6 +2151,10 @@ func validHourlyGeneratedTextJSON() string {
return `{"summary":"Storm chances increase through late morning.","forecast_discussion":"A front will keep the region unsettled.","precipitation_timing":"A cold front is moving into the region.","confidence":"Medium"}`
}
func validTomorrowGeneratedTextJSON() string {
return `{"summary":"Tomorrow starts with showers before improving.","forecast_discussion":["Morning showers should taper as drier air arrives.","Afternoon conditions trend quieter."],"precipitation_timing":"The best rain chance is during the morning."}`
}
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
t.Helper()
cfg.Workspace.Root = t.TempDir()
@@ -2450,6 +2496,13 @@ func (r *selectiveRenderer) Run(_ context.Context, req scriptorium.RunRequest) (
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
r.structuredRunCalls++
body := validHourlyGeneratedTextJSON()
if req.PromptID == "weather.tomorrow_generated_text" {
body = validTomorrowGeneratedTextJSON()
}
if err := os.WriteFile(req.OutputPath, []byte(body), 0o600); err != nil {
return nil, err
}
return &scriptorium.StructuredRunResult{ExitCode: 0, OutputPath: req.OutputPath}, nil
}

View File

@@ -130,7 +130,7 @@ func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) {
if err != nil {
t.Fatalf("read report: %v", err)
}
if !strings.Contains(string(report), "# Daily Report") {
if !strings.Contains(string(report), "# Saturday's Weather") {
t.Fatalf("report output missing markdown:\n%s", string(report))
}
dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml")
@@ -1176,13 +1176,31 @@ if [ "$1" = "render" ]; then
fi
if [ "$1" = "run" ]; then
out=""
prompt=""
while [ "$#" -gt 0 ]; do
if [ "$1" = "--out" ]; then
shift
out="$1"
elif [ "$1" = "--prompt" ]; then
shift
prompt="$1"
fi
shift
done
if [ "$prompt" = "weather.tomorrow_generated_text" ]; then
cat > "$out" <<'JSON'
{
"summary": "Tomorrow starts with showers before improving.",
"forecast_discussion": [
"Morning showers should taper as drier air arrives.",
"Afternoon conditions trend quieter."
],
"precipitation_timing": "The best rain chance is during the morning."
}
JSON
printf 'wrote generated text\n' >&2
exit 0
fi
printf '# Daily Report\n\nGenerated by fake scriptorium.\n' > "$out"
printf 'wrote report\n' >&2
exit 0
@@ -1225,6 +1243,20 @@ if [ "$1" = "run" ]; then
"precipitation_timing": "A cold front is moving into the region.",
"confidence": "Medium"
}
JSON
printf 'wrote generated text\n' >&2
exit 0
fi
if [ "$prompt" = "weather.tomorrow_generated_text" ]; then
cat > "$out" <<'JSON'
{
"summary": "Tomorrow starts with showers before improving.",
"forecast_discussion": [
"Morning showers should taper as drier air arrives.",
"Afternoon conditions trend quieter."
],
"precipitation_timing": "The best rain chance is during the morning."
}
JSON
printf 'wrote generated text\n' >&2
exit 0

View File

@@ -24,18 +24,20 @@ func dailyTodayDefinition() Definition {
func tomorrowDefinition() Definition {
return Definition{
ID: Tomorrow,
Name: "Tomorrow Report",
PromptID: "weather.daily_report",
GenerationMode: GenerationModeScriptoriumMarkdown,
ComparisonStrategy: CompareSameValidDate,
ArtifactGroup: "tomorrow",
BatchOutputName: "tomorrow.md",
Generated: true,
CompatiblePriorIDs: []ID{Tomorrow},
Modules: tomorrowModules(),
Evening: true,
resolve: resolveTomorrow,
ID: Tomorrow,
Name: "Tomorrow Report",
PromptID: "weather.tomorrow_generated_text",
GenerationMode: GenerationModeGeneratedTextTemplate,
TemplateID: "tomorrow",
GeneratedTextSchemaID: "tomorrow",
ComparisonStrategy: CompareSameValidDate,
ArtifactGroup: "tomorrow",
BatchOutputName: "tomorrow.md",
Generated: true,
CompatiblePriorIDs: []ID{Tomorrow},
Modules: tomorrowModules(),
Evening: true,
resolve: resolveTomorrow,
}
}

View File

@@ -42,8 +42,17 @@ func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
t.Fatalf("Resolve() error = %v", err)
}
assertPeriod(t, resolved.ValidPeriod, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00")
if resolved.Definition.PromptID != "weather.daily_report" {
t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID)
if resolved.Definition.PromptID != "weather.tomorrow_generated_text" {
t.Fatalf("PromptID = %q, want weather.tomorrow_generated_text", resolved.Definition.PromptID)
}
if resolved.Definition.GenerationMode != GenerationModeGeneratedTextTemplate {
t.Fatalf("GenerationMode = %q, want generated_text_template", resolved.Definition.GenerationMode)
}
if resolved.Definition.TemplateID != "tomorrow" {
t.Fatalf("TemplateID = %q, want tomorrow", resolved.Definition.TemplateID)
}
if resolved.Definition.GeneratedTextSchemaID != "tomorrow" {
t.Fatalf("GeneratedTextSchemaID = %q, want tomorrow", resolved.Definition.GeneratedTextSchemaID)
}
}
@@ -268,15 +277,16 @@ func TestRegistryDefinitionsDeclareGenerationMetadata(t *testing.T) {
if !definition.Generated {
continue
}
if definition.ID == Hourly {
if definition.ID == Hourly || definition.ID == Tomorrow {
wantTemplate := string(definition.ID)
if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
}
if definition.TemplateID != "hourly" {
t.Fatalf("%s TemplateID = %q, want hourly", definition.ID, definition.TemplateID)
if definition.TemplateID != wantTemplate {
t.Fatalf("%s TemplateID = %q, want %s", definition.ID, definition.TemplateID, wantTemplate)
}
if definition.GeneratedTextSchemaID != "hourly" {
t.Fatalf("%s GeneratedTextSchemaID = %q, want hourly", definition.ID, definition.GeneratedTextSchemaID)
if definition.GeneratedTextSchemaID != wantTemplate {
t.Fatalf("%s GeneratedTextSchemaID = %q, want %s", definition.ID, definition.GeneratedTextSchemaID, wantTemplate)
}
continue
}