DEV Community

Cover image for DecisionDebt: a company brain where every edge has a receipt (built on FalkorDB)
Himanshu Kumar
Himanshu Kumar Subscriber

Posted on

DecisionDebt: a company brain where every edge has a receipt (built on FalkorDB)

TL;DR: DecisionDebt is a company brain built on FalkorDB. Propose a decision ("migrate Auth Gateway to OrionAuth") and it tells you which services break, through which paths, who owns them, which docs go stale and who should review, before anyone says yes. Every edge in the graph carries a receipt: the exact characters in the exact source revision that justify it. An LLM agent handles the ambiguous cases, and it isn't allowed to make anything up.

πŸ”— Live demo (read-only, no sign-up) Β· πŸŽ₯ 3:56 video Β· πŸ’» Code Β· Built for Graph Hacks (WeMakeDevs Γ— FalkorDB), Company Brain track


The decision that breaks a team nobody invited

Aster Works (a fictional company, all its data synthetic) has an accepted auth decision, dec-auth-001. Someone now proposes its successor, dec-auth-002: migrate Auth Gateway to a new provider called OrionAuth.

In the meeting it sounds like a one-service change. It isn't.

  • Session Broker depends on Identity API, which depends on Auth Gateway. It also reaches Auth Gateway by a second path. Two people, Devon Shah and Taylor Roy, are each on record as its primary owner, in two different places.
  • Token Cache, three hops out, is affected too. It has no owner at all.
  • A checklist doc mentions OrionAuth in passing, and nobody knows if that mention means anything.
  • Some of the docs that justify the old decision haven't been reviewed in over 90 days.

None of this is secret. It's all written down somewhere: in 40 docs, 30 tickets and 24 chat threads. The problem isn't missing knowledge. It's that the knowledge is disconnected, so the cost of the decision stays invisible until it's paid. That hidden cost is what I call decision debt.

DecisionDebt turns that pile into one graph and asks it a question before the decision is accepted:

What would replacing Auth Gateway affect, who has to be involved, and how do we know?

For this proposal the answer is 12 services, 12 projects, 1 owner gap, 1 ownership conflict, and 5 stale-knowledge findings. Every item links back to the sentence that proves it.


Why this is a graph problem (and not a RAG problem)

"Which documents mention Auth Gateway?" is a search problem. A vector store handles it fine.

"What breaks if we replace Auth Gateway?" is a different kind of question. The answer is mostly services whose docs never mention Auth Gateway at all. Session Broker's docs talk about Identity API. Identity API's docs talk about Auth Gateway. The connection only exists if you walk it.

That's a traversal, and it's what graph databases are for:

// Q2: reverse impact. Consumer points to provider, at most 5 hops.
MATCH p=(consumer:Service)-[:DEPENDS_ON*1..5]->(root:Service {id:$root_id})
RETURN consumer.id AS service_id, length(p) AS hops,
       [n IN nodes(p) | n.id] AS service_path,
       [r IN relationships(p) | r.claim_id] AS claim_path
ORDER BY hops, service_id
Enter fullscreen mode Exit fullscreen mode

Look at the last two lines. The query doesn't just return which services are affected. It returns the path (why) and the claim ID on every hop (how we know). That one design choice runs through the whole project.


Architecture in one picture

 raw sources                     verification                      FalkorDB 6.0.2
 ───────────                     ────────────                      ──────────────
 40 Markdown docs  ─┐                                          β”Œβ”€> Service, Person, Team,
 30 JSON tickets   ─┼─> parse ─> candidate ─> exact-span ─> Claim ──   Project, Decision
 24 JSONL chats    β”€β”˜   phrases  relation    check vs.     node  └─> semantic edge
                                             SourceRevision           (carries claim_id)
                                                  β”‚
                          hedged / negated / β”€β”€β”€β”€β”€β”˜β”€β”€> inactive candidate (visible, never a fact)
                          unresolved alias

 FalkorDB ──(fixed, parameterized Cypher, ro_query)──> FastAPI ──> React Flow (layout only)
                                                         β”‚
                                       bounded LLM agent β”˜  (8 allowlisted tools, read-mostly)
