Route Tomorrow through generated text rendering
This commit is contained in:
@@ -566,6 +566,7 @@ func GenerateReport(ctx context.Context, req ReportRequest) (*ReportResult, erro
|
|||||||
metadata: metadata,
|
metadata: metadata,
|
||||||
metadataPath: metadataPath,
|
metadataPath: metadataPath,
|
||||||
preflightPath: preflightPath,
|
preflightPath: preflightPath,
|
||||||
|
priorSnapshot: priorSnapshot,
|
||||||
recentChanges: recentChanges,
|
recentChanges: recentChanges,
|
||||||
renderResult: renderResult,
|
renderResult: renderResult,
|
||||||
renderer: renderer,
|
renderer: renderer,
|
||||||
@@ -646,6 +647,7 @@ type generatedReportRequest struct {
|
|||||||
metadata state.Metadata
|
metadata state.Metadata
|
||||||
metadataPath string
|
metadataPath string
|
||||||
preflightPath string
|
preflightPath string
|
||||||
|
priorSnapshot *state.PriorSnapshot
|
||||||
recentChanges []changes.Change
|
recentChanges []changes.Change
|
||||||
renderResult *scriptorium.RenderResult
|
renderResult *scriptorium.RenderResult
|
||||||
renderer Renderer
|
renderer Renderer
|
||||||
@@ -755,6 +757,7 @@ func generateTextTemplateReport(ctx context.Context, req generatedReportRequest)
|
|||||||
NotificationPath: notificationPath,
|
NotificationPath: notificationPath,
|
||||||
Metadata: req.metadata,
|
Metadata: req.metadata,
|
||||||
MetadataPath: metadataPath,
|
MetadataPath: metadataPath,
|
||||||
|
PriorSnapshot: req.priorSnapshot,
|
||||||
RecentChanges: req.recentChanges,
|
RecentChanges: req.recentChanges,
|
||||||
RenderResult: req.renderResult,
|
RenderResult: req.renderResult,
|
||||||
StructuredRunResult: structuredResult,
|
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 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)
|
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:
|
default:
|
||||||
return nil, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
|
return nil, fmt.Errorf("report template %q is not supported for report %q", definition.TemplateID, definition.ID)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -813,7 +813,6 @@ func TestGeneratedTextValidationDispatchSupportsKnownSchemas(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow)
|
tomorrowDefinition := report.DefaultRegistry().MustLookup(report.Tomorrow)
|
||||||
tomorrowDefinition.GeneratedTextSchemaID = "tomorrow"
|
|
||||||
tomorrow, normalized, err := validateGeneratedText(tomorrowDefinition, []byte(`{
|
tomorrow, normalized, err := validateGeneratedText(tomorrowDefinition, []byte(`{
|
||||||
"summary": " Storms become more likely tomorrow. ",
|
"summary": " Storms become more likely tomorrow. ",
|
||||||
"forecast_discussion": [" A front will keep showers in the forecast. ", ""]
|
"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`) {
|
if !strings.Contains(err.Error(), `requires hourly generated text`) {
|
||||||
t.Fatalf("buildRenderContext() error = %v, want hourly generated text requirement", err)
|
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) {
|
func TestGenerateReportDisabledNotificationDoesNotCallNotifier(t *testing.T) {
|
||||||
@@ -1276,8 +1287,8 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
|||||||
}
|
}
|
||||||
renderer := &recordingRenderer{
|
renderer := &recordingRenderer{
|
||||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||||
runBody: "# Tomorrow Planning Brief\n",
|
structuredRunBody: validTomorrowGeneratedTextJSON(),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||||
@@ -1288,10 +1299,22 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("GenerateReport() error = %v", err)
|
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" {
|
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)
|
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")
|
dailySummary, ok, err := module.StanzaValue[map[string]any](result.ModuleSnapshot, "derived_daily_summary")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("decode daily summary: %v", err)
|
t.Fatalf("decode daily summary: %v", err)
|
||||||
@@ -1305,6 +1328,25 @@ func TestGenerateTomorrowReportUsesTomorrowBriefingDate(t *testing.T) {
|
|||||||
if !strings.Contains(filepath.Base(result.ReportPath), "tomorrow") {
|
if !strings.Contains(filepath.Base(result.ReportPath), "tomorrow") {
|
||||||
t.Fatalf("ReportPath = %q, want managed tomorrow report path", result.ReportPath)
|
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) {
|
func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) {
|
||||||
@@ -1335,8 +1377,8 @@ func TestTomorrowReportCanCompareAgainstPriorTomorrowSnapshot(t *testing.T) {
|
|||||||
}
|
}
|
||||||
renderer := &recordingRenderer{
|
renderer := &recordingRenderer{
|
||||||
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
renderResult: &scriptorium.RenderResult{ExitCode: 0},
|
||||||
runResult: &scriptorium.RunResult{ExitCode: 0},
|
structuredRunResult: &scriptorium.StructuredRunResult{ExitCode: 0},
|
||||||
runBody: "# Tomorrow Planning Brief\n",
|
structuredRunBody: validTomorrowGeneratedTextJSON(),
|
||||||
}
|
}
|
||||||
|
|
||||||
result, err := GenerateReport(context.Background(), ReportRequest{
|
result, err := GenerateReport(context.Background(), ReportRequest{
|
||||||
@@ -1671,7 +1713,7 @@ func TestResolveGenerateMapsCommandToReportDefinition(t *testing.T) {
|
|||||||
name: "tomorrow",
|
name: "tomorrow",
|
||||||
kind: ReportTomorrow,
|
kind: ReportTomorrow,
|
||||||
wantID: report.Tomorrow,
|
wantID: report.Tomorrow,
|
||||||
wantPrompt: "weather.daily_report",
|
wantPrompt: "weather.tomorrow_generated_text",
|
||||||
wantStart: "2026-05-30T00:00:00-05:00",
|
wantStart: "2026-05-30T00:00:00-05:00",
|
||||||
wantEnd: "2026-05-31T00:00:00-05:00",
|
wantEnd: "2026-05-31T00:00:00-05:00",
|
||||||
requestDate: time.Time{},
|
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"}`
|
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 {
|
func generateDailyReportForTest(t *testing.T, cfg config.Config) *ReportResult {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
cfg.Workspace.Root = t.TempDir()
|
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) {
|
func (r *selectiveRenderer) StructuredRun(_ context.Context, req scriptorium.StructuredRunRequest) (*scriptorium.StructuredRunResult, error) {
|
||||||
r.structuredRunCalls++
|
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
|
return &scriptorium.StructuredRunResult{ExitCode: 0, OutputPath: req.OutputPath}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -130,7 +130,7 @@ func TestRunGenerateTomorrowWritesMarkdownReport(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("read report: %v", err)
|
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))
|
t.Fatalf("report output missing markdown:\n%s", string(report))
|
||||||
}
|
}
|
||||||
dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml")
|
dataPackagePath := oneArtifact(t, workspaceRoot, "data-packages", "tomorrow", "2026-05-30", "*.data_package.yaml")
|
||||||
@@ -1176,13 +1176,31 @@ if [ "$1" = "render" ]; then
|
|||||||
fi
|
fi
|
||||||
if [ "$1" = "run" ]; then
|
if [ "$1" = "run" ]; then
|
||||||
out=""
|
out=""
|
||||||
|
prompt=""
|
||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
if [ "$1" = "--out" ]; then
|
if [ "$1" = "--out" ]; then
|
||||||
shift
|
shift
|
||||||
out="$1"
|
out="$1"
|
||||||
|
elif [ "$1" = "--prompt" ]; then
|
||||||
|
shift
|
||||||
|
prompt="$1"
|
||||||
fi
|
fi
|
||||||
shift
|
shift
|
||||||
done
|
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 '# Daily Report\n\nGenerated by fake scriptorium.\n' > "$out"
|
||||||
printf 'wrote report\n' >&2
|
printf 'wrote report\n' >&2
|
||||||
exit 0
|
exit 0
|
||||||
@@ -1225,6 +1243,20 @@ if [ "$1" = "run" ]; then
|
|||||||
"precipitation_timing": "A cold front is moving into the region.",
|
"precipitation_timing": "A cold front is moving into the region.",
|
||||||
"confidence": "Medium"
|
"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
|
JSON
|
||||||
printf 'wrote generated text\n' >&2
|
printf 'wrote generated text\n' >&2
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -26,8 +26,10 @@ func tomorrowDefinition() Definition {
|
|||||||
return Definition{
|
return Definition{
|
||||||
ID: Tomorrow,
|
ID: Tomorrow,
|
||||||
Name: "Tomorrow Report",
|
Name: "Tomorrow Report",
|
||||||
PromptID: "weather.daily_report",
|
PromptID: "weather.tomorrow_generated_text",
|
||||||
GenerationMode: GenerationModeScriptoriumMarkdown,
|
GenerationMode: GenerationModeGeneratedTextTemplate,
|
||||||
|
TemplateID: "tomorrow",
|
||||||
|
GeneratedTextSchemaID: "tomorrow",
|
||||||
ComparisonStrategy: CompareSameValidDate,
|
ComparisonStrategy: CompareSameValidDate,
|
||||||
ArtifactGroup: "tomorrow",
|
ArtifactGroup: "tomorrow",
|
||||||
BatchOutputName: "tomorrow.md",
|
BatchOutputName: "tomorrow.md",
|
||||||
|
|||||||
@@ -42,8 +42,17 @@ func TestTomorrowValidPeriodFromEveningGeneration(t *testing.T) {
|
|||||||
t.Fatalf("Resolve() error = %v", err)
|
t.Fatalf("Resolve() error = %v", err)
|
||||||
}
|
}
|
||||||
assertPeriod(t, resolved.ValidPeriod, "2026-05-30T00:00:00-05:00", "2026-05-31T00:00:00-05:00")
|
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" {
|
if resolved.Definition.PromptID != "weather.tomorrow_generated_text" {
|
||||||
t.Fatalf("PromptID = %q, want weather.daily_report", resolved.Definition.PromptID)
|
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 {
|
if !definition.Generated {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if definition.ID == Hourly {
|
if definition.ID == Hourly || definition.ID == Tomorrow {
|
||||||
|
wantTemplate := string(definition.ID)
|
||||||
if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
|
if definition.GenerationMode != GenerationModeGeneratedTextTemplate {
|
||||||
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
|
t.Fatalf("%s GenerationMode = %q, want %q", definition.ID, definition.GenerationMode, GenerationModeGeneratedTextTemplate)
|
||||||
}
|
}
|
||||||
if definition.TemplateID != "hourly" {
|
if definition.TemplateID != wantTemplate {
|
||||||
t.Fatalf("%s TemplateID = %q, want hourly", definition.ID, definition.TemplateID)
|
t.Fatalf("%s TemplateID = %q, want %s", definition.ID, definition.TemplateID, wantTemplate)
|
||||||
}
|
}
|
||||||
if definition.GeneratedTextSchemaID != "hourly" {
|
if definition.GeneratedTextSchemaID != wantTemplate {
|
||||||
t.Fatalf("%s GeneratedTextSchemaID = %q, want hourly", definition.ID, definition.GeneratedTextSchemaID)
|
t.Fatalf("%s GeneratedTextSchemaID = %q, want %s", definition.ID, definition.GeneratedTextSchemaID, wantTemplate)
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user