Files
boshu2__agentops/cli/internal/evalsubstrate/modelspec.go
Boden Fuller 8cdfa85cda feat(eval): Day-2 substrate package — atomic writer, gates, snapshots
Wave 1 of the Day-2 deliverable per ~/.agents/evals/SCHEMA.md §9 row 2:

- evalsubstrate package separate from legacy cli/internal/eval (deterministic
  RunRecord) — distinct concept, no cross-pollination.
- §7 canonicalization: BOM strip, zero-width drop, NFC, YAML/JSON/MD
  per-format rules. Mac LF and bushido CRLF produce identical content_hashes.
- §4 atomic-write contract: temp + fsync(temp) + rename + fsync(parent_dir).
- Run lifecycle: pending->running->complete|failed|aborted|retracted with
  rc2-terminal retracted (no unretract).
- Harness snapshot: dir-walk + per-file canonical hash + aggregate
  content_hash + harness.lock.json. VerifyHarnessLock backs gate #8.
- ModelSpec capture with self-stable content_hash (zero-then-fill).
- §6 Refusal format: GATE FAILED / Why / Evidence / Fix — exact whitespace
  matches doctor-#11 expectations.
- Day-2 gates 1, 6, 7, 8, 9 (refusals only). Gates 2/3/4/5 deferred to
  Day 4 per §9 (need Judge + Suite history + Dolt burn ledger).
- 51 unit tests across canonicalization, atomic write, harness, manifest,
  modelspec, refusal, gates. All green.

Day-1 sealed (rc2 SCHEMA.md content_hash sha256:8cd3c9dd...d9a910d locked,
38 fixtures green). This commit is Wave 1 of Day 2; CLI wiring (eval task,
eval cleanup) lands in the next commit. Stop condition not yet hit.
2026-05-01 19:23:37 -04:00

74 lines
2.0 KiB
Go

package evalsubstrate
import (
"fmt"
"os"
"path/filepath"
"time"
"gopkg.in/yaml.v3"
)
func ModelSpecPath(evalsRoot, specID string) string {
return filepath.Join(evalsRoot, "models", specID, "spec.yaml")
}
// CaptureModelSpec writes the ModelSpec to disk after stamping content_hash.
// Returns (specID, contentHash) for Manifest.ModelSpecRef + Manifest.ModelSpecHash.
func CaptureModelSpec(evalsRoot string, spec *ModelSpec) (string, string, error) {
if spec.ID == "" {
return "", "", fmt.Errorf("CaptureModelSpec: empty id")
}
if spec.SchemaVersion == 0 {
spec.SchemaVersion = SchemaVersion
}
if spec.CapturedAt == "" {
spec.CapturedAt = timeNow().UTC().Format(time.RFC3339)
}
prev := spec.ContentHash
spec.ContentHash = ""
rawYAML, err := yaml.Marshal(spec)
if err != nil {
spec.ContentHash = prev
return "", "", fmt.Errorf("CaptureModelSpec: marshal: %w", err)
}
canon, err := CanonicalizeYAML(rawYAML)
if err != nil {
spec.ContentHash = prev
return "", "", fmt.Errorf("CaptureModelSpec: canonicalize: %w", err)
}
hash := ContentHash(canon)
spec.ContentHash = hash
finalYAML, err := yaml.Marshal(spec)
if err != nil {
return "", "", fmt.Errorf("CaptureModelSpec: re-marshal: %w", err)
}
finalCanon, err := CanonicalizeYAML(finalYAML)
if err != nil {
return "", "", fmt.Errorf("CaptureModelSpec: re-canonicalize: %w", err)
}
dest := ModelSpecPath(evalsRoot, spec.ID)
if err := WriteAtomic(dest, finalCanon); err != nil {
return "", "", fmt.Errorf("CaptureModelSpec: write: %w", err)
}
return spec.ID, hash, nil
}
func LoadModelSpec(evalsRoot, specID string) (*ModelSpec, error) {
raw, err := os.ReadFile(ModelSpecPath(evalsRoot, specID))
if err != nil {
return nil, fmt.Errorf("LoadModelSpec: %w", err)
}
var spec ModelSpec
if err := yaml.Unmarshal(raw, &spec); err != nil {
return nil, fmt.Errorf("LoadModelSpec: parse: %w", err)
}
return &spec, nil
}
func ModelSpecHashEqual(a, b *ModelSpec) bool {
return a != nil && b != nil && a.ContentHash != "" && a.ContentHash == b.ContentHash
}