From 895d1e2318dd87abebb78f3dfa468a1af5405b72 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 11:56:45 -0500 Subject: [PATCH 01/10] feat(fm): validate OKF v0.2 metadata --- internal/fm/frontmatter.go | 257 ++++++++++++++++++++++++++++---- internal/fm/frontmatter_test.go | 108 ++++++++++++++ 2 files changed, 332 insertions(+), 33 deletions(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index c55c0a9..50547f9 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -107,41 +107,16 @@ 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, + // 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, } -// 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 -} +var okfActorRE = regexp.MustCompile(`^(?:[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*|(?:human|process):[A-Za-z0-9][A-Za-z0-9._:@+-]*)$`) func validateStrict(d Document) ([]Finding, error) { if err := requireFrontmatter(d); err != nil { @@ -158,6 +133,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) @@ -179,6 +155,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 @@ -197,6 +207,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] { @@ -210,6 +221,186 @@ 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 { + root := topMappingNode(d) + if root == nil { + return nil + } + var fs []Finding + fs = append(fs, validateGenerated(root, strict)...) + fs = append(fs, validateVerified(root, strict)...) + fs = append(fs, validateStaleAfter(root, strict)...) + fs = append(fs, validateProvenance(root, strict)...) + return fs +} + +func validateGenerated(root *yaml.Node, strict bool) []Finding { + generated, ok := mappingValue(root, "generated") + if !ok { + return nil + } + return validateOKFEvent("generated", generated, strict) +} + +func validateVerified(root *yaml.Node, strict bool) []Finding { + verified, ok := mappingValue(root, "verified") + if !ok { + return nil + } + verified = unwrapNode(verified) + if verified == nil { + return []Finding{okfFinding(strict, "verified_format", "verified must be a mapping or list of mappings")} + } + switch verified.Kind { + case yaml.MappingNode: + return validateOKFEventFields("verified", "verified", verified, strict) + case yaml.SequenceNode: + var fs []Finding + for i, event := range verified.Content { + fs = append(fs, validateOKFEventFields( + fmt.Sprintf("verified[%d]", i), "verified", unwrapNode(event), strict, + )...) + } + return fs + default: + return []Finding{okfFinding(strict, "verified_format", "verified must be a mapping or list of mappings")} + } +} + +func validateStaleAfter(root *yaml.Node, strict bool) []Finding { + staleAfter, ok := mappingValue(root, "stale_after") + if !ok { + return nil + } + value, scalar := nodeScalar(staleAfter) + if scalar && validStaleAfter(value) { + return nil + } + return []Finding{okfFinding(strict, "stale_after_format", + "stale_after %q must be an ISO date or UTC RFC3339 datetime", value)} +} + +func validateProvenance(root *yaml.Node, strict bool) []Finding { + provenance, ok := mappingValue(root, "provenance") + if !ok { + return nil + } + provenance = unwrapNode(provenance) + if provenance == nil || provenance.Kind != yaml.MappingNode { + return []Finding{okfFinding(strict, "provenance_format", "provenance must be a mapping")} + } + at, present := mappingValue(provenance, "at") + if !present { + return nil + } + value, scalar := nodeScalar(at) + if scalar && validUTCDateTime(value) { + return nil + } + return []Finding{okfFinding(strict, "provenance_at_format", + "provenance.at %q must be a UTC RFC3339 datetime", value)} +} + +func validateOKFEvent(field string, event *yaml.Node, strict bool) []Finding { + event = unwrapNode(event) + if event == nil || event.Kind != yaml.MappingNode { + return []Finding{okfFinding(strict, field+"_format", "%s must be a mapping", field)} + } + return validateOKFEventFields(field, field, event, strict) +} + +func validateOKFEventFields(field, ruleBase string, event *yaml.Node, strict bool) []Finding { + var fs []Finding + if event == nil || event.Kind != yaml.MappingNode { + return []Finding{okfFinding(strict, ruleBase+"_format", "%s must be a mapping", field)} + } + by, ok := mappingValue(event, "by") + if !ok { + fs = append(fs, okfFinding(strict, ruleBase+"_by_missing", "%s.by is required", field)) + } else if value, scalar := nodeScalar(by); !scalar || !okfActorRE.MatchString(value) { + value, _ := nodeScalar(by) + fs = append(fs, okfFinding(strict, ruleBase+"_by_format", + "%s.by %q must be /, human:, or process:", field, value)) + } + at, ok := mappingValue(event, "at") + if !ok { + fs = append(fs, okfFinding(strict, ruleBase+"_at_missing", "%s.at is required", field)) + } else if value, scalar := nodeScalar(at); !scalar || !validUTCDateTime(value) { + value, _ := nodeScalar(at) + fs = append(fs, okfFinding(strict, ruleBase+"_at_format", + "%s.at %q must be a UTC RFC3339 datetime", field, value)) + } + return fs +} + +func okfFinding(strict bool, rule, format string, args ...any) Finding { + if strict { + return errf(rule, format, args...) + } + return warnf(rule, format, args...) +} + +func topMappingNode(d Document) *yaml.Node { + if len(d.root.Content) == 0 { + return nil + } + root := unwrapNode(d.root.Content[0]) + if root.Kind != yaml.MappingNode { + return nil + } + return root +} + +func mappingValue(mapping *yaml.Node, key string) (*yaml.Node, bool) { + mapping = unwrapNode(mapping) + if mapping == nil || mapping.Kind != yaml.MappingNode { + return nil, false + } + for i := 0; i+1 < len(mapping.Content); i += 2 { + name := unwrapNode(mapping.Content[i]) + if name.Kind == yaml.ScalarNode && name.Value == key { + return mapping.Content[i+1], true + } + } + return nil, false +} + +func unwrapNode(n *yaml.Node) *yaml.Node { + for n != nil && n.Kind == yaml.AliasNode { + n = n.Alias + } + return n +} + +func nodeScalar(n *yaml.Node) (string, bool) { + n = unwrapNode(n) + if n == nil || n.Kind != yaml.ScalarNode || n.Tag == "!!null" { + return "", false + } + return n.Value, true +} + +func validUTCDateTime(value string) bool { + parsed, err := time.Parse(time.RFC3339, value) + if err != nil { + return false + } + _, offset := parsed.Zone() + return offset == 0 && (strings.HasSuffix(value, "Z") || strings.HasSuffix(value, "+00:00")) +} + +func validStaleAfter(value string) bool { + if len(value) == len("2006-01-02") { + if _, err := time.Parse("2006-01-02", value); err == nil { + return true + } + } + return validUTCDateTime(value) +} + func empty(v any) bool { switch t := v.(type) { case nil: diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index 5908ee8..01309b4 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -114,6 +114,114 @@ func TestValidateStrict(t *testing.T) { } } +func TestValidateOKFV02(t *testing.T) { + raw := `--- +type: Attested Computation +title: Revenue +description: Recognized 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 + - by: process:finance-nightly + at: 2026-06-26T02:00:00+00:00 +stale_after: 2026-12-31 +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 +provenance: + agent: kernel + at: 2026-06-28T14:00:00Z + via: cli +status: stable +created: 2026-06-20T22:53:05Z +tags: [finance] +--- +body +` + fs, err := validate("daybook", raw) + if err != nil { + t.Fatalf("valid OKF v0.2 note must pass daybook: %v", err) + } + for _, f := range fs { + if f.Rule == "unknown_keys" { + t.Fatalf("OKF v0.2 keys should be known: %+v", fs) + } + if f.Rule == "generated_by_format" || f.Rule == "generated_at_format" || + f.Rule == "verified_by_format" || f.Rule == "verified_at_format" || + f.Rule == "stale_after_format" || f.Rule == "provenance_at_format" { + t.Fatalf("valid OKF v0.2 signal should not be flagged: %+v", f) + } + } + if _, err := validate("strict", raw); err != nil { + t.Fatalf("valid OKF v0.2 note must pass strict: %v", err) + } +} + +func TestValidateOKFV02RejectsMalformedSignals(t *testing.T) { + raw := `--- +type: Metric +status: stable +created: 2026-06-20T22:53:05Z +description: Revenue. +tags: [finance] +generated: + by: "human:" + at: 2026-06-20 +verified: + - by: agent + at: 2026-06-25T09:00:00+01:00 +stale_after: not-a-date +provenance: + at: yesterday +--- +body +` + fs, err := validate("daybook", raw) + if err != nil { + t.Fatalf("malformed optional signals must warn, not fail, under daybook: %v", err) + } + for _, rule := range []string{ + "generated_by_format", "generated_at_format", + "verified_by_format", "verified_at_format", + "stale_after_format", "provenance_at_format", + } { + if !hasRule(fs, rule) { + t.Fatalf("daybook missing %s in %+v", rule, fs) + } + } + if _, err := validate("strict", raw); err == nil { + t.Fatal("strict must reject malformed OKF v0.2 signals") + } +} + +func TestValidateOKFV02VerifiedMapping(t *testing.T) { + raw := `--- +type: Metric +verified: {by: process:nightly, at: 2026-06-25T09:00:00Z} +--- +body +` + if fs, err := validate("daybook", raw); err != nil || hasRule(fs, "verified_format") { + t.Fatalf("bare verified mapping is valid OKF v0.2: findings=%+v err=%v", fs, err) + } +} + const richNote = `--- type: decision status: active # keep this comment From 8a4e7da336db75f4504ada926608726e887edc23 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 12:08:13 -0500 Subject: [PATCH 02/10] fix(fm): accept complete OKF timestamp forms --- internal/fm/frontmatter.go | 81 ++++++++++++++++++++++++--------- internal/fm/frontmatter_test.go | 41 ++++++++++++++++- 2 files changed, 99 insertions(+), 23 deletions(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index 50547f9..dbbba90 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -116,8 +116,6 @@ var knownKeys = map[string]bool{ "executor": true, "attester": true, "usage_window": true, } -var okfActorRE = regexp.MustCompile(`^(?:[A-Za-z0-9][A-Za-z0-9._-]*/[A-Za-z0-9][A-Za-z0-9._-]*|(?:human|process):[A-Za-z0-9][A-Za-z0-9._:@+-]*)$`) - func validateStrict(d Document) ([]Finding, error) { if err := requireFrontmatter(d); err != nil { return nil, err @@ -256,12 +254,12 @@ func validateVerified(root *yaml.Node, strict bool) []Finding { } switch verified.Kind { case yaml.MappingNode: - return validateOKFEventFields("verified", "verified", verified, strict) + return validateOKFEventFields("verified", "verified", verified, strict, true) case yaml.SequenceNode: var fs []Finding for i, event := range verified.Content { fs = append(fs, validateOKFEventFields( - fmt.Sprintf("verified[%d]", i), "verified", unwrapNode(event), strict, + fmt.Sprintf("verified[%d]", i), "verified", unwrapNode(event), strict, true, )...) } return fs @@ -280,7 +278,7 @@ func validateStaleAfter(root *yaml.Node, strict bool) []Finding { return nil } return []Finding{okfFinding(strict, "stale_after_format", - "stale_after %q must be an ISO date or UTC RFC3339 datetime", value)} + "stale_after %q must be an RFC3339 datetime with an explicit UTC offset", value)} } func validateProvenance(root *yaml.Node, strict bool) []Finding { @@ -309,10 +307,11 @@ func validateOKFEvent(field string, event *yaml.Node, strict bool) []Finding { if event == nil || event.Kind != yaml.MappingNode { return []Finding{okfFinding(strict, field+"_format", "%s must be a mapping", field)} } - return validateOKFEventFields(field, field, event, strict) + // generated.at is optional when the production time is unknown. + return validateOKFEventFields(field, field, event, strict, false) } -func validateOKFEventFields(field, ruleBase string, event *yaml.Node, strict bool) []Finding { +func validateOKFEventFields(field, ruleBase string, event *yaml.Node, strict, requireAt bool) []Finding { var fs []Finding if event == nil || event.Kind != yaml.MappingNode { return []Finding{okfFinding(strict, ruleBase+"_format", "%s must be a mapping", field)} @@ -320,14 +319,16 @@ func validateOKFEventFields(field, ruleBase string, event *yaml.Node, strict boo by, ok := mappingValue(event, "by") if !ok { fs = append(fs, okfFinding(strict, ruleBase+"_by_missing", "%s.by is required", field)) - } else if value, scalar := nodeScalar(by); !scalar || !okfActorRE.MatchString(value) { + } else if value, scalar := nodeScalar(by); !scalar || !validOKFActor(value) { value, _ := nodeScalar(by) fs = append(fs, okfFinding(strict, ruleBase+"_by_format", "%s.by %q must be /, human:, or process:", field, value)) } at, ok := mappingValue(event, "at") if !ok { - fs = append(fs, okfFinding(strict, ruleBase+"_at_missing", "%s.at is required", field)) + if requireAt { + fs = append(fs, okfFinding(strict, ruleBase+"_at_missing", "%s.at is required", field)) + } } else if value, scalar := nodeScalar(at); !scalar || !validUTCDateTime(value) { value, _ := nodeScalar(at) fs = append(fs, okfFinding(strict, ruleBase+"_at_format", @@ -355,15 +356,49 @@ func topMappingNode(d Document) *yaml.Node { } func mappingValue(mapping *yaml.Node, key string) (*yaml.Node, bool) { - mapping = unwrapNode(mapping) + return mappingValueWithMerges(unwrapNode(mapping), key) +} + +func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { if mapping == nil || mapping.Kind != yaml.MappingNode { return nil, false } + var merges []*yaml.Node for i := 0; i+1 < len(mapping.Content); i += 2 { name := unwrapNode(mapping.Content[i]) - if name.Kind == yaml.ScalarNode && name.Value == key { + if name == nil || name.Kind != yaml.ScalarNode { + continue + } + if name.Value == key { return mapping.Content[i+1], true } + if name.Value == "<<" { + merges = append(merges, mapping.Content[i+1]) + } + } + for _, merged := range merges { + if value, ok := mergedMappingValue(merged, key); ok { + return value, true + } + } + return nil, false +} + +func mergedMappingValue(merged *yaml.Node, key string) (*yaml.Node, bool) { + merged = unwrapNode(merged) + if merged == nil { + return nil, false + } + if merged.Kind == yaml.MappingNode { + return mappingValueWithMerges(merged, key) + } + if merged.Kind != yaml.SequenceNode { + return nil, false + } + for _, item := range merged.Content { + if value, ok := mergedMappingValue(item, key); ok { + return value, true + } } return nil, false } @@ -383,21 +418,25 @@ func nodeScalar(n *yaml.Node) (string, bool) { return n.Value, true } -func validUTCDateTime(value string) bool { - parsed, err := time.Parse(time.RFC3339, value) - if err != nil { +func validOKFActor(value string) bool { + if value == "" || strings.TrimSpace(value) != value || len(strings.Fields(value)) != 1 { return false } - _, offset := parsed.Zone() - return offset == 0 && (strings.HasSuffix(value, "Z") || strings.HasSuffix(value, "+00:00")) + for _, prefix := range []string{"human:", "process:"} { + if strings.HasPrefix(value, prefix) { + return len(value) > len(prefix) + } + } + parts := strings.Split(value, "/") + return len(parts) == 2 && parts[0] != "" && parts[1] != "" +} + +func validUTCDateTime(value string) bool { + _, err := time.Parse(time.RFC3339, value) + return err == nil } func validStaleAfter(value string) bool { - if len(value) == len("2006-01-02") { - if _, err := time.Parse("2006-01-02", value); err == nil { - return true - } - } return validUTCDateTime(value) } diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index 01309b4..ebe2bb3 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -130,7 +130,7 @@ verified: at: 2026-06-25T09:00:00Z - by: process:finance-nightly at: 2026-06-26T02:00:00+00:00 -stale_after: 2026-12-31 +stale_after: 2026-12-31T00:00:00+01:00 runtime: bigquery parameters: - name: year @@ -185,7 +185,7 @@ generated: at: 2026-06-20 verified: - by: agent - at: 2026-06-25T09:00:00+01:00 + at: 2026-06-25 stale_after: not-a-date provenance: at: yesterday @@ -222,6 +222,43 @@ body } } +func TestValidateOKFV02GeneratedAtOptional(t *testing.T) { + raw := `--- +type: Metric +status: stable +created: 2026-06-20T22:53:05Z +description: Revenue. +tags: [finance] +generated: + by: human:alice +--- +body +` + if _, err := validate("strict", raw); err != nil { + t.Fatalf("generated.at is optional: %v", err) + } +} + +func TestValidateOKFV02MergeKeys(t *testing.T) { + raw := `--- +defaults: &defaults + type: Metric + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +<<: *defaults +--- +body +` + fs, err := validate("daybook", raw) + if err != nil { + t.Fatalf("merged OKF metadata must still validate: %v", err) + } + if !hasRule(fs, "generated_by_format") { + t.Fatalf("merged generated metadata was skipped: %+v", fs) + } +} + const richNote = `--- type: decision status: active # keep this comment From a578bb167b2117d95715ba23afdabc70fd1815cb Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 12:10:03 -0500 Subject: [PATCH 03/10] fix(fm): distinguish YAML merge keys --- internal/fm/frontmatter.go | 2 +- internal/fm/frontmatter_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index dbbba90..3954016 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -372,7 +372,7 @@ func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { if name.Value == key { return mapping.Content[i+1], true } - if name.Value == "<<" { + if name.Value == "<<" && name.Tag == "!!merge" { merges = append(merges, mapping.Content[i+1]) } } diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index ebe2bb3..4a056f5 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -257,6 +257,20 @@ body if !hasRule(fs, "generated_by_format") { t.Fatalf("merged generated metadata was skipped: %+v", fs) } + quoted := `--- +type: Metric +"<<": + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +--- +body +` + qfs, qerr := validate("daybook", quoted) + if qerr != nil || hasRule(qfs, "generated_by_format") { + t.Fatalf("quoted << key must not act as a merge: findings=%+v err=%v", qfs, qerr) + } + } const richNote = `--- From 8df94dfc01b66ab81d314293d8d9be809ba7966d Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 12:11:08 -0500 Subject: [PATCH 04/10] fix(fm): honor long-form YAML merge tags --- internal/fm/frontmatter.go | 2 +- internal/fm/frontmatter_test.go | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index 3954016..8565296 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -372,7 +372,7 @@ func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { if name.Value == key { return mapping.Content[i+1], true } - if name.Value == "<<" && name.Tag == "!!merge" { + if name.Value == "<<" && name.ShortTag() == "!!merge" { merges = append(merges, mapping.Content[i+1]) } } diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index 4a056f5..f258ecb 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -271,6 +271,20 @@ body t.Fatalf("quoted << key must not act as a merge: findings=%+v err=%v", qfs, qerr) } + longTag := `--- +defaults: &defaults + type: Metric + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +! "<<": *defaults +--- +body +` + lfs, lerr := validate("daybook", longTag) + if lerr != nil || !hasRule(lfs, "generated_by_format") { + t.Fatalf("long-form merge tag must act as a merge: findings=%+v err=%v", lfs, lerr) + } } const richNote = `--- From c9eef46f4c14d9992c4c6d7768573618d868b93a Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 12:12:39 -0500 Subject: [PATCH 05/10] fix(fm): preserve YAML alias key semantics --- internal/fm/frontmatter.go | 6 ++++-- internal/fm/frontmatter_test.go | 15 +++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index 8565296..235193e 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -365,14 +365,16 @@ func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { } var merges []*yaml.Node for i := 0; i+1 < len(mapping.Content); i += 2 { - name := unwrapNode(mapping.Content[i]) + rawName := mapping.Content[i] + name := unwrapNode(rawName) if name == nil || name.Kind != yaml.ScalarNode { continue } if name.Value == key { return mapping.Content[i+1], true } - if name.Value == "<<" && name.ShortTag() == "!!merge" { + if rawName != nil && rawName.Kind == yaml.ScalarNode && + rawName.Value == "<<" && rawName.ShortTag() == "!!merge" { merges = append(merges, mapping.Content[i+1]) } } diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index f258ecb..cc6ba04 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -285,6 +285,21 @@ body if lerr != nil || !hasRule(lfs, "generated_by_format") { t.Fatalf("long-form merge tag must act as a merge: findings=%+v err=%v", lfs, lerr) } + aliasKey := `--- +type: Metric +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +merge_name: &merge_name "<<" +*merge_name: *defaults +--- +body +` + afs, aerr := validate("daybook", aliasKey) + if aerr != nil || hasRule(afs, "generated_by_format") { + t.Fatalf("aliased << key must not act as a merge: findings=%+v err=%v", afs, aerr) + } } const richNote = `--- From 33980ff42043f9c34343633eed1cd8feb5ffecff Mon Sep 17 00:00:00 2001 From: phaedrus Date: Sun, 30 Aug 2026 12:14:49 -0500 Subject: [PATCH 06/10] fix(fm): match YAML non-specific merge tags --- internal/fm/frontmatter.go | 8 ++++++-- internal/fm/frontmatter_test.go | 14 ++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index 235193e..9aef9ed 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -373,8 +373,7 @@ func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { if name.Value == key { return mapping.Content[i+1], true } - if rawName != nil && rawName.Kind == yaml.ScalarNode && - rawName.Value == "<<" && rawName.ShortTag() == "!!merge" { + if isYAMLMergeKey(rawName) { merges = append(merges, mapping.Content[i+1]) } } @@ -405,7 +404,12 @@ func mergedMappingValue(merged *yaml.Node, key string) (*yaml.Node, bool) { return nil, false } +func isYAMLMergeKey(n *yaml.Node) bool { + return n != nil && n.Kind == yaml.ScalarNode && n.Value == "<<" && + (n.Tag == "" || n.Tag == "!" || n.ShortTag() == "!!merge") +} func unwrapNode(n *yaml.Node) *yaml.Node { + for n != nil && n.Kind == yaml.AliasNode { n = n.Alias } diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index cc6ba04..3af08b5 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -300,6 +300,20 @@ body if aerr != nil || hasRule(afs, "generated_by_format") { t.Fatalf("aliased << key must not act as a merge: findings=%+v err=%v", afs, aerr) } + explicitTag := `--- +type: Metric +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +! <<: *defaults +--- +body +` + efs, eerr := validate("daybook", explicitTag) + if eerr != nil || !hasRule(efs, "generated_by_format") { + t.Fatalf("explicit non-specific merge tag must act as a merge: findings=%+v err=%v", efs, eerr) + } } const richNote = `--- From 9977499b01bbc898dede95004c6901d37944e0e3 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Tue, 1 Sep 2026 15:13:48 -0500 Subject: [PATCH 07/10] refactor(fm): split OKF v0.2 validation into internal/okf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The OKF vocabulary validators move to a deep okf module returning neutral violations; fm keeps profile policy (strict=error, daybook=warning) and maps them onto findings. Timestamp messages now say what OKF SPEC §5 actually requires — an RFC3339 datetime with an explicit UTC offset, any offset, not only zero — resolving CodeRabbit's merge-risk note as incorrect: the validator behavior already matched the spec, the wording did not. Nonzero-offset acceptance is pinned by tests at the okf boundary. fm's token ratchet holds via the split (11,593 -> 8,952 with a new 3,692-token okf module); fm's ceiling rises to 9,000 with the reason recorded in modules.budget. Master reconciled through e0076fd. --- internal/fm/frontmatter.go | 223 +--------------------------- internal/fm/frontmatter_test.go | 202 +++++--------------------- internal/okf/okf.go | 247 ++++++++++++++++++++++++++++++++ internal/okf/okf_test.go | 220 ++++++++++++++++++++++++++++ modules.budget | 6 +- 5 files changed, 511 insertions(+), 387 deletions(-) create mode 100644 internal/okf/okf.go create mode 100644 internal/okf/okf_test.go diff --git a/internal/fm/frontmatter.go b/internal/fm/frontmatter.go index 9aef9ed..72ed55e 100644 --- a/internal/fm/frontmatter.go +++ b/internal/fm/frontmatter.go @@ -12,6 +12,7 @@ import ( "strings" "time" + "github.com/misty-step/exocortex/internal/okf" "go.yaml.in/yaml/v3" ) @@ -223,229 +224,17 @@ func daybookWarnings(d Document) []Finding { // 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 { - root := topMappingNode(d) - if root == nil { - return nil - } - var fs []Finding - fs = append(fs, validateGenerated(root, strict)...) - fs = append(fs, validateVerified(root, strict)...) - fs = append(fs, validateStaleAfter(root, strict)...) - fs = append(fs, validateProvenance(root, strict)...) - return fs -} - -func validateGenerated(root *yaml.Node, strict bool) []Finding { - generated, ok := mappingValue(root, "generated") - if !ok { - return nil - } - return validateOKFEvent("generated", generated, strict) -} - -func validateVerified(root *yaml.Node, strict bool) []Finding { - verified, ok := mappingValue(root, "verified") - if !ok { - return nil - } - verified = unwrapNode(verified) - if verified == nil { - return []Finding{okfFinding(strict, "verified_format", "verified must be a mapping or list of mappings")} - } - switch verified.Kind { - case yaml.MappingNode: - return validateOKFEventFields("verified", "verified", verified, strict, true) - case yaml.SequenceNode: - var fs []Finding - for i, event := range verified.Content { - fs = append(fs, validateOKFEventFields( - fmt.Sprintf("verified[%d]", i), "verified", unwrapNode(event), strict, true, - )...) - } - return fs - default: - return []Finding{okfFinding(strict, "verified_format", "verified must be a mapping or list of mappings")} - } -} - -func validateStaleAfter(root *yaml.Node, strict bool) []Finding { - staleAfter, ok := mappingValue(root, "stale_after") - if !ok { - return nil - } - value, scalar := nodeScalar(staleAfter) - if scalar && validStaleAfter(value) { - return nil - } - return []Finding{okfFinding(strict, "stale_after_format", - "stale_after %q must be an RFC3339 datetime with an explicit UTC offset", value)} -} - -func validateProvenance(root *yaml.Node, strict bool) []Finding { - provenance, ok := mappingValue(root, "provenance") - if !ok { - return nil - } - provenance = unwrapNode(provenance) - if provenance == nil || provenance.Kind != yaml.MappingNode { - return []Finding{okfFinding(strict, "provenance_format", "provenance must be a mapping")} - } - at, present := mappingValue(provenance, "at") - if !present { - return nil - } - value, scalar := nodeScalar(at) - if scalar && validUTCDateTime(value) { - return nil - } - return []Finding{okfFinding(strict, "provenance_at_format", - "provenance.at %q must be a UTC RFC3339 datetime", value)} -} - -func validateOKFEvent(field string, event *yaml.Node, strict bool) []Finding { - event = unwrapNode(event) - if event == nil || event.Kind != yaml.MappingNode { - return []Finding{okfFinding(strict, field+"_format", "%s must be a mapping", field)} - } - // generated.at is optional when the production time is unknown. - return validateOKFEventFields(field, field, event, strict, false) -} - -func validateOKFEventFields(field, ruleBase string, event *yaml.Node, strict, requireAt bool) []Finding { var fs []Finding - if event == nil || event.Kind != yaml.MappingNode { - return []Finding{okfFinding(strict, ruleBase+"_format", "%s must be a mapping", field)} - } - by, ok := mappingValue(event, "by") - if !ok { - fs = append(fs, okfFinding(strict, ruleBase+"_by_missing", "%s.by is required", field)) - } else if value, scalar := nodeScalar(by); !scalar || !validOKFActor(value) { - value, _ := nodeScalar(by) - fs = append(fs, okfFinding(strict, ruleBase+"_by_format", - "%s.by %q must be /, human:, or process:", field, value)) - } - at, ok := mappingValue(event, "at") - if !ok { - if requireAt { - fs = append(fs, okfFinding(strict, ruleBase+"_at_missing", "%s.at is required", field)) + 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)) } - } else if value, scalar := nodeScalar(at); !scalar || !validUTCDateTime(value) { - value, _ := nodeScalar(at) - fs = append(fs, okfFinding(strict, ruleBase+"_at_format", - "%s.at %q must be a UTC RFC3339 datetime", field, value)) } return fs } -func okfFinding(strict bool, rule, format string, args ...any) Finding { - if strict { - return errf(rule, format, args...) - } - return warnf(rule, format, args...) -} - -func topMappingNode(d Document) *yaml.Node { - if len(d.root.Content) == 0 { - return nil - } - root := unwrapNode(d.root.Content[0]) - if root.Kind != yaml.MappingNode { - return nil - } - return root -} - -func mappingValue(mapping *yaml.Node, key string) (*yaml.Node, bool) { - return mappingValueWithMerges(unwrapNode(mapping), key) -} - -func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { - if mapping == nil || mapping.Kind != yaml.MappingNode { - return nil, false - } - var merges []*yaml.Node - for i := 0; i+1 < len(mapping.Content); i += 2 { - rawName := mapping.Content[i] - name := unwrapNode(rawName) - if name == nil || name.Kind != yaml.ScalarNode { - continue - } - if name.Value == key { - return mapping.Content[i+1], true - } - if isYAMLMergeKey(rawName) { - merges = append(merges, mapping.Content[i+1]) - } - } - for _, merged := range merges { - if value, ok := mergedMappingValue(merged, key); ok { - return value, true - } - } - return nil, false -} - -func mergedMappingValue(merged *yaml.Node, key string) (*yaml.Node, bool) { - merged = unwrapNode(merged) - if merged == nil { - return nil, false - } - if merged.Kind == yaml.MappingNode { - return mappingValueWithMerges(merged, key) - } - if merged.Kind != yaml.SequenceNode { - return nil, false - } - for _, item := range merged.Content { - if value, ok := mergedMappingValue(item, key); ok { - return value, true - } - } - return nil, false -} - -func isYAMLMergeKey(n *yaml.Node) bool { - return n != nil && n.Kind == yaml.ScalarNode && n.Value == "<<" && - (n.Tag == "" || n.Tag == "!" || n.ShortTag() == "!!merge") -} -func unwrapNode(n *yaml.Node) *yaml.Node { - - for n != nil && n.Kind == yaml.AliasNode { - n = n.Alias - } - return n -} - -func nodeScalar(n *yaml.Node) (string, bool) { - n = unwrapNode(n) - if n == nil || n.Kind != yaml.ScalarNode || n.Tag == "!!null" { - return "", false - } - return n.Value, true -} - -func validOKFActor(value string) bool { - if value == "" || strings.TrimSpace(value) != value || len(strings.Fields(value)) != 1 { - return false - } - for _, prefix := range []string{"human:", "process:"} { - if strings.HasPrefix(value, prefix) { - return len(value) > len(prefix) - } - } - parts := strings.Split(value, "/") - return len(parts) == 2 && parts[0] != "" && parts[1] != "" -} - -func validUTCDateTime(value string) bool { - _, err := time.Parse(time.RFC3339, value) - return err == nil -} - -func validStaleAfter(value string) bool { - return validUTCDateTime(value) -} - func empty(v any) bool { switch t := v.(type) { case nil: diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index 3af08b5..90e6f7f 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -114,205 +114,69 @@ func TestValidateStrict(t *testing.T) { } } -func TestValidateOKFV02(t *testing.T) { - raw := `--- -type: Attested Computation -title: Revenue -description: Recognized 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 - - by: process:finance-nightly - at: 2026-06-26T02:00:00+00:00 -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 -provenance: - agent: kernel - at: 2026-06-28T14:00:00Z - via: cli -status: stable -created: 2026-06-20T22:53:05Z -tags: [finance] ---- -body -` - fs, err := validate("daybook", raw) - if err != nil { - t.Fatalf("valid OKF v0.2 note must pass daybook: %v", err) - } - for _, f := range fs { - if f.Rule == "unknown_keys" { - t.Fatalf("OKF v0.2 keys should be known: %+v", fs) - } - if f.Rule == "generated_by_format" || f.Rule == "generated_at_format" || - f.Rule == "verified_by_format" || f.Rule == "verified_at_format" || - f.Rule == "stale_after_format" || f.Rule == "provenance_at_format" { - t.Fatalf("valid OKF v0.2 signal should not be flagged: %+v", f) - } - } - if _, err := validate("strict", raw); err != nil { - t.Fatalf("valid OKF v0.2 note must pass strict: %v", err) - } -} - -func TestValidateOKFV02RejectsMalformedSignals(t *testing.T) { - raw := `--- +// 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-20T22:53:05Z -description: Revenue. -tags: [finance] generated: by: "human:" at: 2026-06-20 -verified: - - by: agent - at: 2026-06-25 -stale_after: not-a-date provenance: at: yesterday --- body ` - fs, err := validate("daybook", raw) + fs, err := validate("daybook", malformed) if err != nil { t.Fatalf("malformed optional signals must warn, not fail, under daybook: %v", err) } - for _, rule := range []string{ - "generated_by_format", "generated_at_format", - "verified_by_format", "verified_at_format", - "stale_after_format", "provenance_at_format", - } { - if !hasRule(fs, rule) { - t.Fatalf("daybook missing %s in %+v", rule, fs) + if !hasRule(fs, "generated_by_format") || !hasRule(fs, "generated_at_format") || !hasRule(fs, "provenance_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) } } - if _, err := validate("strict", raw); err == nil { + if _, err := validate("strict", malformed); err == nil { t.Fatal("strict must reject malformed OKF v0.2 signals") } } -func TestValidateOKFV02VerifiedMapping(t *testing.T) { - raw := `--- -type: Metric -verified: {by: process:nightly, at: 2026-06-25T09:00:00Z} ---- -body -` - if fs, err := validate("daybook", raw); err != nil || hasRule(fs, "verified_format") { - t.Fatalf("bare verified mapping is valid OKF v0.2: findings=%+v err=%v", fs, err) - } -} - -func TestValidateOKFV02GeneratedAtOptional(t *testing.T) { +// 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: Metric +type: Attested Computation status: stable created: 2026-06-20T22:53:05Z description: Revenue. tags: [finance] -generated: - by: human:alice ---- -body -` - if _, err := validate("strict", raw); err != nil { - t.Fatalf("generated.at is optional: %v", err) - } -} - -func TestValidateOKFV02MergeKeys(t *testing.T) { - raw := `--- -defaults: &defaults - type: Metric - generated: - by: "bad actor" - at: 2026-06-20T22:53:05Z -<<: *defaults +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("merged OKF metadata must still validate: %v", err) + t.Fatalf("OKF v0.2 note must pass daybook: %v", err) } - if !hasRule(fs, "generated_by_format") { - t.Fatalf("merged generated metadata was skipped: %+v", fs) + if hasRule(fs, "unknown_keys") { + t.Fatalf("OKF v0.2 keys should be known: %+v", fs) } - quoted := `--- -type: Metric -"<<": - generated: - by: "bad actor" - at: 2026-06-20T22:53:05Z ---- -body -` - qfs, qerr := validate("daybook", quoted) - if qerr != nil || hasRule(qfs, "generated_by_format") { - t.Fatalf("quoted << key must not act as a merge: findings=%+v err=%v", qfs, qerr) - } - - longTag := `--- -defaults: &defaults - type: Metric - generated: - by: "bad actor" - at: 2026-06-20T22:53:05Z -! "<<": *defaults ---- -body -` - lfs, lerr := validate("daybook", longTag) - if lerr != nil || !hasRule(lfs, "generated_by_format") { - t.Fatalf("long-form merge tag must act as a merge: findings=%+v err=%v", lfs, lerr) - } - aliasKey := `--- -type: Metric -defaults: &defaults - generated: - by: "bad actor" - at: 2026-06-20T22:53:05Z -merge_name: &merge_name "<<" -*merge_name: *defaults ---- -body -` - afs, aerr := validate("daybook", aliasKey) - if aerr != nil || hasRule(afs, "generated_by_format") { - t.Fatalf("aliased << key must not act as a merge: findings=%+v err=%v", afs, aerr) - } - explicitTag := `--- -type: Metric -defaults: &defaults - generated: - by: "bad actor" - at: 2026-06-20T22:53:05Z -! <<: *defaults ---- -body -` - efs, eerr := validate("daybook", explicitTag) - if eerr != nil || !hasRule(efs, "generated_by_format") { - t.Fatalf("explicit non-specific merge tag must act as a merge: findings=%+v err=%v", efs, eerr) + if _, err := validate("strict", raw); err != nil { + t.Fatalf("valid OKF v0.2 metadata must pass strict: %v", err) } } diff --git a/internal/okf/okf.go b/internal/okf/okf.go new file mode 100644 index 0000000..73dce3b --- /dev/null +++ b/internal/okf/okf.go @@ -0,0 +1,247 @@ +// Package okf validates the optional OKF v0.2 metadata families — +// trust/lifecycle events, stale_after, and provenance — against the +// structural rules of the Open Knowledge Format v0.2 spec (SPEC §5, §10). +// Violations are neutral data; level policy (error vs warning) is the +// calling profile's decision. +package okf + +import ( + "fmt" + "strings" + "time" + + "go.yaml.in/yaml/v3" +) + +// Violation is one structural rule breach in OKF v0.2 metadata. +type Violation struct { + Rule string + Message string +} + +// Validate checks the optional OKF v0.2 fields of a parsed frontmatter +// document node. Absent fields are meaningful (SPEC §5) and never +// flagged; malformed present fields each yield one violation. +func Validate(root *yaml.Node) []Violation { + m := topMapping(root) + if m == nil { + return nil + } + var vs []Violation + vs = append(vs, validateGenerated(m)...) + vs = append(vs, validateVerified(m)...) + vs = append(vs, validateStaleAfter(m)...) + vs = append(vs, validateProvenance(m)...) + return vs +} + +// validTimestamp reports whether value parses as RFC3339. The RFC3339 +// layout enforces the explicit UTC offset OKF §5 requires for every +// timestamp key; the offset itself may be any valid offset, not just +// zero. +func validTimestamp(value string) bool { + _, err := time.Parse(time.RFC3339, value) + return err == nil +} + +func violationf(rule, format string, args ...any) Violation { + return Violation{Rule: rule, Message: fmt.Sprintf(format, args...)} +} + +func validateGenerated(root *yaml.Node) []Violation { + generated, ok := mappingValue(root, "generated") + if !ok { + return nil + } + return validateEvent("generated", generated, false) +} + +func validateVerified(root *yaml.Node) []Violation { + verified, ok := mappingValue(root, "verified") + if !ok { + return nil + } + verified = unwrapNode(verified) + if verified == nil { + return []Violation{{Rule: "verified_format", Message: "verified must be a mapping or list of mappings"}} + } + switch verified.Kind { + case yaml.MappingNode: + return validateEventFields("verified", "verified", verified, true) + case yaml.SequenceNode: + var vs []Violation + for i, event := range verified.Content { + vs = append(vs, validateEventFields( + fmt.Sprintf("verified[%d]", i), "verified", unwrapNode(event), true, + )...) + } + return vs + default: + return []Violation{{Rule: "verified_format", Message: "verified must be a mapping or list of mappings"}} + } +} + +func validateStaleAfter(root *yaml.Node) []Violation { + staleAfter, ok := mappingValue(root, "stale_after") + if !ok { + return nil + } + value, scalar := nodeScalar(staleAfter) + if scalar && validTimestamp(value) { + return nil + } + return []Violation{violationf("stale_after_format", + "stale_after %q must be an RFC3339 datetime with an explicit UTC offset", value)} +} + +func validateProvenance(root *yaml.Node) []Violation { + provenance, ok := mappingValue(root, "provenance") + if !ok { + return nil + } + provenance = unwrapNode(provenance) + if provenance == nil || provenance.Kind != yaml.MappingNode { + return []Violation{{Rule: "provenance_format", Message: "provenance must be a mapping"}} + } + at, present := mappingValue(provenance, "at") + if !present { + return nil + } + value, scalar := nodeScalar(at) + if scalar && validTimestamp(value) { + return nil + } + return []Violation{violationf("provenance_at_format", + "provenance.at %q must be an RFC3339 datetime with an explicit UTC offset", value)} +} + +func validateEvent(field string, event *yaml.Node, requireAt bool) []Violation { + event = unwrapNode(event) + if event == nil || event.Kind != yaml.MappingNode { + return []Violation{violationf(field+"_format", "%s must be a mapping", field)} + } + // generated.at is optional when the production time is unknown. + return validateEventFields(field, field, event, requireAt) +} + +func validateEventFields(field, ruleBase string, event *yaml.Node, requireAt bool) []Violation { + var vs []Violation + if event == nil || event.Kind != yaml.MappingNode { + return []Violation{violationf(ruleBase+"_format", "%s must be a mapping", field)} + } + by, ok := mappingValue(event, "by") + if !ok { + vs = append(vs, violationf(ruleBase+"_by_missing", "%s.by is required", field)) + } else if value, scalar := nodeScalar(by); !scalar || !validActor(value) { + value, _ := nodeScalar(by) + vs = append(vs, violationf(ruleBase+"_by_format", + "%s.by %q must be /, human:, or process:", field, value)) + } + at, ok := mappingValue(event, "at") + if !ok { + if requireAt { + vs = append(vs, violationf(ruleBase+"_at_missing", "%s.at is required", field)) + } + } else if value, scalar := nodeScalar(at); !scalar || !validTimestamp(value) { + value, _ := nodeScalar(at) + vs = append(vs, violationf(ruleBase+"_at_format", + "%s.at %q must be an RFC3339 datetime with an explicit UTC offset", field, value)) + } + return vs +} + +func validActor(value string) bool { + if value == "" || strings.TrimSpace(value) != value || len(strings.Fields(value)) != 1 { + return false + } + for _, prefix := range []string{"human:", "process:"} { + if strings.HasPrefix(value, prefix) { + return len(value) > len(prefix) + } + } + parts := strings.Split(value, "/") + return len(parts) == 2 && parts[0] != "" && parts[1] != "" +} + +func topMapping(root *yaml.Node) *yaml.Node { + if root == nil || len(root.Content) == 0 { + return nil + } + m := unwrapNode(root.Content[0]) + if m == nil || m.Kind != yaml.MappingNode { + return nil + } + return m +} + +func mappingValue(mapping *yaml.Node, key string) (*yaml.Node, bool) { + return mappingValueWithMerges(unwrapNode(mapping), key) +} + +// mappingValueWithMerges resolves key with yaml.v3 merge-key semantics: +// plain "<<", the long-form "!!merge" tag, and the explicit non-specific +// tag all merge; quoted and aliased keys are ordinary keys. +func mappingValueWithMerges(mapping *yaml.Node, key string) (*yaml.Node, bool) { + if mapping == nil || mapping.Kind != yaml.MappingNode { + return nil, false + } + var merges []*yaml.Node + for i := 0; i+1 < len(mapping.Content); i += 2 { + rawName := mapping.Content[i] + name := unwrapNode(rawName) + if name == nil || name.Kind != yaml.ScalarNode { + continue + } + if name.Value == key { + return mapping.Content[i+1], true + } + if isMergeKey(rawName) { + merges = append(merges, mapping.Content[i+1]) + } + } + for _, merged := range merges { + if value, ok := mergedMappingValue(merged, key); ok { + return value, true + } + } + return nil, false +} + +func mergedMappingValue(merged *yaml.Node, key string) (*yaml.Node, bool) { + merged = unwrapNode(merged) + if merged == nil { + return nil, false + } + if merged.Kind == yaml.MappingNode { + return mappingValueWithMerges(merged, key) + } + if merged.Kind != yaml.SequenceNode { + return nil, false + } + for _, item := range merged.Content { + if value, ok := mergedMappingValue(item, key); ok { + return value, true + } + } + return nil, false +} + +func isMergeKey(n *yaml.Node) bool { + return n != nil && n.Kind == yaml.ScalarNode && n.Value == "<<" && + (n.Tag == "" || n.Tag == "!" || n.ShortTag() == "!!merge") +} + +func unwrapNode(n *yaml.Node) *yaml.Node { + for n != nil && n.Kind == yaml.AliasNode { + n = n.Alias + } + return n +} + +func nodeScalar(n *yaml.Node) (string, bool) { + n = unwrapNode(n) + if n == nil || n.Kind != yaml.ScalarNode || n.Tag == "!!null" { + return "", false + } + return n.Value, true +} diff --git a/internal/okf/okf_test.go b/internal/okf/okf_test.go new file mode 100644 index 0000000..9d0bfcb --- /dev/null +++ b/internal/okf/okf_test.go @@ -0,0 +1,220 @@ +package okf + +import ( + "testing" + + "go.yaml.in/yaml/v3" +) + +func parse(t *testing.T, raw string) *yaml.Node { + t.Helper() + var root yaml.Node + if err := yaml.Unmarshal([]byte(raw), &root); err != nil { + t.Fatalf("fixture must parse: %v", err) + } + return &root +} + +func validate(t *testing.T, raw string) []Violation { + t.Helper() + return Validate(parse(t, raw)) +} + +func hasViolation(vs []Violation, rule string) bool { + for _, v := range vs { + if v.Rule == rule { + return true + } + } + return false +} + +const richFixture = `--- +type: Attested Computation +title: Revenue +description: Recognized 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 + - by: process:finance-nightly + at: 2026-06-26T02:00:00+00:00 +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 +provenance: + agent: kernel + at: 2026-06-28T14:00:00Z + via: cli +status: stable +created: 2026-06-20T22:53:05Z +tags: [finance] +--- +body +` + +func TestValidateRichFixture(t *testing.T) { + if vs := validate(t, richFixture); len(vs) != 0 { + t.Fatalf("valid OKF v0.2 signals must not be flagged: %+v", vs) + } +} + +// OKF v0.2 SPEC §5: "Every timestamp-valued key in OKF is an ISO 8601 +// datetime with an explicit UTC offset." Any explicit offset conforms; +// offset zero is one valid spelling, not the only one. +func TestValidateAcceptsNonZeroOffsets(t *testing.T) { + raw := `--- +generated: + by: human:alice + at: 2026-06-20T22:53:05+05:30 +verified: + - by: process:nightly + at: 2026-06-25T09:00:00-04:00 +stale_after: 2026-12-31T00:00:00+01:00 +provenance: + at: 2026-06-28T14:00:00+02:00 +--- +` + vs := validate(t, raw) + for _, rule := range []string{ + "generated_at_format", "verified_at_format", "stale_after_format", "provenance_at_format", + } { + if hasViolation(vs, rule) { + t.Fatalf("explicit nonzero offset must satisfy %s: %+v", rule, vs) + } + } +} + +// The offset must be explicit: RFC3339 datetimes without an offset +// fail the layout parse and violate every timestamp rule. +func TestValidateRejectsMissingOffset(t *testing.T) { + raw := `--- +generated: + by: human:alice + at: 2026-06-20T22:53:05 +provenance: + at: 2026-06-28T14:00:00 +--- +` + vs := validate(t, raw) + if !hasViolation(vs, "generated_at_format") || !hasViolation(vs, "provenance_at_format") { + t.Fatalf("offset-less datetimes must violate: %+v", vs) + } +} + +func TestValidateRejectsMalformedSignals(t *testing.T) { + raw := `--- +generated: + by: "human:" + at: 2026-06-20 +verified: + - by: agent + at: 2026-06-25 +stale_after: not-a-date +provenance: + at: yesterday +--- +` + vs := validate(t, raw) + for _, rule := range []string{ + "generated_by_format", "generated_at_format", + "verified_by_format", "verified_at_format", + "stale_after_format", "provenance_at_format", + } { + if !hasViolation(vs, rule) { + t.Fatalf("missing %s in %+v", rule, vs) + } + } +} + +func TestValidateVerifiedMapping(t *testing.T) { + vs := validate(t, "---\nverified: {by: process:nightly, at: 2026-06-25T09:00:00Z}\n---\n") + if hasViolation(vs, "verified_format") { + t.Fatalf("bare verified mapping is valid OKF v0.2: %+v", vs) + } +} + +func TestValidateGeneratedAtOptional(t *testing.T) { + raw := `--- +generated: + by: human:alice +--- +` + if vs := validate(t, raw); len(vs) != 0 { + t.Fatalf("generated.at is optional when production time is unknown: %+v", vs) + } +} + +func TestValidateMergeKeys(t *testing.T) { + plain := `--- +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +<<: *defaults +--- +` + if vs := validate(t, plain); !hasViolation(vs, "generated_by_format") { + t.Fatalf("merged generated metadata was skipped: %+v", vs) + } + quoted := `--- +:"<<": + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +--- +` + if vs := validate(t, quoted); hasViolation(vs, "generated_by_format") { + t.Fatalf("quoted << key must not act as a merge: %+v", vs) + } + longTag := `--- +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +! "<<": *defaults +--- +` + if vs := validate(t, longTag); !hasViolation(vs, "generated_by_format") { + t.Fatalf("long-form merge tag must act as a merge: %+v", vs) + } + aliasKey := `--- +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +merge_name: &merge_name "<<" +*merge_name: *defaults +--- +` + if vs := validate(t, aliasKey); hasViolation(vs, "generated_by_format") { + t.Fatalf("aliased << key must not act as a merge: %+v", vs) + } + explicitTag := `--- +defaults: &defaults + generated: + by: "bad actor" + at: 2026-06-20T22:53:05Z +! <<: *defaults +--- +` + if vs := validate(t, explicitTag); !hasViolation(vs, "generated_by_format") { + t.Fatalf("explicit non-specific merge tag must act as a merge: %+v", vs) + } +} diff --git a/modules.budget b/modules.budget index 2778f33..079776b 100644 --- a/modules.budget +++ b/modules.budget @@ -12,7 +12,11 @@ kernel internal/kernel 58400 # FlagSet-driven splitArgs: VisitAll replaces per-command value-flag maps. cli internal/cli 16200 -fm internal/fm 8100 +# PR #40: OKF v0.2 vocabulary split out of fm so the fm ratchet holds; +# timestamp messages aligned to OKF SPEC §5 (RFC3339 with an explicit +# UTC offset — any offset, not just zero). +fm internal/fm 9000 +okf internal/okf 4000 mcp internal/mcp 5000 orient internal/orient 4500 qmd internal/qmd 9000 From 737103fe872812481d9683a12aa3cf613e4e6f63 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Tue, 1 Sep 2026 15:24:42 -0500 Subject: [PATCH 08/10] test(fm): pin strict OKF promotion; record truthful fm raise reason LevelPolicy's strict branch completed the floor keys so key_missing cannot mask the OKF finding, and now asserts the contract error is a promoted OKF violation via ContractFinding (mutation-checked: the masking variant fails with Rule:key_missing). modules.budget states the actual 8100 -> 9000 raise reason: residual fm-owned profile wiring after the okf split. --- internal/fm/frontmatter_test.go | 19 ++++++++++++++++++- modules.budget | 9 ++++++--- 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index 90e6f7f..fa665ed 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -119,6 +119,10 @@ func TestValidateStrict(t *testing.T) { 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 @@ -139,9 +143,22 @@ body t.Fatalf("daybook must warn, never error, on %v", f) } } - if _, err := validate("strict", malformed); err == nil { + _, 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 a + // promoted OKF violation, not the floor key_missing. + f, ok := ContractFinding(err) + if !ok { + t.Fatalf("strict error is not a finding: %v", err) + } + okfRules := map[string]bool{ + "generated_by_format": true, "generated_at_format": true, "provenance_at_format": true, + } + if f.Level != "error" || !okfRules[f.Rule] { + t.Fatalf("strict must promote an OKF violation, got %+v", f) + } } // Every OKF v0.2 vocabulary key is a known key under the daybook diff --git a/modules.budget b/modules.budget index 079776b..141ee36 100644 --- a/modules.budget +++ b/modules.budget @@ -12,9 +12,12 @@ kernel internal/kernel 58400 # FlagSet-driven splitArgs: VisitAll replaces per-command value-flag maps. cli internal/cli 16200 -# PR #40: OKF v0.2 vocabulary split out of fm so the fm ratchet holds; -# timestamp messages aligned to OKF SPEC §5 (RFC3339 with an explicit -# UTC offset — any offset, not just zero). +# PR #40: OKF v0.2 vocabulary split out of fm into internal/okf +# (11,593 -> 8,952 here, 3,692 there); timestamp messages aligned to +# OKF SPEC §5 (RFC3339 with an explicit UTC offset — any offset, not +# just zero). The 8100 -> 9000 rise covers the residual fm-owned +# profile wiring: level-policy and known-keys coverage the split +# leaves in fm. fm internal/fm 9000 okf internal/okf 4000 mcp internal/mcp 5000 From d38e9c5a96a4b3c4cd5fa3d594e1543fcadb0ce9 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Tue, 1 Sep 2026 15:27:47 -0500 Subject: [PATCH 09/10] test(fm): pin strict OKF promotion; calibrate fm ratchet at 9200 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LevelPolicy's strict branch completes the floor keys so key_missing cannot mask the OKF finding, then asserts the contract error is the promoted generated_by_format violation via ContractFinding — the masking mutation fails with Rule:key_missing. modules.budget states the actual raise reason (residual fm-owned profile wiring after the okf split) and calibrates fm at 9200 for the measured 9,057. --- internal/fm/frontmatter_test.go | 13 ++++--------- modules.budget | 4 ++-- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/internal/fm/frontmatter_test.go b/internal/fm/frontmatter_test.go index fa665ed..7a6fb4c 100644 --- a/internal/fm/frontmatter_test.go +++ b/internal/fm/frontmatter_test.go @@ -126,8 +126,6 @@ tags: [finance] generated: by: "human:" at: 2026-06-20 -provenance: - at: yesterday --- body ` @@ -135,7 +133,7 @@ body 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") || !hasRule(fs, "provenance_at_format") { + if !hasRule(fs, "generated_by_format") || !hasRule(fs, "generated_at_format") { t.Fatalf("daybook missing OKF violations in %+v", fs) } for _, f := range fs { @@ -147,17 +145,14 @@ body if err == nil { t.Fatal("strict must reject malformed OKF v0.2 signals") } - // With the floor keys satisfied, the contract error must be a + // 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) } - okfRules := map[string]bool{ - "generated_by_format": true, "generated_at_format": true, "provenance_at_format": true, - } - if f.Level != "error" || !okfRules[f.Rule] { - t.Fatalf("strict must promote an OKF violation, got %+v", f) + if f.Level != "error" || f.Rule != "generated_by_format" { + t.Fatalf("strict must promote the generated_by_format violation, got %+v", f) } } diff --git a/modules.budget b/modules.budget index 141ee36..060e463 100644 --- a/modules.budget +++ b/modules.budget @@ -17,8 +17,8 @@ cli internal/cli 16200 # OKF SPEC §5 (RFC3339 with an explicit UTC offset — any offset, not # just zero). The 8100 -> 9000 rise covers the residual fm-owned # profile wiring: level-policy and known-keys coverage the split -# leaves in fm. -fm internal/fm 9000 +# leaves in fm; the strict-promotion coverage completes it at 9,113. +fm internal/fm 9200 okf internal/okf 4000 mcp internal/mcp 5000 orient internal/orient 4500 From dbdfd0051a881e65d267ee68dddd4fe05ee71fd7 Mon Sep 17 00:00:00 2001 From: phaedrus Date: Tue, 1 Sep 2026 15:32:21 -0500 Subject: [PATCH 10/10] chore(budget): state the actual fm raise and measured size The audit comment cited the superseded 9000 ceiling and a stale 9,113 measurement; it now records the real raise (8100 -> 9200), the proven 9,057, and the coverage that justifies it. --- modules.budget | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules.budget b/modules.budget index 060e463..774f70a 100644 --- a/modules.budget +++ b/modules.budget @@ -13,11 +13,11 @@ kernel internal/kernel 58400 # FlagSet-driven splitArgs: VisitAll replaces per-command value-flag maps. cli internal/cli 16200 # PR #40: OKF v0.2 vocabulary split out of fm into internal/okf -# (11,593 -> 8,952 here, 3,692 there); timestamp messages aligned to +# (11,593 -> 9,057 here, 3,692 there); timestamp messages aligned to # OKF SPEC §5 (RFC3339 with an explicit UTC offset — any offset, not -# just zero). The 8100 -> 9000 rise covers the residual fm-owned -# profile wiring: level-policy and known-keys coverage the split -# leaves in fm; the strict-promotion coverage completes it at 9,113. +# just zero). The 8100 -> 9200 rise covers the residual fm-owned +# profile wiring after the split: level-policy (incl. strict OKF +# promotion, mutation-checked) and known-keys coverage; measured 9,057. fm internal/fm 9200 okf internal/okf 4000 mcp internal/mcp 5000