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
74 changes: 74 additions & 0 deletions internal/operad/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package operad
import (
"encoding/json"
"fmt"
"log"
"os"
"sort"
"strings"

"moos/kernel/internal/graph"
)
Expand Down Expand Up @@ -103,9 +106,72 @@ func LoadRegistry(path string) (*Registry, error) {
reg.PortColorMatrix = parseColorMatrix(raw.PortColorCompatibility.Matrix)
}

// Port-name → color map (§12.1, moos-kernel#50): built-in defaults merged
// with the optional ontology override object. The override lets later
// ontology versions add or recolor ports without a kernel release; the
// literal empty-string color declares an explicit exemption (matrix
// check skipped). Unknown color names and JSON null are load errors — a
// typo or a None-emitting script would otherwise silently weaken the
// fail-closed gate (null would alias onto the exemption).
reg.PortColors = DefaultPortColors()
for port, colorName := range raw.PortColorCompatibility.PortColorMap {
if colorName == nil {
return nil, fmt.Errorf("operad: port_color_map: null color for port %q — use \"\" for an explicit exemption", port)
}
color := graph.PortColor(*colorName)
if *colorName != "" && !knownPortColor(color) {
return nil, fmt.Errorf("operad: port_color_map: unknown color %q for port %q (valid: auth, topology, transport, compute, storage, workflow, semantic, projection, or \"\" for exempt)", *colorName, port)
}
reg.PortColors[port] = color
}

// Load-time coverage check (soft, moos-kernel#50 review): every declared
// pair's ports should have a color entry — an uncovered port means every
// LINK on that pair will be rejected fail-closed at validation time.
// Warn loudly but boot anyway: refusing to start on a future ontology's
// new pair would trade a scoped LINK failure for a bricked fleet.
var uncovered []string
for wf, spec := range reg.RewriteCategories {
for _, pr := range declaredPairs(spec) {
for _, port := range pr {
if _, ok := reg.PortColors[port]; !ok {
uncovered = append(uncovered, fmt.Sprintf("%s:%s", wf, port))
}
}
}
}
if len(uncovered) > 0 {
sort.Strings(uncovered)
log.Printf("operad: WARNING — %d declared port(s) have no color; LINKs on their pairs will be REJECTED fail-closed (§12.2): %s (add them to port_color_compatibility.port_color_map)",
len(uncovered), strings.Join(uncovered, ", "))
}

return reg, nil
}

// declaredPairs returns every (src, tgt) port pair the WF declares —
// the primary pair (when present) plus all additional_port_pairs.
func declaredPairs(spec RewriteCategorySpec) [][2]string {
pairs := make([][2]string, 0, 1+len(spec.AdditionalPortPairs))
if spec.SrcPort != "" || spec.TgtPort != "" {
pairs = append(pairs, [2]string{spec.SrcPort, spec.TgtPort})
}
for _, ap := range spec.AdditionalPortPairs {
pairs = append(pairs, [2]string{ap.SrcPort, ap.TgtPort})
}
return pairs
}

// knownPortColor reports whether c is one of the eight §12.1 colors.
func knownPortColor(c graph.PortColor) bool {
switch c {
case graph.ColorAuth, graph.ColorTopology, graph.ColorTransport, graph.ColorCompute,
graph.ColorStorage, graph.ColorWorkflow, graph.ColorSemantic, graph.ColorProjection:
return true
}
return false
}

