From d57a3411e028abe8776e9083d621aa45161c7eb0 Mon Sep 17 00:00:00 2001 From: Sam Maassen <215926119+MSD21091969@users.noreply.github.com> Date: Fri, 9 Oct 2026 14:37:55 +0200 Subject: [PATCH] Refs #79: Enforce strict type list checking and wildcard compatibility --- internal/fold/errors.go | 20 +-- internal/graph/node.go | 6 +- internal/graph/state.go | 6 +- internal/hdc/crosswalk.go | 2 +- internal/hdc/fiber.go | 4 +- internal/kernel/liveness_test.go | 2 +- internal/kernel/sweep.go | 6 +- internal/mcp/protocol.go | 6 +- internal/operad/color_equality_t342_test.go | 22 ++-- internal/operad/loader.go | 33 +++++ internal/operad/loader_closure_t342_test.go | 2 +- .../operad/loader_color_source_t342_test.go | 10 +- internal/operad/loader_test.go | 2 +- internal/operad/occupancy.go | 4 +- internal/operad/occupancy_test.go | 5 +- internal/operad/ports_generate_t342_test.go | 4 +- internal/operad/registry.go | 3 +- internal/operad/validate.go | 28 ++--- .../operad/wildcard_acceptance_t342_test.go | 116 ++++++++++++++++++ internal/reactive/engine_test.go | 10 +- internal/reactive/predicate_test.go | 6 +- internal/transport/auth_test.go | 2 +- internal/transport/server_port_colors_test.go | 2 +- 23 files changed, 225 insertions(+), 76 deletions(-) create mode 100644 internal/operad/wildcard_acceptance_t342_test.go diff --git a/internal/fold/errors.go b/internal/fold/errors.go index 99849cd..ac48edb 100644 --- a/internal/fold/errors.go +++ b/internal/fold/errors.go @@ -6,9 +6,9 @@ import "errors" // These are returned by Evaluate and matched by Replay for idempotent skip logic. var ( - ErrNodeExists = errors.New("node already exists") - ErrNodeNotFound = errors.New("node not found") - ErrRelationExists = errors.New("relation already exists") + ErrNodeExists = errors.New("node already exists") + ErrNodeNotFound = errors.New("node not found") + ErrRelationExists = errors.New("relation already exists") ErrRelationNotFound = errors.New("relation not found") ErrImmutableProperty = errors.New("property is immutable — cannot MUTATE") @@ -16,12 +16,12 @@ var ( ErrUnauthorized = errors.New("actor does not satisfy authority_scope for this field") ErrVersionConflict = errors.New("optimistic CAS failed — version mismatch") - ErrInvalidRewriteType = errors.New("unknown rewrite type") - ErrMissingActor = errors.New("envelope missing actor URN") - ErrMissingNodeURN = errors.New("ADD envelope missing node_urn") - ErrMissingTypeID = errors.New("ADD envelope missing type_id") - ErrMissingRelationURN = errors.New("LINK/UNLINK envelope missing relation_urn") - ErrMissingSrcTgt = errors.New("LINK envelope missing src_urn or tgt_urn") - ErrMissingTargetField = errors.New("MUTATE envelope missing target_urn or field") + ErrInvalidRewriteType = errors.New("unknown rewrite type") + ErrMissingActor = errors.New("envelope missing actor URN") + ErrMissingNodeURN = errors.New("ADD envelope missing node_urn") + ErrMissingTypeID = errors.New("ADD envelope missing type_id") + ErrMissingRelationURN = errors.New("LINK/UNLINK envelope missing relation_urn") + ErrMissingSrcTgt = errors.New("LINK envelope missing src_urn or tgt_urn") + ErrMissingTargetField = errors.New("MUTATE envelope missing target_urn or field") ErrWF15MissingContract = errors.New("WF15 relation requires contract_urn") ) diff --git a/internal/graph/node.go b/internal/graph/node.go index 14a7075..b3b6cc6 100644 --- a/internal/graph/node.go +++ b/internal/graph/node.go @@ -11,10 +11,10 @@ type TypeID string // Changing properties does not change identity (CI-3). // An ADD rewrite creates a node. No rewrite deletes it. type Node struct { - URN URN `json:"urn"` - TypeID TypeID `json:"type_id"` + URN URN `json:"urn"` + TypeID TypeID `json:"type_id"` Properties map[string]Property `json:"properties"` - CreatedAt time.Time `json:"created_at"` + CreatedAt time.Time `json:"created_at"` // Version is a monotonic counter incremented on every MUTATE. // Used for optimistic CAS in MUTATE rewrites. Version int64 `json:"version"` diff --git a/internal/graph/state.go b/internal/graph/state.go index aa07c6c..7ce0d0b 100644 --- a/internal/graph/state.go +++ b/internal/graph/state.go @@ -10,9 +10,9 @@ package graph // indexes so hot paths (sweep, t-cone, occupancy walks) don't pay O(N) or // O(R) scans on every call: // -// NodesByType — for "give me all t_hooks" / "all governance_proposals" -// RelationsBySrc — for "what does session X point at?" (has-occupant walk) -// RelationsByTgt — for "who points at this role?" (governed-by walk) +// NodesByType — for "give me all t_hooks" / "all governance_proposals" +// RelationsBySrc — for "what does session X point at?" (has-occupant walk) +// RelationsByTgt — for "who points at this role?" (governed-by walk) // // Indexes are derived from Nodes/Relations — they are NOT serialized (the // JSON wire shape stays (nodes, relations) for backward compatibility with diff --git a/internal/hdc/crosswalk.go b/internal/hdc/crosswalk.go index c563a4f..8d714da 100644 --- a/internal/hdc/crosswalk.go +++ b/internal/hdc/crosswalk.go @@ -295,7 +295,7 @@ func explicitCrosswalkPairs(state graph.GraphState) map[string]struct{} { } srcNode, srcOk := state.Nodes[rel.SrcURN] tgtNode, tgtOk := state.Nodes[rel.TgtURN] - + var srcPtr, tgtPtr *graph.Node if srcOk { srcPtr = &srcNode diff --git a/internal/hdc/fiber.go b/internal/hdc/fiber.go index 371b4ff..363aa5b 100644 --- a/internal/hdc/fiber.go +++ b/internal/hdc/fiber.go @@ -55,7 +55,7 @@ func EncodeFiber(state graph.GraphState, kernelURN graph.URN, enc *Encoder) HV { } nodeURNs := NodesInKernel(state, kernelURN) vectors := make([]HV, 0, len(nodeURNs)) - + encodedBatch := enc.EncodeNodes(state, nodeURNs) for _, urn := range nodeURNs { vectors = append(vectors, encodedBatch[urn]) @@ -292,7 +292,7 @@ func shardPrefixToKernelMap(state graph.GraphState, kernels []graph.URN) map[str if !ok || strings.TrimSpace(prefix) == "" { continue } - + for _, rel := range state.Relations { if rel.SrcURN == shardURN && rel.SrcPort == "routes-to" { if kernelURN, hit := kernelSet[rel.TgtURN.String()]; hit { diff --git a/internal/kernel/liveness_test.go b/internal/kernel/liveness_test.go index 7c49762..4d1534a 100644 --- a/internal/kernel/liveness_test.go +++ b/internal/kernel/liveness_test.go @@ -706,7 +706,7 @@ func TestApplyProgram_M12_IntraBatchADDThenKernelAuthorityMUTATE_Rejected(t *tes RewriteType: graph.MUTATE, Actor: "urn:moos:agent:claude", TargetURN: "urn:moos:program:freshly-baked", // created in envelope 1 - Field: "target_t", // authority_scope=kernel per type spec + Field: "target_t", // authority_scope=kernel per type spec NewValue: 200.0, }, } diff --git a/internal/kernel/sweep.go b/internal/kernel/sweep.go index b595c56..be2ca8f 100644 --- a/internal/kernel/sweep.go +++ b/internal/kernel/sweep.go @@ -155,9 +155,9 @@ func SweepOnce(state graph.GraphState, currentT int, actor graph.URN, baseLogSeq // stageHookProposalPair builds the atomic two-envelope staging shape for a // firing t_hook: // -// 1. ADD a governance_proposal node (title, status=pending, -// source_t_hook_urn, fires_at_t, proposed_envelope, owner_urn) -// 2. MUTATE the source hook's firing_state → "proposed" +// 1. ADD a governance_proposal node (title, status=pending, +// source_t_hook_urn, fires_at_t, proposed_envelope, owner_urn) +// 2. MUTATE the source hook's firing_state → "proposed" // // Shared by the TIME sweep (SweepOnce) and the M6 event pathway // (Runtime.stageEventHookLocked) — governance parity by construction diff --git a/internal/mcp/protocol.go b/internal/mcp/protocol.go index dd0571d..be16422 100644 --- a/internal/mcp/protocol.go +++ b/internal/mcp/protocol.go @@ -12,9 +12,9 @@ type Request struct { } type Response struct { - JSONRPC string `json:"jsonrpc"` - ID any `json:"id"` - Result any `json:"result,omitempty"` + JSONRPC string `json:"jsonrpc"` + ID any `json:"id"` + Result any `json:"result,omitempty"` Error *RPCError `json:"error,omitempty"` } diff --git a/internal/operad/color_equality_t342_test.go b/internal/operad/color_equality_t342_test.go index d81380f..d362934 100644 --- a/internal/operad/color_equality_t342_test.go +++ b/internal/operad/color_equality_t342_test.go @@ -239,27 +239,31 @@ func legacyColorStage98f2ccc(colors map[string]string, matrix PortColorMatrix, p func tableOntology(t *testing.T, version string, pairs []declaredPair, own map[string]string, matrix map[string]map[string]any) string { t.Helper() type rawPair struct { - Src string `json:"src_port"` - Tgt string `json:"tgt_port"` + Src string `json:"src_port"` + Tgt string `json:"tgt_port"` + SrcTypes []string `json:"src_types"` + TgtTypes []string `json:"tgt_types"` } type rawWF struct { - ID string `json:"id"` - Allowed []string `json:"allowed_rewrites"` - Src string `json:"src_port"` - Tgt string `json:"tgt_port"` - Add []rawPair `json:"additional_port_pairs,omitempty"` + ID string `json:"id"` + Allowed []string `json:"allowed_rewrites"` + Src string `json:"src_port"` + Tgt string `json:"tgt_port"` + SrcTypes []string `json:"src_types"` + TgtTypes []string `json:"tgt_types"` + Add []rawPair `json:"additional_port_pairs,omitempty"` } var wfs []*rawWF byID := map[graph.RewriteCategory]*rawWF{} for _, p := range pairs { w, ok := byID[p.wf] if !ok { - w = &rawWF{ID: string(p.wf), Allowed: []string{"LINK", "UNLINK"}, Src: p.src, Tgt: p.tgt} + w = &rawWF{ID: string(p.wf), Allowed: []string{"LINK", "UNLINK"}, Src: p.src, Tgt: p.tgt, SrcTypes: []string{"*"}, TgtTypes: []string{"*"}} byID[p.wf] = w wfs = append(wfs, w) continue } - w.Add = append(w.Add, rawPair{p.src, p.tgt}) + w.Add = append(w.Add, rawPair{Src: p.src, Tgt: p.tgt, SrcTypes: []string{"*"}, TgtTypes: []string{"*"}}) } body, err := json.Marshal(map[string]any{ "version": version, diff --git a/internal/operad/loader.go b/internal/operad/loader.go index 0518a8f..61ff7ae 100644 --- a/internal/operad/loader.go +++ b/internal/operad/loader.go @@ -60,6 +60,15 @@ func LoadRegistry(path string) (*Registry, error) { for _, rt := range wf.AllowedRewrites { spec.AllowedRewrites = append(spec.AllowedRewrites, graph.RewriteType(rt)) } + if wf.SrcPort != "" || wf.TgtPort != "" { + pairLabel := fmt.Sprintf("primary pair (%s, %s)", wf.SrcPort, wf.TgtPort) + if err := validateTypeList(wf.ID, pairLabel, "src_types", wf.SrcTypes); err != nil { + return nil, err + } + if err := validateTypeList(wf.ID, pairLabel, "tgt_types", wf.TgtTypes); err != nil { + return nil, err + } + } for _, t := range wf.SrcTypes { spec.SrcTypes = append(spec.SrcTypes, graph.TypeID(t)) } @@ -87,6 +96,13 @@ func LoadRegistry(path string) (*Registry, error) { PromotesFragment: app.PromotesFragment, Description: app.Description, } + pairLabel := fmt.Sprintf("additional pair (%s, %s)", app.SrcPort, app.TgtPort) + if err := validateTypeList(wf.ID, pairLabel, "src_types", app.SrcTypes); err != nil { + return nil, err + } + if err := validateTypeList(wf.ID, pairLabel, "tgt_types", app.TgtTypes); err != nil { + return nil, err + } for i, t := range app.SrcTypes { pair.SrcTypes[i] = graph.TypeID(t) } @@ -672,3 +688,20 @@ type rawPortColorCompat struct { // remain doc-only and unloaded. PortColorMap map[string]*string `json:"port_color_map"` } + +func validateTypeList(wfID string, pairLabel string, key string, types []string) error { + if len(types) == 0 { + return fmt.Errorf("operad: %s %s: %s is empty or missing — list the admitted types, or [\"*\"] for all types", wfID, pairLabel, key) + } + hasStar := false + for _, t := range types { + if t == "*" { + hasStar = true + break + } + } + if hasStar && len(types) > 1 { + return fmt.Errorf("operad: %s %s: %s cannot mix '*' wildcard with other types", wfID, pairLabel, key) + } + return nil +} diff --git a/internal/operad/loader_closure_t342_test.go b/internal/operad/loader_closure_t342_test.go index abf68c0..70b9729 100644 --- a/internal/operad/loader_closure_t342_test.go +++ b/internal/operad/loader_closure_t342_test.go @@ -102,7 +102,7 @@ func TestLoadRegistry_ColorClosure_Rejections_t342(t *testing.T) { // projection, which this ontology's vocabulary does not declare. name: "legacy color at a declared port outside port_colors", section: `{"port_colors": ["auth", "topology", "transport", "compute", "storage", "workflow", "semantic"], "matrix": {}}`, - wfs: `[{"id": "WF16", "allowed_rewrites": ["LINK"], "src_port": "projected-to", "tgt_port": "rendered-as"}]`, + wfs: `[{"id": "WF16", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "projected-to", "tgt_port": "rendered-as"}]`, want: []string{`color "projection" of declared port`, "is not in port_color_compatibility.port_colors"}, }, } diff --git a/internal/operad/loader_color_source_t342_test.go b/internal/operad/loader_color_source_t342_test.go index 88b61c7..4a9af8a 100644 --- a/internal/operad/loader_color_source_t342_test.go +++ b/internal/operad/loader_color_source_t342_test.go @@ -21,8 +21,8 @@ import ( // (plus extra, when given) and the given own port_color_map. func sourceOntology(colorMap, extraWFs string) string { wfs := `[ - {"id": "WF03", "allowed_rewrites": ["LINK"], "src_port": "hosts", "tgt_port": "hosted-on"}, - {"id": "WF19", "allowed_rewrites": ["LINK"], "src_port": "opens-on", "tgt_port": "occupied-by"}` + extraWFs + ` + {"id": "WF03", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "hosts", "tgt_port": "hosted-on"}, + {"id": "WF19", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "opens-on", "tgt_port": "occupied-by"}` + extraWFs + ` ]` return colorSectionOntology(`{"matrix": {}, "port_color_map": `+colorMap+`}`, wfs) } @@ -86,7 +86,7 @@ func TestLoadRegistry_PartialOwnMapIsLegacyMerge_t342(t *testing.T) { {"three of four", `{"hosts": "topology", "hosted-on": "topology", "opens-on": "workflow"}`, "", "3", "4"}, {"complete except {semantic}", `{"hosts": "topology", "hosted-on": "topology", "opens-on": "workflow", "occupied-by": "workflow"}`, - `, {"id": "WF15", "allowed_rewrites": ["LINK"], "src_port": "{semantic}", "tgt_port": "{semantic}"}`, + `, {"id": "WF15", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "{semantic}", "tgt_port": "{semantic}"}`, "4", "5"}, } for _, c := range cases { @@ -180,8 +180,8 @@ func TestLoadRegistry_ColorGateReport_t342(t *testing.T) { logged := captureLog(t, func() { _, err = LoadRegistry(writeOntology(t, colorSectionOntology( `{"matrix": {"auth": {"auth": true, "topology": true}, "workflow": {"workflow": false}}}`, - `[{"id": "WF96", "allowed_rewrites": ["LINK"], "src_port": "governs", "tgt_port": "hosted-on"}, - {"id": "WF19", "allowed_rewrites": ["LINK"], "src_port": "opens-on", "tgt_port": "occupied-by"}]`))) + `[{"id": "WF96", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "governs", "tgt_port": "hosted-on"}, + {"id": "WF19", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "opens-on", "tgt_port": "occupied-by"}]`))) }) if err != nil { t.Fatalf("the color-gate report must never fail the load: %v", err) diff --git a/internal/operad/loader_test.go b/internal/operad/loader_test.go index 8b0218d..aa0b828 100644 --- a/internal/operad/loader_test.go +++ b/internal/operad/loader_test.go @@ -233,7 +233,7 @@ func TestLoadRegistry_UncoveredDeclaredPort_WarnsButBoots(t *testing.T) { "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"} + "src_types": ["*"], "tgt_types": ["*"], "src_port": "future-src", "tgt_port": "future-tgt"} ], "port_color_compatibility": {"matrix": {}} }`) diff --git a/internal/operad/occupancy.go b/internal/operad/occupancy.go index e3ec133..a7ca1fd 100644 --- a/internal/operad/occupancy.go +++ b/internal/operad/occupancy.go @@ -40,8 +40,8 @@ const superadminRoleURN graph.URN = "urn:moos:role:superadmin" // (ResolveSessionOccupant, RotateSessionOccupant) and tests all spell them // the same way. const ( - hasOccupantSrcPort = "has-occupant" - isOccupantOfTgtPort = "is-occupant-of" + hasOccupantSrcPort = "has-occupant" + isOccupantOfTgtPort = "is-occupant-of" ) // principalTypes enumerates the node type_ids that may act as a principal diff --git a/internal/operad/occupancy_test.go b/internal/operad/occupancy_test.go index 00c588c..96de30a 100644 --- a/internal/operad/occupancy_test.go +++ b/internal/operad/occupancy_test.go @@ -29,8 +29,9 @@ func TestPortColor_HasOccupant(t *testing.T) { // -------------------------------------------------------------------- // stateWithOccupancy builds a state with: -// session sam.hp-laptop --has-occupant--> user:sam -// (separately) session sam.idle (no has-occupant relation) +// +// session sam.hp-laptop --has-occupant--> user:sam +// (separately) session sam.idle (no has-occupant relation) func stateWithOccupancy() graph.GraphState { return graph.GraphState{ Nodes: map[graph.URN]graph.Node{ diff --git a/internal/operad/ports_generate_t342_test.go b/internal/operad/ports_generate_t342_test.go index 213c833..5720216 100644 --- a/internal/operad/ports_generate_t342_test.go +++ b/internal/operad/ports_generate_t342_test.go @@ -21,7 +21,7 @@ import ( // portsWFs exercises every generation rule, in declaration order: // // P1 WF03 hosts→hosted-on src [a] tgt [b, c] -// P2 WF07 participates→participated-by src [d] tgt [] (every type) +// P2 WF07 participates→participated-by src [d] tgt ["*"] (every type) // P3 WF07 anchors→anchor (additional) src [c] tgt ["*"] // — WF15 {semantic}→{semantic} placeholder, skipped // P5 WF19 pins-urn→pinned-by-session (additional only) src [a] tgt ["*"] @@ -29,7 +29,7 @@ import ( // P7 WF98 hosts→hosted-on src [c] tgt [a] (repeated names) const portsWFs = `[ {"id": "WF03", "allowed_rewrites": ["LINK"], "src_types": ["a"], "tgt_types": ["b", "c"], "src_port": "hosts", "tgt_port": "hosted-on"}, - {"id": "WF07", "allowed_rewrites": ["LINK"], "src_types": ["d"], "src_port": "participates", "tgt_port": "participated-by", + {"id": "WF07", "allowed_rewrites": ["LINK"], "src_types": ["d"], "tgt_types": ["*"], "src_port": "participates", "tgt_port": "participated-by", "additional_port_pairs": [{"src_port": "anchors", "tgt_port": "anchor", "src_types": ["c"], "tgt_types": ["*"]}]}, {"id": "WF15", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "{semantic}", "tgt_port": "{semantic}"}, {"id": "WF19", "allowed_rewrites": ["LINK"], diff --git a/internal/operad/registry.go b/internal/operad/registry.go index 0b4290c..039a733 100644 --- a/internal/operad/registry.go +++ b/internal/operad/registry.go @@ -116,8 +116,7 @@ type RewriteCategorySpec struct { // pins-urn, D19.4 filtered-by, D20.1 mounts-tool) for §M18-§M20 workspace shape. // // Loader consumes these into the registry so ValidateLINK can accept any -// declared pair for the WF. SrcTypes/TgtTypes, when non-empty, further restrict -// the pair to specific node types — empty means the WF's top-level lists apply. +// declared pair for the WF. For LINKs on this pair, SrcTypes/TgtTypes replace the WF-level lists (no fallback). ["*"] admits every type. LoadRegistry rejects empty, null, absent or mixed lists, so only a hand-built registry can carry an empty list, which admits every type. type AdditionalPortPair struct { SrcPort string TgtPort string diff --git a/internal/operad/validate.go b/internal/operad/validate.go index 00dac11..2e6e14f 100644 --- a/internal/operad/validate.go +++ b/internal/operad/validate.go @@ -313,12 +313,9 @@ func containsString(list []string, s string) bool { // 2. If the WF category declares non-empty src_types or tgt_types, the actual // node types must be in those lists. (Previously stubbed — now enforced.) // 3. If the LINK matches a declared AdditionalPortPair (not the primary pair), -// and that pair carries non-empty SrcTypes/TgtTypes restrictions, the actual -// node types must additionally satisfy the pair-level restriction. The -// ontology uses this to narrow an extension pair below the WF's overall -// src/tgt lists — e.g. WF19's has-occupant pair restricts to session→ -// (user|agent|group) even though WF19 itself accepts session|agent|agent_session -// as sources and a broader tgt_types list. (T=171 round 11 PR 1.) +// the pair's own SrcTypes/TgtTypes replace the WF-level lists. ["*"] +// admits every type. Empty lists are rejected at load, so only a hand-built +// registry can carry an empty list. func (r *Registry) ValidateStrataLink(env graph.Envelope, state graph.GraphState) error { srcNode, srcOk := state.Nodes[env.SrcURN] tgtNode, tgtOk := state.Nodes[env.TgtURN] @@ -363,11 +360,11 @@ func (r *Registry) ValidateStrataLink(env graph.Envelope, state graph.GraphState if !isAdditional { // Rule 2: WF-level src_types / tgt_types enforcement (primary-pair LINKs). - if len(wfSpec.SrcTypes) > 0 && !containsTypeID(wfSpec.SrcTypes, srcNode.TypeID) { + if len(wfSpec.SrcTypes) > 0 && !typeAllowed(wfSpec.SrcTypes, srcNode.TypeID) { return fmt.Errorf("operad: src type %q not in allowed list for %s: %v", srcNode.TypeID, env.RewriteCategory, wfSpec.SrcTypes) } - if len(wfSpec.TgtTypes) > 0 && !containsTypeID(wfSpec.TgtTypes, tgtNode.TypeID) { + if len(wfSpec.TgtTypes) > 0 && !typeAllowed(wfSpec.TgtTypes, tgtNode.TypeID) { return fmt.Errorf("operad: tgt type %q not in allowed list for %s: %v", tgtNode.TypeID, env.RewriteCategory, wfSpec.TgtTypes) } @@ -375,23 +372,22 @@ func (r *Registry) ValidateStrataLink(env graph.Envelope, state graph.GraphState } // Rule 3: pair-level src_types/tgt_types enforcement (additional-pair LINKs). - // A tgt_types entry of "*" is a wildcard per the ontology convention (see - // WF19.pins-urn in ontology.json v3.12): any tgt type passes. - if len(pair.SrcTypes) > 0 && !containsTypeID(pair.SrcTypes, srcNode.TypeID) { + // A src_types or tgt_types entry of "*" is a wildcard per the ontology convention (see + // WF19.pins-urn in ontology.json v3.12): any type passes. + if len(pair.SrcTypes) > 0 && !typeAllowed(pair.SrcTypes, srcNode.TypeID) { return fmt.Errorf("operad: src type %q not allowed on pair (%s, %s) of %s: %v", srcNode.TypeID, env.SrcPort, env.TgtPort, env.RewriteCategory, pair.SrcTypes) } - if len(pair.TgtTypes) > 0 && !containsTypeID(pair.TgtTypes, "*") && - !containsTypeID(pair.TgtTypes, tgtNode.TypeID) { + if len(pair.TgtTypes) > 0 && !typeAllowed(pair.TgtTypes, tgtNode.TypeID) { return fmt.Errorf("operad: tgt type %q not allowed on pair (%s, %s) of %s: %v", tgtNode.TypeID, env.SrcPort, env.TgtPort, env.RewriteCategory, pair.TgtTypes) } return nil } -func containsTypeID(list []graph.TypeID, id graph.TypeID) bool { - for _, v := range list { - if v == id { +func typeAllowed(types []graph.TypeID, ty graph.TypeID) bool { + for _, v := range types { + if v == "*" || v == ty { return true } } diff --git a/internal/operad/wildcard_acceptance_t342_test.go b/internal/operad/wildcard_acceptance_t342_test.go new file mode 100644 index 0000000..50ff584 --- /dev/null +++ b/internal/operad/wildcard_acceptance_t342_test.go @@ -0,0 +1,116 @@ +package operad + +import ( + "os" + "strings" + "testing" + + "moos/kernel/internal/graph" +) + +func wildcardTestRegistry() (*Registry, graph.GraphState) { + reg := EmptyRegistry() + st := graph.NewGraphState() + for _, ty := range []graph.TypeID{"user", "agent", "kernel", "program"} { + reg.NodeTypes[ty] = NodeTypeSpec{ID: ty, Stratum: "S2"} + u := graph.URN("urn:moos:" + string(ty) + ":t") + st.Nodes[u] = graph.Node{URN: u, TypeID: ty} + } + star := []graph.TypeID{"*"} + reg.RewriteCategories["WFA"] = RewriteCategorySpec{ID: "WFA", SrcPort: "a", TgtPort: "b", + SrcTypes: star, TgtTypes: star} + reg.RewriteCategories["WFB"] = RewriteCategorySpec{ID: "WFB", SrcPort: "a", TgtPort: "b", + SrcTypes: star, TgtTypes: []graph.TypeID{"program"}, + AdditionalPortPairs: []AdditionalPortPair{ + {SrcPort: "c", TgtPort: "d", SrcTypes: star, TgtTypes: []graph.TypeID{"program"}}, + }} + return reg, st +} + +func TestValidateStrataLink_WildcardEveryPosition(t *testing.T) { + reg, st := wildcardTestRegistry() + cases := []struct { + wf, sp, tp, src, tgt string + ok bool + }{ + {"WFA", "a", "b", "agent", "program", true}, + {"WFA", "a", "b", "user", "kernel", true}, + {"WFB", "a", "b", "agent", "program", true}, + {"WFB", "a", "b", "agent", "user", false}, + {"WFB", "c", "d", "kernel", "program", true}, + {"WFB", "c", "d", "kernel", "user", false}, + } + for _, c := range cases { + env := graph.Envelope{RewriteType: graph.LINK, RewriteCategory: graph.RewriteCategory(c.wf), + SrcURN: graph.URN("urn:moos:" + c.src + ":t"), SrcPort: c.sp, + TgtURN: graph.URN("urn:moos:" + c.tgt + ":t"), TgtPort: c.tp} + if err := reg.ValidateStrataLink(env, st); (err == nil) != c.ok { + t.Errorf("%s (%s,%s) %s->%s: want ok=%v, got %v", c.wf, c.sp, c.tp, c.src, c.tgt, c.ok, err) + } + } +} + +func TestLoadRegistry_RejectsEmptyOrMixedTypeLists(t *testing.T) { + const head = `{"version":"t","types":{"s2_infrastructure":[{"id":"user","stratum":"S2"}]},"rewrite_categories":[` + const tail = `],"port_color_compatibility":{"matrix":{}}}` + bad := map[string]string{ + "wf src []": `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":[],"tgt_types":["user"]}`, + "wf tgt null": `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":["user"],"tgt_types":null}`, + "wf tgt absent": `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":["user"]}`, + "pair tgt []": `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":["user"],"tgt_types":["user"], + "additional_port_pairs":[{"src_port":"c","tgt_port":"d","src_types":["user"],"tgt_types":[]}]}`, + "mixed *": `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":["*","user"],"tgt_types":["user"]}`, + } + for name, wf := range bad { + _, err := LoadRegistry(writeOntology(t, head+wf+tail)) + if err == nil { + t.Errorf("%s: expected load error", name) + continue + } + if !strings.Contains(err.Error(), "WFZ") || !strings.Contains(err.Error(), "_types") { + t.Errorf("%s: error should name the WF and the key; got %q", name, err) + } + } + good := `{"id":"WFZ","allowed_rewrites":["LINK"],"src_port":"a","tgt_port":"b","src_types":["*"],"tgt_types":["user"]}` + if _, err := LoadRegistry(writeOntology(t, head+good+tail)); err != nil { + t.Errorf(`["*"] must load: %v`, err) + } +} + +func TestIntegration_WF15WildcardAdmitsParseableTypePairs(t *testing.T) { + if os.Getenv("MOOS_INTEGRATION") != "1" { + t.Skip("MOOS_INTEGRATION != 1") + } + reg, err := LoadRegistry(integrationOntologyPath(t)) + if err != nil { + t.Fatal(err) + } + st := graph.NewGraphState() + var ids []graph.TypeID + for id, spec := range reg.NodeTypes { + if _, err := graph.ParseStratum(spec.Stratum); err != nil { + continue // stratum early return: separate issue + } + ids = append(ids, id) + u := graph.URN("urn:moos:t:" + string(id)) + st.Nodes[u] = graph.Node{URN: u, TypeID: id} + } + rejected := 0 + for _, s := range ids { + for _, g := range ids { + env := graph.Envelope{RewriteType: graph.LINK, RewriteCategory: graph.WF15, + SrcURN: graph.URN("urn:moos:t:" + string(s)), SrcPort: "{semantic}", + TgtURN: graph.URN("urn:moos:t:" + string(g)), TgtPort: "{semantic}", + ContractURN: "urn:moos:contract:test.v1"} + if err := reg.ValidateLINK(env); err != nil { + t.Fatalf("ValidateLINK %s->%s: %v", s, g, err) + } + if err := reg.ValidateStrataLink(env, st); err != nil { + rejected++ + } + } + } + if rejected > 0 { + t.Errorf("WF15 rejected %d of %d parseable type pairs", rejected, len(ids)*len(ids)) + } +} diff --git a/internal/reactive/engine_test.go b/internal/reactive/engine_test.go index 6af18bd..465f695 100644 --- a/internal/reactive/engine_test.go +++ b/internal/reactive/engine_test.go @@ -41,11 +41,11 @@ func TestEvaluate_WatchReactGuard(t *testing.T) { TypeID: "watcher", Properties: map[string]graph.Property{ "name": {Value: "raw-ki-claim-extract", Mutability: "immutable"}, - "match_rewrite_type": {Value: "MUTATE", Mutability: "mutable"}, - "match_type_id": {Value: "knowledge_item", Mutability: "mutable"}, - "match_urn_prefix": {Value: "", Mutability: "mutable"}, - "match_port": {Value: "", Mutability: "mutable"}, - "status": {Value: "active", Mutability: "mutable"}, + "match_rewrite_type": {Value: "MUTATE", Mutability: "mutable"}, + "match_type_id": {Value: "knowledge_item", Mutability: "mutable"}, + "match_urn_prefix": {Value: "", Mutability: "mutable"}, + "match_port": {Value: "", Mutability: "mutable"}, + "status": {Value: "active", Mutability: "mutable"}, }, CreatedAt: now, }, diff --git a/internal/reactive/predicate_test.go b/internal/reactive/predicate_test.go index ad13dec..c417ccf 100644 --- a/internal/reactive/predicate_test.go +++ b/internal/reactive/predicate_test.go @@ -394,9 +394,9 @@ func TestThookPredicate_JSONNumberCoercion(t *testing.T) { // Predicate stored as float64 (simulating JSON round-trip). pred := map[string]any{ - "kind": "after_urn", - "urn": "urn:moos:external_op:sam.test", - "prop": "deadline_t", + "kind": "after_urn", + "urn": "urn:moos:external_op:sam.test", + "prop": "deadline_t", "value": float64(187), } diff --git a/internal/transport/auth_test.go b/internal/transport/auth_test.go index d071403..05e3eb5 100644 --- a/internal/transport/auth_test.go +++ b/internal/transport/auth_test.go @@ -14,7 +14,7 @@ func TestValidBearer(t *testing.T) { }{ {"Bearer s3cret", "s3cret", true}, {"bearer s3cret", "s3cret", true}, // scheme is case-insensitive - {"Bearer s3cret ", "s3cret", true}, // surrounding whitespace trimmed + {"Bearer s3cret ", "s3cret", true}, // surrounding whitespace trimmed {"Bearer wrong", "s3cret", false}, {"s3cret", "s3cret", false}, // missing scheme {"", "s3cret", false}, diff --git a/internal/transport/server_port_colors_test.go b/internal/transport/server_port_colors_test.go index c9aab42..af2840b 100644 --- a/internal/transport/server_port_colors_test.go +++ b/internal/transport/server_port_colors_test.go @@ -20,7 +20,7 @@ func TestHandleGetPortColors_t342(t *testing.T) { {"self-colored", `{ "version": "t342-transport", "types": {"s2_infrastructure": [], "s1_grammar": [], "interaction_nodes": []}, - "rewrite_categories": [{"id": "WF08", "allowed_rewrites": ["LINK"], "src_port": "bound-to", "tgt_port": "binds"}], + "rewrite_categories": [{"id": "WF08", "allowed_rewrites": ["LINK"], "src_types": ["*"], "tgt_types": ["*"], "src_port": "bound-to", "tgt_port": "binds"}], "port_color_compatibility": {"matrix": {"topology": {"topology": true}}, "port_color_map": {"bound-to": "topology", "binds": "topology"}} }`, operad.PortColorSourceOntology}, }