feat(operad+kernel): WF21 causes ValidateCausalAcyclic + round-15 ceremony#34
Conversation
…ook (round-15 v314-3) Round-15 ceremony validator-extension companion to v314-3-wf21-causes grammar_fragment promotion (Z440 :8000 log_seq 388, status=proposed). Adds: - ValidateCausalAcyclic(env, state) on Registry — BFS forward from tgt through outgoing WF21 causes-edges; rejects if env.SrcURN reachable. Handles non-WF21 LINKs (pass-through), non-LINK rewrites (pass-through), self-LINK 1-cycle, direct 2-cycles, transitive N-cycles, DAG forks (pass), and reverse-direction caused-by edges (ignored — only true causes-direction edges count). - runtime.go hooks both Apply (single-envelope path) and ApplyProgram (atomic-batch path; uses workingState so intra-batch causes-LINKs cannot close cycles even when individual envelopes pass against pre-batch state). - 8 unit tests covering all branches. Doctrine anchors: - kb/research/spec/07-time-fabric.md §7.3.5 (WF21 lift path) - kb/research/spec/05-external-substrates.md §5.1.5 + §5.3.5 (transitional citation surface → caused-by LINK lift) Pairs with v314-2-clock-type + v314-4-substrate-property + v314-6- channel-kind-video promotions; ontology.json bump v3.14.0 → v3.15.0 holds on ffs0 wolfram/r15-ontology-v3-15-bump branch pending this PR's merge + 5-kernel rebuild. All operad tests pass; full suite green.
There was a problem hiding this comment.
Pull request overview
Extends operad/kernel validation to support the new WF21 causes/caused-by rewrite category by enforcing acyclicity for WF21 LINK rewrites, and integrates the check into both single-envelope and batch apply paths.
Changes:
- Added
Registry.ValidateCausalAcyclicimplementing a forward BFS to reject WF21 LINKs that would close a causes-cycle. - Hooked WF21 acyclicity validation into
Runtime.applyWithOptionsandRuntime.ApplyProgram(working-state-aware for intra-batch detection). - Added unit tests covering pass-through cases and cycle detection scenarios for WF21 causes edges.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
| internal/operad/validate.go | Adds WF21 acyclicity validator using BFS over existing causes edges |
| internal/kernel/runtime.go | Calls new validator during LINK validation in both Apply and ApplyProgram |
| internal/operad/validate_causal_acyclic_test.go | Adds unit tests for WF21 acyclicity behavior |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| state.Relations[relURN] = graph.Relation{ | ||
| URN: relURN, | ||
| RewriteCategory: graph.RewriteCategory("WF21"), | ||
| SrcURN: e[0], |
There was a problem hiding this comment.
This file repeatedly uses graph.RewriteCategory("WF21") literals. To match existing usage of typed WF constants (graph.WF18/WF19/etc.) and reduce typo risk, consider adding graph.WF21 and using it throughout these tests and the validator.
| for relURN := range state.RelationsBySrc[cur] { | ||
| rel, ok := state.Relations[relURN] | ||
| if !ok { | ||
| continue | ||
| } |
There was a problem hiding this comment.
ValidateCausalAcyclic iterates via state.RelationsBySrc[cur] directly. Per GraphState docs, indexes can be nil/uninitialized (e.g., state loaded from JSON without Rebuild), and read paths are expected to tolerate that. As written, a nil RelationsBySrc will make the BFS see no outgoing edges and may incorrectly allow a cycle. Prefer using the GraphState accessor (RelationsFrom) or falling back to scanning state.Relations when the index is nil.
| for len(queue) > 0 { | ||
| cur := queue[0] | ||
| queue = queue[1:] |
There was a problem hiding this comment.
The BFS queue uses queue = queue[1:], which repeatedly reslices and can retain the underlying array, increasing allocations/GC for large traversals. Consider an index-based queue (head integer) or a simple ring buffer pattern to keep this validation cheaper on large graphs.
| for len(queue) > 0 { | |
| cur := queue[0] | |
| queue = queue[1:] | |
| head := 0 | |
| for head < len(queue) { | |
| cur := queue[head] | |
| head++ |
| if env.RewriteCategory != graph.RewriteCategory("WF21") { | ||
| return nil | ||
| } |
There was a problem hiding this comment.
This introduces a raw rewrite-category literal graph.RewriteCategory("WF21"). Elsewhere the codebase consistently uses typed constants (e.g., graph.WF15/WF19) to avoid typos and keep refactors centralized. Consider adding graph.WF21 in internal/graph/relation.go and using that constant here (and in tests).
| // rewrites. Called with the kernel read-lock held (state is consistent), in | ||
| // the same apply path as ValidateStrataLink. |
There was a problem hiding this comment.
The doc comment says this is called with the kernel read-lock held, but the current apply paths call registry LINK validations under rt.mu.Lock() (write-lock). To avoid misleading future callers, consider rewording to something like “called with the kernel lock held / state is consistent” rather than specifying a read-lock.
| // rewrites. Called with the kernel read-lock held (state is consistent), in | |
| // the same apply path as ValidateStrataLink. | |
| // rewrites. Called with the kernel lock held (state is consistent), in the | |
| // same apply path as ValidateStrataLink. |
| func buildAcyclicTestState(edges [][2]graph.URN) graph.GraphState { | ||
| state := graph.NewGraphState() | ||
| urnSet := map[graph.URN]struct{}{} | ||
| for _, e := range edges { | ||
| urnSet[e[0]] = struct{}{} | ||
| urnSet[e[1]] = struct{}{} | ||
| } | ||
| for urn := range urnSet { | ||
| state.Nodes[urn] = graph.Node{ | ||
| URN: urn, | ||
| TypeID: "derivation", | ||
| } | ||
| graph.IndexAddNodeByType(state.NodesByType, urn, "derivation") | ||
| } | ||
| for i, e := range edges { | ||
| relURN := graph.URN("urn:moos:rel:test." + string(e[0]) + ".causes." + string(e[1]) + "." + string(rune('a'+i))) | ||
| state.Relations[relURN] = graph.Relation{ | ||
| URN: relURN, | ||
| RewriteCategory: graph.RewriteCategory("WF21"), | ||
| SrcURN: e[0], | ||
| SrcPort: "causes", | ||
| TgtURN: e[1], | ||
| TgtPort: "caused-by", | ||
| CreatedAt: time.Now(), | ||
| } | ||
| graph.IndexAddRelationEndpoints(state.RelationsBySrc, state.RelationsByTgt, relURN, e[0], e[1]) | ||
| } | ||
| return state |
There was a problem hiding this comment.
The tests only exercise states built via graph.NewGraphState (indexes initialized). Since GraphState indexes can be nil after JSON load (until Rebuild), it would be good to add a test case where Relations is populated but RelationsBySrc is nil, to ensure ValidateCausalAcyclic still detects cycles (or to enforce the intended fallback behavior).
Round-15 ceremony validator extension
Companion to ffs0 round-15 v3.14 → v3.15 ontology bump (held on
wolfram/r15-ontology-v3-15-bump). Promotes 4 grammar_fragments through WF20 ceremony:v314-2-clock-typeclockS2 node-type with cardinality/embedding/frame/density/cycle_periodv314-3-wf21-causesValidateCausalAcyclic(this PR)v314-4-substrate-propertysubstrateenum +substrate_anchor_urnon channel + knowledge_itemv314-6-channel-kind-videochannel.kindenum extended withvideo+audioOnly WF21 needs new code; the other three are data-driven via the loader.
ValidateCausalAcyclic
When a new LINK
src --causes--> tgtis proposed, walk forward fromtgtthrough existing WF21 causes-edges. Ifsrcis reached, reject — the new edge would close a cycle.Rules:
src == tgt) is a 1-cycle — rejected immediately.tgtthrough outgoing edges whereRewriteCategory=WF21andSrcPort="causes"(true causes-direction; reversecaused-byside ignored to avoid over-detection).Hooked into both
runtime.Apply(single-envelope path) andruntime.ApplyProgram(atomic-batch path; usesworkingStateso intra-batch causes-LINKs cannot close cycles even when individual envelopes pass against pre-batch state).Doctrine anchors
kb/research/spec/07-time-fabric.md§7.3.5 — WF21 lift pathkb/research/spec/05-external-substrates.md§5.1.5 + §5.3.5 — transitional citation surface → caused-by LINK liftclaim:wolfram.cross-kernel-reciprocity-as-m9-prefiguration(Z440 :8000 log_seq 385) — first pre-§M9 instance of HG-resident cross-kernel coherenceTests
8 unit tests in
internal/operad/validate_causal_acyclic_test.go:TestValidateCausalAcyclic_NonWF21LinkPasses— pass-through for other LINK categoriesTestValidateCausalAcyclic_NonLinkPasses— pass-through for ADD/MUTATE/UNLINKTestValidateCausalAcyclic_SelfLinkRejected— 1-cycleTestValidateCausalAcyclic_NoPathPasses— disjoint subgraphsTestValidateCausalAcyclic_DirectCycleRejected— 2-cycleTestValidateCausalAcyclic_TransitiveCycleRejected— 4-cycle through B→C→D→ATestValidateCausalAcyclic_DAGForkPasses— fork (B→C, B→D), new edge A→B passesTestValidateCausalAcyclic_NonCausesPortIgnored— defensive — only true causes-direction edges followedAll passing locally. Full
go test ./...green.Round-15 deploy sequence
go build)wolfram/r15-ontology-v3-15-bump→main(ontology to v3.15.0)/operad/node-typesreportsclock;/operad/rewrite-categoriesreportsWF21;channel.kindenum includesvideo+audiogrammar_fragment:v314-{2,3,4,6}proposed → promoted (atomic batch)caused-byLINKs + N derivationconsumes/produceslifts fromstochastic_weights.anchors[]Round-15 vehicle: ffs0#42