func parseNodeTypeSpec(raw rawNodeType) NodeTypeSpec {
spec := NodeTypeSpec{
ID: graph.TypeID(raw.ID),
Expand Down Expand Up @@ -225,4 +291,12 @@ type rawAdditionalPortPair struct {

type rawPortColorCompat struct {
Matrix map[string]map[string]any `json:"matrix"`
// PortColorMap is the optional port-name → color-name override object
// (moos-kernel#50). Absent in ontology v4.0.1 — DefaultPortColors carries
// the full vocabulary until an ontology round adopts this key. Values are
// pointers so JSON null is distinguishable from the literal "" exemption
// (null is a load error). Sibling keys (description, port_colors
// vocabulary array, projection_rule, semantic_rule, declared_pairs_by_wf)
// remain doc-only and unloaded.
PortColorMap map[string]*string `json:"port_color_map"`
}
185 changes: 185 additions & 0 deletions internal/operad/loader_test.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,14 @@
package operad

import (
"bytes"
"log"
"os"
"path/filepath"
"strings"
"testing"

"moos/kernel/internal/graph"
)

// TestLoadRegistry_Version verifies the top-level "version" field of
Expand Down Expand Up @@ -57,3 +62,183 @@ func TestLoadRegistry_EmptyPath(t *testing.T) {
t.Errorf("LoadRegistry(\"\").Version = %q, want empty string", reg.Version)
}
}

// ------------------------------------------------------------------
// §12.1/§12.2 color loading (moos-kernel#50)
// ------------------------------------------------------------------

func writeOntology(t *testing.T, body string) string {
t.Helper()
path := filepath.Join(t.TempDir(), "ontology.json")
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
t.Fatalf("seed ontology: %v", err)
}
return path
}

// TestParseColorMatrix_ValueKinds covers the four cell encodings the loaded
// ontology uses (bool true/false, "wf15_only", "sink_only") plus the
// junk-string fallback — previously untested.
func TestParseColorMatrix_ValueKinds(t *testing.T) {
m := parseColorMatrix(map[string]map[string]any{
"workflow": {
"workflow": true,
"projection": "sink_only",
"semantic": "wf15_only",
"auth": false,
"storage": "garbage",
},
})
if !m.Allowed(graph.ColorWorkflow, graph.ColorWorkflow, graph.WF19) {
t.Errorf("bool true should parse to allowed")
}
if m.Allowed(graph.ColorWorkflow, graph.ColorProjection, graph.WF19) {
t.Errorf("sink_only should reject non-projection use")
}
if m.Allowed(graph.ColorWorkflow, graph.ColorSemantic, graph.WF19) {
t.Errorf("wf15_only should reject under WF19")
}
if !m.Allowed(graph.ColorWorkflow, graph.ColorSemantic, graph.WF15) {
t.Errorf("wf15_only should allow under WF15")
}
if m.Allowed(graph.ColorWorkflow, graph.ColorAuth, graph.WF19) {
t.Errorf("bool false should parse to rejected")
}
if m.Allowed(graph.ColorWorkflow, graph.ColorStorage, graph.WF19) {
t.Errorf("unrecognized string should parse to rejected")
}
if m.Allowed(graph.ColorAuth, graph.ColorAuth, graph.WF19) {
t.Errorf("missing src row should reject")
}
}

// TestLoadRegistry_PortColors_DefaultsWhenKeyAbsent: an ontology without
// port_color_map (v4.0.1 shape) still yields the full built-in vocabulary.
func TestLoadRegistry_PortColors_DefaultsWhenKeyAbsent(t *testing.T) {
path := writeOntology(t, `{
"version": "4.0.1",
"types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []},
"rewrite_categories": [],
"port_color_compatibility": {"matrix": {}}
}`)
reg, err := LoadRegistry(path)
if err != nil {
t.Fatalf("LoadRegistry: %v", err)
}
if got := reg.PortColors["opens-on"]; got != graph.ColorWorkflow {
t.Errorf("defaults not applied: opens-on = %q, want workflow", got)
}
}

// TestLoadRegistry_PortColorMapOverride: the optional ontology object can
// recolor an existing port, add a new one, and declare an exemption — all
// merged over the defaults without a kernel release.
func TestLoadRegistry_PortColorMapOverride(t *testing.T) {
path := writeOntology(t, `{
"version": "4.0.2-test",
"types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []},
"rewrite_categories": [],
"port_color_compatibility": {
"matrix": {},
"port_color_map": {
"opens-on": "topology",
"future-port": "auth",
"weird-port": ""
}
}
}`)
reg, err := LoadRegistry(path)
if err != nil {
t.Fatalf("LoadRegistry: %v", err)
}
if got := reg.PortColors["opens-on"]; got != graph.ColorTopology {
t.Errorf("override lost: opens-on = %q, want topology", got)
}
if got := reg.PortColors["future-port"]; got != graph.ColorAuth {
t.Errorf("new port lost: future-port = %q, want auth", got)
}
if got, ok := reg.PortColors["weird-port"]; !ok || got != "" {
t.Errorf("explicit exemption lost: weird-port = (%q, %v), want (\"\", true)", got, ok)
}
if got := reg.PortColors["governs"]; got != graph.ColorAuth {
t.Errorf("untouched default lost: governs = %q, want auth", got)
}
}

// TestLoadRegistry_PortColorMap_UnknownColorErrors: a typo'd color name must
// fail the load loudly — silently accepting it would weaken the fail-closed
// gate in ways only visible at LINK time.
func TestLoadRegistry_PortColorMap_UnknownColorErrors(t *testing.T) {
path := writeOntology(t, `{
"version": "4.0.2-test",
"types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []},
"rewrite_categories": [],
"port_color_compatibility": {
"matrix": {},
"port_color_map": {"some-port": "chartreuse"}
}
}`)
_, err := LoadRegistry(path)
if err == nil {
t.Fatalf("expected load error for unknown color name")
}
if !strings.Contains(err.Error(), "chartreuse") || !strings.Contains(err.Error(), "some-port") {
t.Errorf("error should name the color and the port; got %q", err.Error())
}
}

// TestLoadRegistry_PortColorMap_NullColorErrors: JSON null must not alias
// onto the "" exemption (adversarial-review finding on #50). A None-emitting
// script would otherwise silently exempt a port from the matrix check.
func TestLoadRegistry_PortColorMap_NullColorErrors(t *testing.T) {
path := writeOntology(t, `{
"version": "4.0.2-test",
"types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []},
"rewrite_categories": [],
"port_color_compatibility": {
"matrix": {},
"port_color_map": {"opens-on": null}
}
}`)
_, err := LoadRegistry(path)
if err == nil {
t.Fatalf("expected load error for null color value")
}
if !strings.Contains(err.Error(), "null") || !strings.Contains(err.Error(), "opens-on") {
t.Errorf("error should name null and the port; got %q", err.Error())
}
if !strings.Contains(err.Error(), `""`) {
t.Errorf("error should point at the \"\" exemption syntax; got %q", err.Error())
}
}

// TestLoadRegistry_UncoveredDeclaredPort_WarnsButBoots: a declared pair whose
// port has no color entry must not brick the load — it logs a loud warning
// and the fail-closed gate handles it at LINK time (soft-first posture).
func TestLoadRegistry_UncoveredDeclaredPort_WarnsButBoots(t *testing.T) {
path := writeOntology(t, `{
"version": "4.0.2-test",
"types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []},
"rewrite_categories": [
{"id": "WF98", "name": "Future category", "allowed_rewrites": ["LINK"],
"src_port": "future-src", "tgt_port": "future-tgt"}
],
"port_color_compatibility": {"matrix": {}}
}`)
var buf bytes.Buffer
prev := log.Writer()
log.SetOutput(&buf)
defer log.SetOutput(prev)

reg, err := LoadRegistry(path)
if err != nil {
t.Fatalf("uncovered declared port must not fail the load: %v", err)
}
if _, ok := reg.PortColors["future-src"]; ok {
t.Errorf("future-src should not be colored by defaults in this fixture")
}
warned := buf.String()
if !strings.Contains(warned, "WARNING") || !strings.Contains(warned, "WF98:future-src") {
t.Errorf("expected loud coverage warning naming WF98:future-src; got %q", warned)
}
}
Loading
Loading