Enter fullscreen mode Exit fullscreen mode

Three rules hold everywhere:

  1. The graph is the only source of truth for relationships. Python formats and validates rows. React only lays out nodes the API returned. There's no second impact calculation in the browser and no cached answer to fall back on.
  2. No edge without a claim. If the system can't point to the characters that justify an edge, the edge doesn't exist.
  3. People approve, the agent proposes. The model can draft work. It can't accept a decision, assign an ambiguous owner, or write to anything outside the graph.

The graph model

(Person)-[:OWNS {claim_id}]->(Service)<-[:DEPENDS_ON {claim_id}]-(Service)
(Person)-[:MEMBER_OF {claim_id}]->(Team)
(Project)-[:USES {claim_id}]->(Service)
(Decision {proposed})-[:CHANGES {claim_id}]->(Service)
(Source)-[:REFERENCES {claim_id}]->(Decision {accepted})
(Source)-[:ACTIVE_REVISION]->(SourceRevision)<-[:SUPPORTED_BY]-(Claim)
(Task)-[:CITES]->(Claim)      (Task)-[:AFFECTS]->(Service)
(Decision)-[:SUPERSEDES]->(Decision)   // only after a human accepts
Enter fullscreen mode Exit fullscreen mode

Twelve labels in total: Service, Person, Team, Project, Decision, Source, SourceRevision, Claim, Task, AgentRun, ApprovalEvent and Meta.

The interesting one is Claim. A Claim stores:

Field Why it's there
subject, predicate, object the fact itself (session-broker DEPENDS_ON identity-api)
source ID + source revision which version of which document said it
exact start/end offsets + anchor field the precise characters, in front matter, JSON or body
excerpt human-readable proof
extraction method + status how we got it, and whether it's active

So when you click an edge in the UI, this runs:

// Q5: recover exact provenance for a claim on an impact path.
MATCH (c:Claim {id:$claim_id})-[:SUPPORTED_BY]->(r:SourceRevision)
RETURN properties(c) AS claim, r.source_id AS source_id,
       r.sha256 AS source_sha256, r.body AS source_body, r.raw_text AS source_raw_text
Enter fullscreen mode Exit fullscreen mode

…and you see the original sentence highlighted, with the source's SHA-256. Not a summary of the doc. The actual sentence.


Ingestion: claims first, edges second

Most "build a knowledge graph with an LLM" tutorials go text β†’ LLM β†’ triples β†’ graph. That gives you a graph full of plausible facts with no way to audit any of them.

I went the other way. The extractor recognises explicit, supported phrasing and verifies every candidate against the exact span in the stored revision before it writes anything. Hedged ("we might move to…"), negated, unresolved-alias and unsupported statements never become facts. They're stored as inactive candidates: visible, auditable, and never traversed.

