Building MindMap Debugger for AWS First Commit by #WeMakeDevs — from the first working extraction to the bugs I found the night before submission.
What it does
MindMap Debugger takes any argument or transcript you paste in, and finds two things in it:
- Contradictions — two things that were said which can't both be true.
- Circular reasoning — a chain of claims that loops back and ends up using itself as its own proof.
The idea came from a simple observation: reading carefully doesn't scale. A meeting transcript says the deadline is fixed. Ten minutes later someone says there isn't time to hit it. Nobody flags it because everyone's listening for their own part, not tracking the whole argument. The same thing happens in design docs, contracts, long debates — claims pile up faster than any one reader can cross-check them.
Day 1 — Getting something to work at all
I started with the basic shape: paste text in, call an LLM to extract "propositions" (claims) and "relations" (how those claims connect — supports, depends_on, contradicts), then run detection logic over the relations to find contradictions and cycles.
For the model backend, I used Groq — free, no card required, which mattered because I don't have a debit or credit card and UPI isn't accepted by AWS account signup. That also meant pivoting from the "Ship It" track (needs a real AWS account) to "Build It" (no AWS account required). To still satisfy the "use AWS-native tooling" requirement, I wrapped the Groq call in AWS's Strands Agents SDK — Strands is model-agnostic, so it happily talks to Groq through an OpenAI-compatible endpoint even though Groq isn't Bedrock.
The first real bug showed up almost immediately: I was using gpt-oss-120b, a reasoning model, and its internal chain-of-thought was leaking straight into the output instead of staying hidden. Instead of clean JSON, I'd get a wall of the model "thinking out loud" followed by the actual answer buried somewhere inside.
The fix was passing reasoning_format=hidden and reasoning_effort=high through Strands' extra_body parameter — plain response_format alone and a top-level reasoning_format both failed silently. extra_body was the only path that actually worked.
By the end of Day 1, the pipeline could take a paragraph, extract claims, detect an obvious contradiction, and show it in a rough UI. It felt like a huge milestone. It was also, I'd learn the next day, held together with tape.
Day 2 — Every bug I fixed was hiding another one
Day 2 started with a simple question: does this work reliably, or did I just get lucky once?
I ran the same input five times in a row. The number of findings bounced between 2 and 6. Same text, same prompt, same everything — wildly different output. That's when I learned Groq's serving of gpt-oss-120b is genuinely non-deterministic across calls, even with low temperature. A single extraction call just isn't trustworthy on its own.
The fix was consensus extraction: run the same extraction three times, then merge the results. In theory this smooths out the noise — if two of three runs agree on a relation, that's more trustworthy than any single run.
In practice, merging three noisy outputs together doesn't just cancel the noise. It creates new problems.
Bug: paraphrased duplicates weren't merging. The model would phrase the exact same claim slightly differently between calls — "the deadline is fixed" in one run, "the deadline cannot move" in another. My merge logic was doing exact text matching, so these became two separate propositions instead of one. I added a similarity check: if two propositions shared enough words, treat them as the same claim and merge them, keeping the higher-confidence version.
Bug: cycle detection had a blind spot. My find_cycles() function only built graph edges from depends_on relations. But a circular reasoning chain in the wild doesn't neatly alternate through one relation type — a loop might go depends_on → supports → depends_on back to the start. Because I was only looking at one edge type, I was missing real cycles that used a mix. Fixed by unioning both relation types when building the graph for cycle detection.
Bug: duplicate findings. The cycle-finding DFS could rediscover the exact same cycle starting from a different node in the loop — so a single 3-claim circular chain would get reported three times, once per starting point. Added a dedup step that collapses cycles with identical node sets, regardless of which node the search happened to start from.
By the end of Day 2, I had what looked like a solid pipeline: consensus extraction, similarity-based proposition merging, cross-relation-type cycle detection, deduped findings, and a Cedar policy gate on top (more on that below). I tested it, it looked right, and I moved on to building out the UI.
I should have kept testing longer.
Day 3 — The bugs the Day 2 fixes were hiding
This is the part of the story I almost didn't catch, and it's the most important part.
The pipeline "worked." But something felt off when testing edge cases, so instead of just looking at the UI, I went back to reading the raw console logs from pipeline.py — the same habit that had caught the Day 2 bugs.
The pipeline "worked." But something felt off when testing edge cases, so instead of just looking at the UI, the session went back to reading the raw console logs from pipeline.py — the same habit that had caught the Day 2 bugs.
Bug #1: the similarity merge was too aggressive
Take these two propositions from a witness-testimony test case:
P1: "the witness is reliable because their testimony is consistent"
P2: "their testimony is consistent because they are telling the truth"
These are about different things — P1 is about the witness's reliability, P2 is about their truthfulness — but they share a lot of common English scaffolding: "the," "is," "because," "their," "testimony," "consistent."
The old similarity formula was overlap divided by the smaller set's size. With 6 shared words and a smaller set of 8 words, that's 6/8 = 0.75 — well above the 0.7 merge threshold. The two distinct claims got collapsed into one.
The fix was switching to Jaccard similarity — overlap divided by the union of both sets, not just the smaller one:
overlap = len(words_a & words_b)
union = len(words_a | words_b)
return overlap / union
Same two propositions: 6 / 12 = 0.5 — below threshold, correctly kept separate. The union denominator punishes cases where two sentences only look similar because they both use common connector words. Genuine paraphrases (same core claim, same key content words) still merge fine; superficially similar-but-different claims no longer do.
After this fix, a 4-sentence test input that was collapsing into 3 propositions correctly produced 4. The fake circular finding (a nonsensical two-node "reliable → reliable" self-loop) disappeared, and the real 3-claim circular chain in that same text — reliable → consistent → truthful → reliable — was still correctly detected. The fix didn't suppress findings. It made them honest.
Bug #2: the model was directionally inconsistent, and merging fabricated cycles
This one was sneakier. Testing a "bridge maintenance" sample designed to have exactly one contradiction and zero circular chains, the tool reported one contradiction — correct — plus four fake circular chains.
The console logs showed why:
Run 1: P4 depends_on P5, P5 depends_on P6
Run 2: P5 depends_on P4, P6 depends_on P5
Run 3: P4 depends_on P5, P5 depends_on P6
The model was expressing the same underlying relationship with the arrow pointing in different directions across runs. "Regular maintenance requires a budget" got labeled maintenance depends_on budget in two runs and budget depends_on maintenance in the third. Individually, none of the three runs contained a cycle — each one's relations were internally consistent. But the merge step was unioning all edges from all three runs, and once you union P4→P5 from one run with P5→P4 from another, you've created a 2-node loop that never existed in any single reasoning chain. The cycle detector, doing its job correctly, found that loop and flagged it.
The fix: after merging relations, prune reverse-direction depends_on pairs, keeping only the higher-confidence direction:
for (from_t, to_t, rel_type) in list(all_rels.keys()):
if rel_type != "depends_on":
continue
reverse = (to_t, from_t, rel_type)
if reverse in all_rels:
if all_rels[reverse]["confidence"] > all_rels[(from_t, to_t, rel_type)]["confidence"]:
all_rels.pop((from_t, to_t, rel_type))
else:
all_rels.pop(reverse, None)
Two details mattered here. First, this only applies to depends_on — a bidirectional supports relation ("A supports B and B supports A") is genuinely circular reasoning by definition, so those are left alone. Second, when both directions exist, keep whichever one the model was more confident about, rather than just picking one arbitrarily.
After this fix, the bridge sample dropped from 1 contradiction + 4 fake cycles to exactly 1 contradiction, 0 circular — correct. And the witness sample still correctly reported its real 3-claim cycle, unaffected, because that cycle used consistent-direction edges across all three runs.
That's the actual engineering payoff of this whole project, more than the UI or the graph: a merge layer that's honest about disagreeing with itself instead of quietly averaging its way into fabricated findings.
Here's what the fully-fixed pipeline looks like live in the browser, on the product-launch sample from the top of this post — real contradictions and real circular chains, no fakes mixed in:
The smaller bug: a white flash on page load
Separately, on a hard refresh, the hero section's 3D constellation would flash as a plain white box for a fraction of a second before rendering. Browsers paint a <canvas> element with a default white backing store until WebGL actually attaches and draws to it — and on a cold load, there's a small race between the DOM painting and the JS clearing the canvas.
The fix was to keep the canvas invisible until the very first real frame has rendered:
<canvas id="hero-canvas" style="opacity:0;background:transparent;transition:opacity .5s ease"></canvas>
requestAnimationFrame(() => {
renderer.render(scene, camera);
canvasEl.style.opacity = '1';
animateHero();
});
Small fix, but it's the kind of "everyone who's touched WebGL knows this" gotcha that never gets written down anywhere obvious.
What I actually learned
LLMs aren't just noisy in what they say — they're noisy in which direction they say it. Two runs can both be individually correct and still disagree in a way that, if merged carelessly, manufactures a finding that never existed in either run alone. If you're combining multiple LLM calls to reduce noise, you have to explicitly check for this, because naive merging can create errors that are more confident-looking than the noise you were trying to remove.
Similarity metrics have real semantics, not just a threshold to tune. Overlap-over-smallest-set measures "how much of the smaller sentence is contained in the larger one." Jaccard similarity measures "how similar are these two sets overall, penalizing everything that's different." For deduplication, you almost always want the second one — the first one silently favors merging short, generic-sounding text with anything that shares its common words.
"The UI shows correct-looking output" is not the same as "the output is correct." The pipeline ran successfully the entire time on Day 2 and looked done. It just quietly produced wrong answers on certain inputs, and the only way to catch that was going back to raw console logs and deliberately trying to break it with adversarial test cases, instead of trusting that a clean-looking UI meant a clean-working pipeline.
Design is mostly subtraction. Not a pipeline lesson, but a real one from rebuilding the UI multiple times: every glow effect, every drop-shadow, every "make it look like the hero section" pass started too strong and ended up dialed back. The final cursor-following border glow on the dashboard cards is a 3px line with one radial gradient. Earlier versions had two stacked shadows and a masked pseudo-element and looked like a neon sign.
What the tool does, end to end
- Paste text with claims in it.
- Extraction runs three times through Groq via the Strands Agents SDK.
- Propositions are merged by Jaccard similarity — genuine paraphrases collapse, distinct claims survive.
- Relations are unioned across runs, deduped by confidence, and reverse-direction
depends_onpairs are pruned so the merge can't fabricate cycles. - Contradictions are found by scanning
contradictsedges. - Circular reasoning is found via depth-first search over
depends_onandsupportsedges, with duplicate cycles collapsed. - Everything is gated through a Cedar policy: contradictions always surface regardless of confidence; circular findings only surface above 0.6 confidence.
- Findings render as a 3D relation graph and a plain-language summary — no jargon, just "these two things can't both be true" and "this loops back on itself."
Repo and stack
Repo: github.com/mauryasagar/mindmap-debugger — MIT licensed, runs locally:
pip install flask strands-agents openai
python app.py
Then open http://localhost:5000.
Track: WeMakeDevs × AWS First Commit, Build It
Stack: Strands Agents SDK + Cedar (the required AWS-native tools), Groq gpt-oss-120b, Flask, Three.js
Why Strands and Cedar, specifically
This was built for the Build It track, which asks for AWS-native tooling — but I didn't want to bolt on an AWS SDK just to check a box, so here's the actual reasoning for each one.
Strands Agents SDK is the only thing standing between this project and Bedrock. I don't have a card to set up billing on a real AWS account, so Bedrock itself was off the table. Strands is model-agnostic by design, which meant I could point it at Groq's OpenAI-compatible endpoint instead and still get a real, working AWS agent framework doing the actual work of calling the model, handling the response, and giving me a structured place to add tools later if the project grows. It's not a token integration — every extraction call in the pipeline runs through it.
Cedar does the actual gating. Contradictions always surface regardless of confidence, because a false contradiction is still worth a human's attention — better to over-flag than hide something real. Circular reasoning only surfaces above 0.6 confidence, because a weak circular claim is more often noise than signal. That's a real policy decision, not a default, and Cedar is what makes it declarative and auditable instead of an if statement buried in detect.py.
Between the two, they cover the actual shape of what "Build It" is asking for: a real agent framework driving the reasoning, and a real policy layer deciding what a user sees.
Built with Claude and DeepSeek as coding partners.
I set out to build something that catches contradictions. Turns out the biggest one was mine: thinking it was finished.







Top comments (0)