Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 59 additions & 34 deletions internal/fm/frontmatter.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"strings"
"time"

"github.com/misty-step/exocortex/internal/okf"
"go.yaml.in/yaml/v3"
)

Expand Down Expand Up @@ -107,40 +108,13 @@ func warnf(rule, format string, a ...any) Finding {
var knownKeys = map[string]bool{
"type": true, "status": true, "created": true,
"description": true, "tags": true, "provenance": true,
}

// Validate applies a named profile to an already-parsed document.
// Errors are contract failures; warnings are advisory and never
// blocking under any profile.
func Validate(profile string, d Document) ([]Finding, error) {
switch profile {
case "daybook":
return validateDaybook(d)
case "strict":
return validateStrict(d)
default:
return nil, fmt.Errorf("unknown profile %q", profile)
}
}

func requireFrontmatter(d Document) error {
if !d.Note.HasFM {
return contract(errf("fm_missing", "frontmatter missing"))
}
if d.err != nil {
return contract(errf("fm_unparseable", "frontmatter is not parseable YAML: %v", d.err))
}
return nil
}

func validateDaybook(d Document) ([]Finding, error) {
if err := requireFrontmatter(d); err != nil {
return nil, err
}
if t, ok := d.Map["type"].(string); !ok || strings.TrimSpace(t) == "" {
return nil, contract(errf("type_missing", "frontmatter has no non-empty \"type\""))
}
return daybookWarnings(d), nil
// OKF v0.2's optional provenance, lifecycle, and computation
// families are part of the frontmatter vocabulary even when a
// particular concept does not use them.
"title": true, "resource": true, "sources": true,
"generated": true, "verified": true, "stale_after": true,
"runtime": true, "parameters": true, "computation": true,
"executor": true, "attester": true, "usage_window": true,
}

func validateStrict(d Document) ([]Finding, error) {
Expand All @@ -158,6 +132,7 @@ func validateStrict(d Document) ([]Finding, error) {
fs = append(fs, errf("created_format", "created %q is not RFC3339", c))
}
}
fs = append(fs, validateOKF(d, true)...)
for _, f := range fs {
if f.Level == "error" {
return fs, contract(f)
Expand All @@ -179,6 +154,40 @@ func ContractFinding(err error) (Finding, bool) {
return ce.f, ok
}

// Validate applies a named profile to an already-parsed document.
// Errors are contract failures; warnings are advisory and never
// blocking under any profile.
func Validate(profile string, d Document) ([]Finding, error) {
switch profile {
case "daybook":
return validateDaybook(d)
case "strict":
return validateStrict(d)
default:
return nil, fmt.Errorf("unknown profile %q", profile)
}
}

func requireFrontmatter(d Document) error {
if !d.Note.HasFM {
return contract(errf("fm_missing", "frontmatter missing"))
}
if d.err != nil {
return contract(errf("fm_unparseable", "frontmatter is not parseable YAML: %v", d.err))
}
return nil
}

func validateDaybook(d Document) ([]Finding, error) {
if err := requireFrontmatter(d); err != nil {
return nil, err
}
if t, ok := d.Map["type"].(string); !ok || strings.TrimSpace(t) == "" {
return nil, contract(errf("type_missing", "frontmatter has no non-empty \"type\""))
}
return daybookWarnings(d), nil
}

func daybookWarnings(d Document) []Finding {
// Journal micro-notes (type: memo) are quiet under the daybook
// profile: they are not wiki notes, and constant key warnings would
Expand All @@ -197,6 +206,7 @@ func daybookWarnings(d Document) []Finding {
fs = append(fs, warnf("created_format", "created %q is not RFC3339", c))
}
}
fs = append(fs, validateOKF(d, false)...)
var unknown []string
for k := range d.Map {
if !knownKeys[k] {
Expand All @@ -210,6 +220,21 @@ func daybookWarnings(d Document) []Finding {
return fs
}

// validateOKF applies the structural checks shared by the OKF v0.2
// provenance and lifecycle fields. Daybook reports malformed optional
// fields as warnings; strict promotes the same findings to errors.
func validateOKF(d Document, strict bool) []Finding {
var fs []Finding
for _, v := range okf.Validate(&d.root) {
if strict {
fs = append(fs, errf(v.Rule, "%s", v.Message))
} else {
fs = append(fs, warnf(v.Rule, "%s", v.Message))
}
}
return fs
}

func empty(v any) bool {
switch t := v.(type) {
case nil:
Expand Down
78 changes: 78 additions & 0 deletions internal/fm/frontmatter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,84 @@ func TestValidateStrict(t *testing.T) {
}
}

// Malformed optional OKF fields violate the same rules under both
// profiles; only the level policy differs.
func TestValidateOKFLevelPolicy(t *testing.T) {
malformed := `---
type: Metric
status: stable
created: 2026-06-25T09:00:00Z
description: Revenue.
tags: [finance]
generated:
by: "human:"
at: 2026-06-20
---
body
`
fs, err := validate("daybook", malformed)
if err != nil {
t.Fatalf("malformed optional signals must warn, not fail, under daybook: %v", err)
}
if !hasRule(fs, "generated_by_format") || !hasRule(fs, "generated_at_format") {
t.Fatalf("daybook missing OKF violations in %+v", fs)
}
for _, f := range fs {
if f.Level != "warning" {
t.Fatalf("daybook must warn, never error, on %v", f)
}
}
_, err = validate("strict", malformed)
if err == nil {
t.Fatal("strict must reject malformed OKF v0.2 signals")
}
// With the floor keys satisfied, the contract error must be the
// promoted OKF violation, not the floor key_missing.
f, ok := ContractFinding(err)
if !ok {
t.Fatalf("strict error is not a finding: %v", err)
}
if f.Level != "error" || f.Rule != "generated_by_format" {
t.Fatalf("strict must promote the generated_by_format violation, got %+v", f)
}
}

// Every OKF v0.2 vocabulary key is a known key under the daybook
// profile, and valid OKF metadata must not fail strict.
func TestValidateOKFKeysAreKnown(t *testing.T) {
raw := `---
type: Attested Computation
status: stable
created: 2026-06-20T22:53:05Z
description: Revenue.
tags: [finance]
title: Revenue
resource: https://example.test/revenue
sources: [{resource: https://example.test/policy}]
generated: {by: reference_agent/gemini-2.5-pro, at: 2026-06-20T22:53:05Z}
verified: [{by: human:ahormati, at: 2026-06-25T09:00:00Z}]
stale_after: 2026-12-31T00:00:00+01:00
runtime: bigquery
parameters: [{name: year, type: integer, required: true}]
computation: references/revenue.sql
executor: {resource: references/run.md}
attester: {resource: references/check.py}
usage_window: {from: 2026-06-01, to: 2026-06-30}
---
body
`
fs, err := validate("daybook", raw)
if err != nil {
t.Fatalf("OKF v0.2 note must pass daybook: %v", err)
}
if hasRule(fs, "unknown_keys") {
t.Fatalf("OKF v0.2 keys should be known: %+v", fs)
}
if _, err := validate("strict", raw); err != nil {
t.Fatalf("valid OKF v0.2 metadata must pass strict: %v", err)
}
}

const richNote = `---
type: decision
status: active # keep this comment
Expand Down
Loading
Loading