ancestors_of in src/codealmanac/services/topics/graph.py increments depth once per node popped rather than per level, so while frontier and depth < 32 caps total iterations instead of depth. the walk gives up after 32 nodes and returns an incomplete ancestor set, so reject_cycle reports clean and the cycle gets written.
repro on a fresh wiki (v0.4.7, python 3.12.13):
# chain of 33 topics: n0 -> n1 -> ... -> n32
$ codealmanac topics link n32 n0
codealmanac: adding n0 as parent of n32 would create a cycle # exit 1
# same shape, one more topic: n0 -> ... -> n33
$ codealmanac topics link n33 n0
linked n33 -> n0 # exit 0
the cycle is persisted to topics.yaml, survives reindex, and is never reported by health or validate, so it is permanent once created. topics create --parent has the same hole.
it also fires well below 32 levels. frontier is seeded from a set, so on a graph only 2 levels deep but 41 nodes wide the same command flips between accept and reject depending on PYTHONHASHSEED (2 of 10 seeds missed it).
this breaks the documented invariant that topics form a dag (docs/concepts.md). reject_cycle is the only cycle guard in the codebase.
note that tests/test_architecture.py asserts "depth < 32" in graph_text, so a fix has to update that assertion as well.
ancestors_ofinsrc/codealmanac/services/topics/graph.pyincrementsdepthonce per node popped rather than per level, sowhile frontier and depth < 32caps total iterations instead of depth. the walk gives up after 32 nodes and returns an incomplete ancestor set, soreject_cyclereports clean and the cycle gets written.repro on a fresh wiki (v0.4.7, python 3.12.13):
the cycle is persisted to
topics.yaml, survivesreindex, and is never reported byhealthorvalidate, so it is permanent once created.topics create --parenthas the same hole.it also fires well below 32 levels.
frontieris seeded from aset, so on a graph only 2 levels deep but 41 nodes wide the same command flips between accept and reject depending onPYTHONHASHSEED(2 of 10 seeds missed it).this breaks the documented invariant that topics form a dag (
docs/concepts.md).reject_cycleis the only cycle guard in the codebase.note that
tests/test_architecture.pyasserts"depth < 32" in graph_text, so a fix has to update that assertion as well.