From 337a4abac17b5b388198ac874f83c344e76480ab Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 13:23:00 +0100 Subject: [PATCH 1/3] =?UTF-8?q?ci:=20Julia=201.12=20=E2=80=94=20Pkg=201.10?= =?UTF-8?q?=20ignores=20[sources],=20breaking=20sibling-path=20deps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The krl-tests job installed Julia 1.10, but server/Project.toml resolves its unregistered siblings (KnotTheory/Skein/AcceleratorGate) through a [sources] table — a Pkg 1.11+ feature that 1.10 silently ignores, so Pkg.instantiate fell back to a registry lookup and failed with 'expected package KnotTheory [215268c9] to be registered' (observed on this PR's first CI run). Local verification ran on 1.12.6; pin CI to the 1.12 channel to match. Co-Authored-By: Claude Fable 5 --- .github/workflows/krl-verification.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/krl-verification.yml b/.github/workflows/krl-verification.yml index 64098c4..1c46128 100644 --- a/.github/workflows/krl-verification.yml +++ b/.github/workflows/krl-verification.yml @@ -38,11 +38,14 @@ jobs: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4 - - name: Install Julia 1.10 + # Julia >= 1.11 is required: server/Project.toml uses a [sources] table + # (path-resolved sibling deps), which Pkg 1.10 silently ignores — the + # instantiate then fails with "expected package … to be registered". + - name: Install Julia 1.12 run: | set -euo pipefail curl -fsSL https://install.julialang.org -o "$RUNNER_TEMP/juliaup-init.sh" - sh "$RUNNER_TEMP/juliaup-init.sh" --yes --default-channel 1.10 + sh "$RUNNER_TEMP/juliaup-init.sh" --yes --default-channel 1.12 echo "$HOME/.juliaup/bin" >> "$GITHUB_PATH" - name: Julia version From a1b798214e0c3df16f95706e4ce7134d01417a58 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:02:53 +0100 Subject: [PATCH 2/3] fix(semantic): make canonicalize_presentation idempotent via orbit-cycle minimum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The old single relabelling pass was not idempotent: BFS relabelling from the canonical-pass ordering can itself change the ordering, so blob(canonicalize(p)) != blob(canonicalize(canonicalize(p))) — measured at 95/200 seeded BR-5 trials once the upstream KnotTheory.jl PD-integrity bugs (braid strand-swap, r2_simplify re-splice) were fixed and the harness could see it. Fix: iterate the pass, detecting the inevitable cycle of serialised states, and return the lexicographically minimal state on the cycle. That choice is a fixed point (canonicalising it lands back on it) and is reached from every presentation in the same orbit. Where the old pass was already stable — every presentation in the shipped test corpus — the result is unchanged, so stored fingerprints remain valid. Also factors _presentation_serial out of canonical_presentation_blob so the cycle detector and the blob share one serialisation. Validated: BR-5 fuzz, 200 trials against fixed KnotTheory.jl — 8752 assertions pass, 0 hard failures, 1 expected Broken (11/200 quandle_key residual, upstream KnotTheory.jl#42). Co-Authored-By: Claude Fable 5 --- server/quandle_semantic.jl | 55 ++++++++++++++++++++++++++++++++------ 1 file changed, 47 insertions(+), 8 deletions(-) diff --git a/server/quandle_semantic.jl b/server/quandle_semantic.jl index daaf6a9..f0a779b 100644 --- a/server/quandle_semantic.jl +++ b/server/quandle_semantic.jl @@ -142,7 +142,7 @@ Two presentations produce the same canonical form iff they represent the same presentation up to generator renaming. This is the basis for `canonical_presentation_blob`'s fingerprint. """ -function canonicalize_presentation(p::QuandlePresentation)::QuandlePresentation +function _canonicalize_pass(p::QuandlePresentation)::QuandlePresentation sorted_rel = sort(p.relations, by = r -> (r.lhs, r.rhs, r.out, r.is_inverse ? 1 : 0)) mapping = Dict{Int, Int}() @@ -172,6 +172,51 @@ function canonicalize_presentation(p::QuandlePresentation)::QuandlePresentation QuandlePresentation(p.generator_count, canon_rel) end +""" + canonicalize_presentation(p::QuandlePresentation) -> QuandlePresentation + +Idempotent canonicalisation. A single relabel-and-resort pass is not +idempotent: it relabels by first appearance in the *old* ordering and then +re-sorts under the *new* labels, so a second pass can see a different order +and relabel again. Iterating the pass from any start eventually enters a +cycle (finite deterministic orbit); every member of that cycle reaches the +same cycle again, so returning the cycle's lexicographically-minimal +serialisation is a true fixpoint: canonicalize(canonicalize(p)) == +canonicalize(p). Where the single pass was already stable (the common case, +and everything previously stored) the result is unchanged. +""" +function canonicalize_presentation(p::QuandlePresentation)::QuandlePresentation + order = String[] + states = Dict{String, QuandlePresentation}() + cur = p + while true + cur = _canonicalize_pass(cur) + s = _presentation_serial(cur) + if haskey(states, s) + i = findfirst(==(s), order) + cycle = order[i:end] + return states[minimum(cycle)] + end + push!(order, s) + states[s] = cur + end +end + +""" + _presentation_serial(c::QuandlePresentation) -> String + +Serialise a presentation as `qpres-v1|g=|r=lhs,rhs,out,sign;...` without +hashing. `canonical_presentation_blob` hashes this for canonical forms. +""" +function _presentation_serial(c::QuandlePresentation)::String + rel_tokens = String[] + for r in c.relations + sign_flag = r.is_inverse ? -1 : 1 + push!(rel_tokens, string(r.lhs, ",", r.rhs, ",", r.out, ",", sign_flag)) + end + string("qpres-v1|g=", c.generator_count, "|r=", join(rel_tokens, ";")) +end + """ canonical_presentation_blob(p::QuandlePresentation) -> String @@ -184,13 +229,7 @@ generator renaming). This blob is the input to SHA-256 fingerprinting in `quandle_descriptor`. """ function canonical_presentation_blob(p::QuandlePresentation)::String - c = canonicalize_presentation(p) - rel_tokens = String[] - for r in c.relations - sign_flag = r.is_inverse ? -1 : 1 - push!(rel_tokens, string(r.lhs, ",", r.rhs, ",", r.out, ",", sign_flag)) - end - blob = string("qpres-v1|g=", c.generator_count, "|r=", join(rel_tokens, ";")) + blob = _presentation_serial(canonicalize_presentation(p)) # Compute BLAKE3 hash of the blob ctx = Blake3Hash.Blake3Ctx() Blake3Hash.update!(ctx, Vector{UInt8}(blob)) From 27b704b86651cb6a143e191276fe0badf79f57d3 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Tue, 21 Jul 2026 14:07:06 +0100 Subject: [PATCH 3/3] test: drop R2 c5 @test_broken marker (after KnotTheory.jl#43) (#77) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Removes the `@test_broken` marker on the R2-invariance 5-colouring assertion in `server/test_quandle_axioms.jl`, restoring the plain hard `@test`. The marker tracked an upstream defect: `r2_simplify` in KnotTheory.jl removed a bigon without re-splicing the severed arcs, so the trefoil's extracted presentation gained 2 phantom generators and its 5-colouring count came out 25 instead of 5. That is fixed in **hyperpolymath/KnotTheory.jl#43** (union-find arc re-splice + the braid strand-swap fix underneath it). ## Merge order — do not merge early 1. quandledb #76 (CI Julia 1.12 floor + idempotent canonicalisation) 2. **KnotTheory.jl#43** ← this PR's CI checks out that repo's *default branch*, so until #43 merges the restored hard assertion fails here (red is expected and correct) 3. this PR The `@test_broken` mechanism makes step 3 mandatory rather than optional: once #43 merges, the marker reports "unexpected pass" — a hard error — in #76's follow-up CI runs, and this PR is the remedy. ## Verification With the #43 fix applied locally (Julia 1.12.6, 200-trial BR-5 corpus): the axioms suite passes fully with the hard assertion restored; the only remaining Broken mark in the repo is the BR-5 `quandle_key` order-sensitivity counter (11/200 residual, upstream KnotTheory.jl#42 — polynomial unit normalisation, deliberately kept). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Fable 5 --- server/test_quandle_axioms.jl | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/server/test_quandle_axioms.jl b/server/test_quandle_axioms.jl index 97535d0..cafc31b 100644 --- a/server/test_quandle_axioms.jl +++ b/server/test_quandle_axioms.jl @@ -209,12 +209,7 @@ end d_canonical = quandle_descriptor(trefoil_canonical) @test d_simplified.colouring_count_3 == d_canonical.colouring_count_3 - # KNOWN-BROKEN (upstream): KnotTheory.jl r2_simplify removes the bigon - # without re-splicing the severed arcs, leaving 4 arc labels that occur - # only once; the extracted presentation then has 5 generators instead - # of 3 and c5 comes out 25 instead of 5. Flips to "unexpected pass" - # (forcing removal of this marker) once the upstream fix lands. - @test_broken d_simplified.colouring_count_5 == d_canonical.colouring_count_5 + @test d_simplified.colouring_count_5 == d_canonical.colouring_count_5 end end