Conversation
When one source note linked to a target several times, the backlinks UI sorted those individual context cards lexicographically by their surrounding block content (the derived `Ord [B.Block]` tie-breaker on `Rel`) instead of by source-file order, and two links sharing an identical context were collapsed into one card by `IxSet.fromList`'s set-dedup. Add a `_relSrcPos :: Int` field on `Rel` between `_relTo` and `_relCtx`, populated in `noteRels` as the pandoc-traversal index. The derived `Ord` now breaks ties between rels sharing `(_relFrom, _relTo)` by source position, so source order survives `IxSet.toList` and the `groupNE` aggregation in `modelLookupBacklinks` — and same-context multi-links no longer collide on equality. Two regression tests cover the pair of guarantees: lex-vs-source order disagreement, and identical-context dedup.
…variant Original wording — "Pandoc-traversal index of this link in the source note" — overstated what the field actually guarantees. The flattening in noteRels visits URL keys in Map order (alphabetical), not pandoc traversal order; only within a single (relFrom, relTo) bucket does the index reflect the order in which queryLinksWithContext yielded the contexts. Narrow the haddock so a future reader cannot infer document-wide source position from this field.
The field exists to drive backlink-card render order in modelLookupBacklinks; it carries no cross-note graph-semantics meaning. Document the boundary so future readers know other Rel consumers (Resolve, Export.JSON, Graph.folgezettel*) intentionally do not depend on it and so the field is not mistaken for part of the relation's identity.
The Ord precedence (relFrom > relTo > relSrcPos > relCtx) is implied purely by field declaration order, with no type-level enforcement. A future field reorder for readability would silently break the backlinks-source-order invariant — RelSpec catches this, but a comment makes the intent legible at the site of the trap rather than only in the test that closes it.
…oteRels The fix's correctness for issue #186 hinges on the NonEmpty inside queryLinksWithContext's per-URL value carrying instances in pandoc traversal order. That assumption is upstream behavior — make it explicit at the call site so a future maintainer who suspects the ordering bug has resurfaced reads what we depend on without spelunking the pandoc-link-context source.
…istWith reversal The initial issue-#186 fix assumed queryLinksWithContext's per-URL NonEmpty was in source-traversal order. It is not. The library accumulates entries via Map.fromListWith (<>), whose combining function is applied as f new old, so each later-traversed entry is prepended onto the accumulator — yielding the per-URL NonEmpty in reverse traversal order. zipWith [0 ..] over that list assigned srcPos to later-source links first, so Ix.toList returned them out of order. The new RelSpec test for source order caught it on the first run (expected ["Z first: ", "A second: "], got ["A second: ", "Z first: "]). Reverse the per-URL instances list before zipping so srcPos counts up in source order.
…llbacks The test's hand-rolled paragraph-text extractor had a "??" sentinel fallback that would have turned a structural-shape regression into a silent string-equality mismatch. Switch to the project's existing 'WL.plainify :: [B.Inline] -> Text' helper (used 6+ places in src, e.g. Note.hs:489) and 'error' on unexpected ctx shape, and tighten barRoute's failure message so a future fixture-invariant break names itself. The new assertion also reads the link label, so a regression that preserves prefix text but corrupts the wikilink itself would still fail.
…Rels The hickey/lowy refinement commits added a 6-line NOTE block above 'data Rel', a 17-line haddock on '_relSrcPos', and a 16-line inline comment above 'extractLinks' — three places explaining variations of the same "Ord field-position breaks ties for issue #186" story, each re-deriving the across-URL alphabetical caveat. Trim them so each location says one thing once: the field-order invariant lives above 'data Rel' (3 lines), the field's contract lives in its haddock (8 lines), and the 'Map.fromListWith' reversal explanation lives at the implementation site that needs it (5 lines). Net: ~28 lines removed, no information lost.
The 'zipWith [0 ..]' lazy-list source means each rel currently carries a thunk for its position field. Adding a bang annotation forces the Int at construction time, dropping one thunk per rel from the global 'IxSet RelIxs Rel'. No call site reads '_relSrcPos' lazily — it only participates in derived 'Ord' comparisons, which always force the field anyway.
Hickey/Lowy Analysis
Hickey rationaleThe diff was reviewed at three angles: complecting, fragility, and token-vs-data. Complecting (no finding). Fragility (Finding 2 fixed). The derived Token vs data (Finding 3 deferred). Hidden dependency (Finding 1 + 4 fixed). The original haddock claimed "Pandoc-traversal index of this link in the source note" — implying document-wide source position. The actual invariant is narrower: within rels sharing Lowy rationaleVolatility-based decomposition asks: which axes of change does this fix encapsulate, and at which boundary? Volatility map. Two independent axes are at play: cross-note graph semantics (a relation is a triple Why the storage-layer site is acceptable here (Finding 8 deferred). The architectural alternative — extracting Encapsulation hardening (Finding 5 + 6). Two documentation gaps: nothing said the field was a presentation-layer artifact (so a future reader might mistake it for graph state and propagate it through Resolve / Export.JSON), and nothing said the dedup opt-out was intentional (so a future reader might "tighten" Reuse (Finding 7 deferred). The |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 0s | git fetch ok; forge=github; noGit=false |
| research | ✓ | 9m 24s | Bug isolated to Rel's derived Ord tie-breaking on Ord [B.Block]. |
| branch | ✓ | 14s | On feature branch fit-belt (worktree), up-to-date with origin/master. |
| implement | ✓ | 1m 38s | Added _relSrcPos :: Int; updated noteRels with zipWith [0 ..]; two new RelSpec tests. |
| check | ✓ | 52s | cabal build all clean (60 modules + executable). |
| docs | ✓ | 23s | CHANGELOG entry under 1.6.0.0 (Unreleased) → Bug fixes. |
| fmt | ✓ | 26s | just fmt clean (cabal-fmt, fourmolu, hlint, nixpkgs-fmt). |
| commit | ✓ | 18s | Primary commit 01e85710 pushed; upstream set. |
| hickey+lowy | ✓ | 9m 9s | 4 fix-in-PR commits + 3 deferred issues (#689, #690, #691). |
| police | ✓ | 13m 2s | Rules clean; fact-check caught Map.fromListWith reversal bug; elegance landed 3 commits via /simplify. |
| test | ✓ | 23s | cabal test all 49/49 pass. |
| create-pr | ✓ | 2m 3s | Draft PR #692 + Hickey/Lowy ledger comment. |
| ci | ✓ | 2m 16s | vira ci signed off both arches; e2e-{live,static,morph} green at HEAD. |
| evidence | ✓ | 3m 26s | Before/after screenshots posted under ## Evidence. |
| Total | 43m 49s |
Slowest step: police (13m 2s, ~30% of total).
Optimization suggestions
policewas the slowest because the fact-check pass caught a real bug (theMap.fromListWith (<>)reversal) requiring an extra commit + test re-run, and elegance ran three sub-fix cycles each with their own fmt + test. That is the right outcome — the bug would have shipped otherwise — so the time was load-bearing, not waste.research(9m 24s) andhickey+lowy(9m 9s) are sub-agent-driven and dominate afterpolice. For follow-up commits to this PR,--from polishor--from ci-onlyskips both.- The Map.fromListWith reversal is non-obvious enough that a one-line property-style test ("noteRels indices count up with source order") would have caught it during
implementrather thanpolice. Filing as a small RelSpec extension would shorten future fact-check on adjacent rel-ordering work.
Workflow completed at 2026-04-30.


Backlink context cards from the same source note now render in source order, the order their links appear in the underlying
.mdfile. Previously, when one note linked to a target several times, the cards were sorted lexicographically by surrounding block content (whatever fell out ofOrd [B.Block]), and two links sharing one paragraph ([[Foo]] and [[Foo]]) collapsed into a single card underIxSet.fromList's set-dedup. Closes #186.What broke and where
Two failures both rooted in
Rel's derivedOrd = (relFrom, relTo, relCtx):(relFrom, relTo)→ tie-break onOrd [B.Block](lex on Pandoc inlines)(relFrom, relTo, relCtx)→ equal underEq→ dropped byIxSet.fromList's set-dedupThe fix
Add
_relSrcPos :: !Intbetween_relToand_relCtx. The derivedOrdthen breaks ties between same-source-target rels by source position before falling back to lexicographic block order, and identical-context rels are no longer equal so the set-dedup leaves them alone.noteRelspopulates_relSrcPosviazipWith [0 ..]over the flattened per-note pandoc-link list.Subtle:
Text.Pandoc.LinkContext.queryLinksWithContextaccumulates per-URL contexts viaMap.fromListWith (<>), whose combiner runs asf new old— each later traversal entry is prepended, yielding the per-URLNonEmptyin reverse source order. The first cut of this PR missed that and the regression test caught it;noteRelsnow reverses the per-URL list back to forward order before zipping.Tests
Two regressions in
Emanote.Model.Link.RelSpec:Ix.toListwould yield A-then-Z; the assertion checks Z-then-A.Foo.mdtwice;length (Ix.toList (noteRels note))must be 2.Commit history
The branch traces a structural-review arc rather than a single squash:
01e85710noteRels4538fd07f40e826549e069361dab2e51d5dff3ab/code-policefact-check — caught theMap.fromListWithreversal bug7a875c480e96b8395682953c/code-policeelegance —WL.plainifyin tests, comment dedup,!IntstrictnessThree follow-up issues filed for deferred structural critiques: #689 (
newtype SrcPoswrapper), #690 (reusableWithSourcePospattern when a second instance appears), #691 (consider relocating presentation order tomodelLookupBacklinks).Try it locally
Generated by
/doon Claude Code (modelclaude-opus-4-7).