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
20 changes: 10 additions & 10 deletions internal/fold/errors.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,22 +6,22 @@ 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")
ErrFieldNotInScope = errors.New("field not in MUTATE scope for this rewrite category")
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")
)
6 changes: 3 additions & 3 deletions internal/graph/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
6 changes: 3 additions & 3 deletions internal/graph/state.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion internal/hdc/crosswalk.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions internal/hdc/fiber.go
Original file line number Diff line number Diff line change
Expand Up @@ -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])
Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion internal/kernel/liveness_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
}
Expand Down
6 changes: 3 additions & 3 deletions internal/kernel/sweep.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions internal/mcp/protocol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}

Expand Down
22 changes: 13 additions & 9 deletions internal/operad/color_equality_t342_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
}
Comment thread
Copilot marked this conversation as resolved.
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,
Expand Down
33 changes: 33 additions & 0 deletions internal/operad/loader.go
Original file line number Diff line number Diff line change
Expand Up @@ -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))
}
Expand Down Expand Up @@ -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)
}
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion internal/operad/loader_closure_t342_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"},
},
}
Expand Down
10 changes: 5 additions & 5 deletions internal/operad/loader_color_source_t342_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion internal/operad/loader_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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": {}}
}`)
Expand Down
4 changes: 2 additions & 2 deletions internal/operad/occupancy.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions internal/operad/occupancy_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down
4 changes: 2 additions & 2 deletions internal/operad/ports_generate_t342_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ 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 ["*"]
// P6 WF04 contains→contained-in src [a, b] tgt [b]
// 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"],
Expand Down
3 changes: 1 addition & 2 deletions internal/operad/registry.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
28 changes: 12 additions & 16 deletions internal/operad/validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -363,35 +360,34 @@ 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)
}
return nil
}

// 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
}
}
Expand Down
Loading
Loading