The initial import produced 315 verified claims and semantic edges and 4 inactive candidates from 94 sources. On the synthetic fixture, an independent BFS oracle (which runtime code can't read) measured precision 1.0 and recall 1.0 across those 315 relationships. To be clear about what that means: it proves the pipeline is faithful to the generator. It does not prove accuracy on arbitrary real company prose. I'd rather say that plainly than let a perfect score imply something it doesn't.

The payoff: retraction for free

Because every edge points at a claim, and every claim points at a source revision, updating a document retracts exactly what that document said, and nothing else.

In the demo I import a new ticket:

"External Portal depends on Billing API for customer identity verification."

The preview goes from 12 β†’ 13 affected services. Then I edit the same ticket to remove that sentence, and it goes back to 12. The graph moved through revisions 14 β†’ 15 β†’ 16. If another document had independently backed that edge, the edge would have survived, because support is tracked per claim.


Blast radius: diamonds, cycles and an honest boundary

Real dependency graphs are not trees. The canonical preview includes:

  • A diamond: Session Broker reaches Auth Gateway through two verified paths. The backend keeps up to three sorted supporting paths per service and reports how many alternatives exist, so you see every reason, not just the first one found.
  • A cycle: variable-length traversal with a hop cap (*1..5) keeps it bounded.
  • A boundary: there is a consumer beyond five hops. Rather than quietly drop it, a separate one-edge FRONTIER query detects it and the API returns truncated=true. The UI says so: "Max depth 5 Β· Cycle paths bounded Β· Further consumers exist beyond this boundary."

That last bullet matters more than it looks. A tool that silently caps its search and calls the result "the blast radius" is lying by omission. DecisionDebt shows you exactly where it stopped looking.

Then the affected set fans out to projects and owners:

// Q3: projects connected to the multi-hop affected set from Q2.
MATCH (p:Project)-[u:USES]->(s:Service) WHERE s.id IN $affected_ids
RETURN DISTINCT p.id AS project_id, p.name AS name,
       s.id AS via_service, u.claim_id AS claim_id
ORDER BY project_id, via_service
Enter fullscreen mode Exit fullscreen mode
// Q4: primary-owner claims for each affected service.
MATCH (p:Person)-[o:OWNS]->(s:Service {id:$service_id})
RETURN p.id AS owner_id, o.role AS role, o.claim_id AS claim_id
Enter fullscreen mode Exit fullscreen mode

Priority: a formula you can argue with

Every affected service gets a score, and the formula is public on purpose:

priority = clamp( 60 βˆ’ 10Β·depth + 5Β·criticality
                  + 12Β·unowned + 8Β·conflict
                  + 6Β·T1 + 4Β·T2 + 2Β·T3 , 0, 100 )

P0 β‰₯ 70   Β·   P1 β‰₯ 45
Enter fullscreen mode Exit fullscreen mode

Closer services score higher. Unowned services score higher than contested ones, because "nobody owns this" is worse than "two people think they do". Stale knowledge adds weight by tier. Session Broker, at depth 2 with a live ownership conflict, lands at 65 / 100: P1.

You can disagree with the weights. That's the point: they're visible, so you can disagree.


Drift: three kinds of stale

When a decision is proposed, some of what the company "knows" is about to become wrong. DecisionDebt sorts it into tiers by how sure we are:

Tier Meaning How it's found Canonical case
T1 Doc explicitly cites the decision being replaced graph join: Source -[:REFERENCES]-> Decision 1
T2 Doc mentions the old system but doesn't link the decision FalkorDB native full-text, then exact text re-check 1
T3 Linked doc last reviewed β‰₯ 90 days before the proposal review-date metadata 3

T1 means "this doc is now wrong". T3 means "someone should look at this eventually". Putting them in one undifferentiated list is how teams learn to ignore alerts.

War story: the full-text index that wouldn't build

T2 uses FalkorDB's native full-text search. My spec used the procedure form:

CALL db.idx.fulltext.createNodeIndex('Source','title','body')
Enter fullscreen mode Exit fullscreen mode

On the pinned falkordb/falkordb:6.0.2 ARM64 image, that returned "Received 3 arguments … expected at most 1". The DDL form worked:

CREATE FULLTEXT INDEX FOR (s:Source) ON (s.title, s.body)

CALL db.idx.fulltext.queryNodes('Source', $safe_query)
YIELD node, score
RETURN node.id AS source_id, node.title AS title, score
Enter fullscreen mode Exit fullscreen mode

Two more lessons came out of the same code path:

  • Hyphens break the full-text parser. Hyphenated service IDs caused a parser error. The fix was to resolve IDs to catalog names and quote safe alphanumeric tokens.
  • Index DDL isn't idempotent here. Re-running setup returned Attribute 'id' is already indexed. Setup now ignores exactly that response and still fails on any other schema error.

And one rule I didn't relax: a full-text score is not certainty. Every hit is re-checked against the exact alias in the active source revision before it becomes a finding, with a review cap of 80 candidates per alias.


The agent: an LLM on a short leash

Here's my bias, stated up front: the harness matters as much as the model. So the most reliable part of DecisionDebt doesn't use an LLM at all.

Public mode: no LLM, five templates

The "Ask the company graph" box on the live demo maps your question to one of five parameterised, read-only Cypher templates:

Ask Template
Who owns Session Broker? OWNERS
What would replacing Auth Gateway affect? DEPENDENTS + FRONTIER
Which projects use Billing API? PROJECTS
Who should review a change to Auth Gateway? REVIEWERS
What documents mention Auth Gateway? DOCUMENTS

Every answer shows the executed Cypher, bound parameters, row counts, timings and hash-verified source spans. Fuzzy matching reads the current Service nodes and asks you to confirm a typo instead of guessing. Ask something outside these shapes and it lists what it can answer. There are no keys on the server and no prompt to inject.

Local mode: a bounded investigator

The model's job is the three cases deterministic rules can't settle: a missing owner, conflicting owners, and an unlinked mention. The controller around it is where most of the engineering went:

  • 8 allowlisted tools, with a budget of 8 model turns, 12 tool calls and 90 seconds.
  • Tool schemas enumerate real IDs. Service and evidence parameters are enums built from the live graph, so the model can't cite a service that doesn't exist.
  • Coverage feedback after every turn, saying which retrieval work is still outstanding.
  • Outcome-stage tool restriction. Once all required evidence is retrieved, only propose and abstain tools remain. The model can't keep wandering.
  • Server-side guards. A proposal that assigns an owner the evidence doesn't support is rejected, whatever the model says.

The recorded run used nvidia/nemotron-3-super-120b-a12b:free on OpenRouter, with fallback disabled and zero-price caps:

8 turns Β· 11 tool calls Β· 51.6 s Β· 56,976 tokens Β· $0 provider-reported cost

What it concluded:

Case Evidence it actually retrieved Outcome
Token Cache has no owner No primary-owner claims; a verified dependency in doc-005; ticket-047 says Project Waypoint uses Token Cache verify_owner task, owner left blank: using a service doesn't make you its owner
Session Broker has two owners Both primary claims: Devon Shah (doc-021) and Taylor Roy (chat-077) verify_owner task, owner left blank: resolve the conflict first
"OrionAuth" mentioned in a checklist Native full-text search for OrionAuth, then the full doc-002 paragraph, which says the mention doesn't establish a dependency review_doc task, uncertainty=suspected_drift, no dependency activated

Notice the pattern: in all three cases the right answer was some form of "I don't know yet, a human should decide", and the system made that the easy path. A model that confidently picks Devon or Taylor is worse than useless, because it's wrong in a way that looks authoritative.

It made mistakes, and the harness caught them

The trace keeps three rejected calls: a mention ID passed where a Claim ID was expected, then two ownership proposals that cited a mention as evidence. The model corrected course and finally cited only the verified dependency claim. The server accepted no invented owner.

I also kept the failures. Six earlier incomplete runs are in the repo, covering turn and call exhaustion, incomplete case coverage, and a 121-second overrun where HTTP keep-alives defeated the read timeout. That bug led to a hard total deadline wrapped around the HTTP client. None of the failed attempts is relabelled as a success, and a mocked run can never satisfy the proof gate.


Humans in the loop, recorded in the graph

  • Previewing is free. GET /preview never stores findings or creates an AgentRun.
  • Approving a task goes through its own confirmation dialog and records an ApprovalEvent. It changes only that task's state.
  • Accepting a decision is a separate act. Only then does dec-auth-002 -[:SUPERSEDES]-> dec-auth-001 exist. Until then the old decision stays accepted and the new one stays proposed.

So the graph doesn't just hold what the company looks like now. It holds why it changed, who said yes, and what they were looking at when they did.


Proving FalkorDB is load-bearing

It's easy to claim "the graph does the work". So the deployment test does it the blunt way: stop only the decisiondebt-public-db container on the live server and see what happens.

Preview, company summary and agent-history reads all returned 503 GRAPH_UNAVAILABLE. There was no stale cache and no Python fallback traversal pretending everything was fine. Restarting the container brought back the same graph revision. CI does the same thing: GitHub Actions starts the pinned FalkorDB service and runs a real outage-and-restart test inside the suite.

Product operation Without FalkorDB
Decision impact (Q1, Q2) no affected set, no paths, no priorities, no boundary
Ownership and projects (Q3, Q4) no owner gaps, no conflicts, no project scope
Source-backed answers (Q5–Q8) no provenance, no stale citations, no full-text drift
Work and history no drafts, no investigation history, no approval lineage

A public demo you can't break

Judges should be able to poke the live demo as hard as they like, so public mode is locked down:

  • Every admin route and every non-read request returns HTTP 403, before any graph or provider work. Admin and LLM credentials are discarded at startup, even if someone supplies them by accident.
  • Per-peer and global token buckets. Overload returns 429 with Retry-After.
  • The database has no published port and sits on its own internal network.
  • Forwarded client addresses are trusted only from the verified Caddy proxy.

The public graph is an unchanged synthetic snapshot, seeded by an importer that refuses to overwrite a populated graph. A fresh check after deployment confirmed it was still at revision 16 after every write attempt was denied.


What I learned

  1. Design for "I don't know". Inactive candidates, blank owners, truncated=true, suspected_drift: the most valuable outputs were the honest non-answers. Build them in as first-class states and the system becomes something people can trust.
  2. Provenance isn't overhead, it's the product. Character offsets looked like over-engineering. They gave me retraction, the citation UI, auditable agent reasoning and a clean answer to "how do you know?"
  3. Constrain the action space, not just the prompt. Enum'd IDs, staged tool sets, budgets and server-side guards did more for agent reliability than any prompt wording.
  4. Keep your failures. Six failed runs and three rejected calls are in the repo. They show the harness working better than one clean trace ever could.
  5. Test the claim you're making. "FalkorDB is load-bearing" is a claim. Killing the database container is the test.

Stack

FalkorDB 6.0.2 Β· official falkordb Python client 1.8.0 (ro_query for every read, query for trusted writes) Β· Python 3.12 + FastAPI Β· Vite + React + TypeScript + Tailwind + React Flow Β· Docker Β· AWS Lightsail + Caddy (HTTPS) Β· GitHub Actions CI: 76 pytest tests (including the real DB outage) + 7 Chromium checks at 1440 px and 375 px.

Honest limits

  • The data is synthetic. Extraction handles explicit phrasing only and hasn't been evaluated on real company prose.
  • Single API worker with an in-process lock. A crash mid-import can mean re-running ingestion, and there are no distributed transactions.
  • Five hops is a deliberate bound, and the UI says so whenever it's hit.
  • Provider availability and pricing can change. The $0 is a receipt from one run, not a promise.

AI disclosure

The core idea and UI direction are mine: the separate landing page and decision studio, the read-only public deployment, the requirement for a genuine LLM run covering all three ambiguity cases, and the 4K narrated demo. AI expanded the idea into a detailed spec, and Codex generated most of the implementation, tests and styling. The demo narration is Deepgram Aura-2. I'm responsible for the graph model, the queries and the design choices, and I'm happy to defend any of them in the comments.


Try it

Open the studio and ask: "What would replacing Auth Gateway affect?" Then click Session Broker and follow the receipt all the way back to the sentence.

If you've ever been the person who found out last, I'd love to hear which decision you wish this had caught. ⭐ the repo if it was useful.

Top comments (0)