Skip to content

Commit 3458ced

Browse files
spboyerCopilot
andauthored
feat: schema versioning policy (closes #368) (#382)
* feat: add schema versioning policy #368 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: harden schema migration guidance #368 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * test: stabilize inline grader duration check #368 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: detect schema artifact json shape #368 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * fix: skip incompatible stored results #368 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f0770af commit 3458ced

40 files changed

Lines changed: 899 additions & 55 deletions

README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,9 @@ waza grade eval.yaml --results results.json
146146
# Compare results across models
147147
waza compare results-gpt4.json results-sonnet.json
148148

149+
# Check whether a schema artifact needs migration
150+
waza migrate eval.yaml
151+
149152
# Generate eval coverage grid
150153
waza coverage --format markdown
151154

@@ -409,6 +412,15 @@ Compare results from multiple evaluation runs side by side — per-task score de
409412
|------|-------|-------------|
410413
| `--format <fmt>` | `-f` | Output format: `table` or `json` (default: `table`) |
411414

415+
### `waza migrate <file>`
416+
417+
Check a public schema artifact and migrate it to the current schema version when a future major schema requires it. The current schema is `1.0`, so v1 `eval.yaml` and `results.json` files are already current and no file changes are made.
418+
419+
```bash
420+
waza migrate eval.yaml
421+
waza migrate results.json
422+
```
423+
412424
### `waza coverage [root]`
413425

414426
Generate a skill-to-eval coverage grid showing which skills are fully covered, partially covered, or missing evals.
@@ -931,6 +943,7 @@ skills/ Example skills
931943
```yaml
932944
name: my-eval
933945
skill: my-skill
946+
schemaVersion: "1.0"
934947
version: "1.0"
935948
936949
config:
@@ -1002,6 +1015,8 @@ tasks:
10021015
# range: [1, 10] # Only include rows 1-10 (0-indexed, skips header)
10031016
```
10041017

1018+
`schemaVersion` uses `MAJOR.MINOR` format and defaults to `1.0` when omitted for backward compatibility. Readers allow same-major minor additions with warnings for unknown fields, but reject different majors with a hint to run `waza migrate <file>`.
1019+
10051020
### Custom Input Variables
10061021

10071022
Use the `inputs` section to define key-value variables available throughout your evaluation as `{{.Vars.key}}`:

cmd/waza/cmd_compare.go

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import (
44
"encoding/json"
55
"fmt"
66
"math"
7-
"os"
87
"strings"
98

109
"github.com/microsoft/waza/internal/models"
@@ -79,15 +78,7 @@ func compareCommandE(_ *cobra.Command, args []string) error {
7978
}
8079

8180
func loadOutcomeFile(path string) (*models.EvaluationOutcome, error) {
82-
data, err := os.ReadFile(path)
83-
if err != nil {
84-
return nil, err
85-
}
86-
var outcome models.EvaluationOutcome
87-
if err := json.Unmarshal(data, &outcome); err != nil {
88-
return nil, err
89-
}
90-
return &outcome, nil
81+
return models.LoadEvaluationOutcome(path)
9182
}
9283

9384
func buildComparisonReport(files []string, outcomes []*models.EvaluationOutcome) *comparisonReport {

cmd/waza/cmd_grade.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,8 @@ func runGrade(ctx context.Context, w, errW io.Writer, specPath, taskID, resultsF
8282
if readErr != nil {
8383
return fmt.Errorf("failed to read results file: %w", readErr)
8484
}
85-
var outcome models.EvaluationOutcome
86-
if jsonErr := json.Unmarshal(data, &outcome); jsonErr != nil {
85+
outcome, jsonErr := models.ParseEvaluationOutcome(data, resultsFile)
86+
if jsonErr != nil {
8787
return fmt.Errorf("failed to parse results JSON: %w", jsonErr)
8888
}
8989

@@ -181,7 +181,7 @@ func runGrade(ctx context.Context, w, errW io.Writer, specPath, taskID, resultsF
181181
}
182182
}
183183

184-
graded := orchestration.RegradeOutcome(&outcome, finalOutcomes, effectiveJudgeModel)
184+
graded := orchestration.RegradeOutcome(outcome, finalOutcomes, effectiveJudgeModel)
185185
if err := saveOutcome(graded, outputFile); err != nil {
186186
return fmt.Errorf("failed to save graded outcome: %w", err)
187187
}

cmd/waza/cmd_migrate.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"io"
6+
"os"
7+
"path/filepath"
8+
9+
"github.com/microsoft/waza/internal/models"
10+
"github.com/spf13/cobra"
11+
"gopkg.in/yaml.v3"
12+
)
13+
14+
func newMigrateCommand() *cobra.Command {
15+
cmd := &cobra.Command{
16+
Use: "migrate <file>",
17+
Short: "Migrate a waza schema artifact to the current schema version",
18+
Long: `Migrate a waza schema artifact to the current schema version.
19+
20+
The current schema version is 1.0, so v1 artifacts are already current and the
21+
command performs no file changes. Future major schema versions will add explicit
22+
migration steps here.`,
23+
Args: cobra.ExactArgs(1),
24+
RunE: func(cmd *cobra.Command, args []string) error {
25+
return runMigrate(cmd.OutOrStdout(), args[0])
26+
},
27+
}
28+
return cmd
29+
}
30+
31+
func runMigrate(out io.Writer, path string) error {
32+
data, err := os.ReadFile(path)
33+
if err != nil {
34+
return fmt.Errorf("reading %s: %w", path, err)
35+
}
36+
37+
artifact, version, err := readArtifactSchemaVersion(path, data)
38+
if err != nil {
39+
return err
40+
}
41+
42+
version, err = models.ValidateSchemaVersion(artifact, path, version)
43+
if err != nil {
44+
return err
45+
}
46+
47+
if version != models.CurrentSchemaVersion {
48+
_, err = fmt.Fprintf(out, "%s uses schemaVersion %s, which is compatible with waza schemaVersion %s; no migration needed.\n", artifact, version, models.CurrentSchemaVersion)
49+
return err
50+
}
51+
52+
_, err = fmt.Fprintf(out, "%s is already compatible with schemaVersion %s; no migration needed.\n", artifact, models.CurrentSchemaVersion)
53+
return err
54+
}
55+
56+
func readArtifactSchemaVersion(path string, data []byte) (artifact string, version string, err error) {
57+
switch filepath.Base(path) {
58+
case "eval.yaml", "eval.yml":
59+
artifact = "eval.yaml"
60+
var header struct {
61+
SchemaVersion string `yaml:"schemaVersion"`
62+
}
63+
if err := yaml.Unmarshal(data, &header); err != nil {
64+
return "", "", fmt.Errorf("parsing %s: %w", path, err)
65+
}
66+
return artifact, header.SchemaVersion, nil
67+
default:
68+
if filepath.Ext(path) == ".json" {
69+
version, ok, err := models.ProbeEvaluationOutcomeSchemaVersion(data)
70+
if err != nil {
71+
return "", "", fmt.Errorf("parsing %s: %w", path, err)
72+
}
73+
if !ok {
74+
return "", "", fmt.Errorf("unsupported JSON schema artifact %s: expected a results.json object with top-level eval_id, eval_name, summary, or tasks", path)
75+
}
76+
return "results.json", version, nil
77+
}
78+
}
79+
return "", "", fmt.Errorf("unsupported schema artifact %s: expected eval.yaml, eval.yml, or a JSON results artifact", path)
80+
}

cmd/waza/cmd_migrate_test.go

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
package main
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"path/filepath"
7+
"testing"
8+
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func TestMigrateCommandNoOpForCurrentMajor(t *testing.T) {
13+
dir := t.TempDir()
14+
path := filepath.Join(dir, "eval.yaml")
15+
require.NoError(t, os.WriteFile(path, []byte("schemaVersion: \"1.0\"\n"), 0o644))
16+
17+
var out bytes.Buffer
18+
err := runMigrate(&out, path)
19+
20+
require.NoError(t, err)
21+
require.Contains(t, out.String(), "already compatible with schemaVersion 1.0")
22+
}
23+
24+
func TestMigrateCommandRejectsIncompatibleMajor(t *testing.T) {
25+
dir := t.TempDir()
26+
path := filepath.Join(dir, "results.json")
27+
require.NoError(t, os.WriteFile(path, []byte(`{"schemaVersion":"2.0","tasks":[]}`), 0o644))
28+
29+
var out bytes.Buffer
30+
err := runMigrate(&out, path)
31+
32+
require.Error(t, err)
33+
require.Contains(t, err.Error(), `waza migrate <file>`)
34+
require.Empty(t, out.String())
35+
}
36+
37+
func TestMigrateCommandDefaultsMissingSchemaVersion(t *testing.T) {
38+
dir := t.TempDir()
39+
path := filepath.Join(dir, "results.json")
40+
require.NoError(t, os.WriteFile(path, []byte(`{"eval_id":"run-1"}`), 0o644))
41+
42+
var out bytes.Buffer
43+
err := runMigrate(&out, path)
44+
45+
require.NoError(t, err)
46+
require.Contains(t, out.String(), "already compatible with schemaVersion 1.0")
47+
}
48+
49+
func TestMigrateCommandRejectsUnknownJSONShape(t *testing.T) {
50+
dir := t.TempDir()
51+
path := filepath.Join(dir, "settings.json")
52+
require.NoError(t, os.WriteFile(path, []byte(`{"schemaVersion":"1.0","name":"not-results"}`), 0o644))
53+
54+
var out bytes.Buffer
55+
err := runMigrate(&out, path)
56+
57+
require.Error(t, err)
58+
require.Contains(t, err.Error(), "unsupported JSON schema artifact")
59+
require.Contains(t, err.Error(), "expected a results.json object")
60+
require.Empty(t, out.String())
61+
}
62+
63+
func TestMigrateCommandDetectsResultsJSONByShape(t *testing.T) {
64+
dir := t.TempDir()
65+
path := filepath.Join(dir, "custom-name.json")
66+
require.NoError(t, os.WriteFile(path, []byte(`{"schemaVersion":"1.0","tasks":[]}`), 0o644))
67+
68+
var out bytes.Buffer
69+
err := runMigrate(&out, path)
70+
71+
require.NoError(t, err)
72+
require.Contains(t, out.String(), "results.json is already compatible")
73+
}
74+
75+
func TestMigrateCommandMissingFile(t *testing.T) {
76+
var out bytes.Buffer
77+
err := runMigrate(&out, filepath.Join(t.TempDir(), "missing.json"))
78+
79+
require.Error(t, err)
80+
require.Contains(t, err.Error(), "reading")
81+
}

cmd/waza/root.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ performance against predefined test cases.`,
7070
cmd.AddCommand(newQualityCommand())
7171
cmd.AddCommand(newUpdateCommand())
7272
cmd.AddCommand(newSpecCommand())
73+
cmd.AddCommand(newMigrateCommand())
7374

7475
return cmd
7576
}

docs/PRD.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ Port existing Python waza functionality to Go for single-binary distribution.
6969
| E1-06 | As a developer, I can execute against Copilot SDK | Full integration with streaming responses |
7070
| E1-07 | As a developer, I can use verbose mode for debugging | Real-time conversation display |
7171
| E1-08 | As a developer, I can save transcripts for analysis | JSON log output with full conversation |
72+
| E1-09 | As a developer, public artifacts are schema-versioned | `eval.yaml` and `results.json` include `schemaVersion`; readers warn on same-major drift and reject cross-major versions with a migration hint |
7273

7374
### Epic 2: Sensei Engine (P0)
7475

examples/code-explainer/eval.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ description: |
99
- Code patterns (recursion, async/await, comprehensions, joins)
1010
1111
skill: code-explainer
12+
schemaVersion: "1.0"
1213
version: "1.0"
1314

1415
config:

examples/custom-agent/eval.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
name: security-reviewer-eval
22
description: Evaluates the security-reviewer custom agent
33
skill: security-reviewer
4+
schemaVersion: "1.0"
45
version: "1.0"
56

67
config:

examples/grader-showcase/eval.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ description: |
1616
of configuration options and use cases.
1717
1818
skill: general-coding-assistant
19+
schemaVersion: "1.0"
1920
version: "1.0"
2021

2122
config:

0 commit comments

Comments
 (0)