The Knowledge Graph

The catalogs hold what a number is. This holds what it means — 325 mechanics across 29 compartments, joined by 398 typed relationships, carrying 1,689 individual claims that each name their source and the date it was checked. It is queryable like any other data structure, and it is built so a wrong number is detectable rather than merely unlikely.

A Node

Every mechanic is a node. Beyond a summary, it carries the SDK exports that implement it, the specific ways it has been misread, and its assertions.

import { knowledgeFor } from 'thetowersdk/knowledge'

const gt = knowledgeFor('ultimateWeapon.goldenTower')

console.log(gt.label)          // 'Golden Tower'
console.log(gt.kind)           // 'system'  — also: rule, entity, currency, stat, tier
console.log(gt.claimType)      // 'objective'
console.log(gt.verification)   // 'verified_here'
console.log(gt.summary)
// 'Golden Tower upgrades Multiplier, Duration, Cooldown with stones, and its UW+
//  enhancement is Golden Combo. Maxing every stat costs 47,256 stones.'

console.log(gt.disambiguation)
// 'The weapon itself, not its enhancement — Golden Combo is a separate purchase…'

console.log(gt.implementedBy)  // ['uwStoneChartData'] — the export that holds these numbers

Claims Are Checkable

An assertion is a subject, a predicate and a value, with provenance attached. That is what makes the graph testable: a claim says where it came from, so it can be re-checked against the same place rather than taken on trust.

console.log(gt.assertions[0])
// {
//   subject: 'ultimateWeapon.goldenTower',
//   predicate: 'statCount',
//   value: 4,
//   provenance: {
//     origin: 'code',
//     ref: 'thetowersdk/data uwStoneChartData',
//     verifiedAt: '2026-08-17'
//   }
// }

import { collectAssertions, GAME_KNOWLEDGE } from 'thetowersdk/knowledge'

console.log(collectAssertions(GAME_KNOWLEDGE).length)   // 1642

origin says what kind of source it is — game for something you can see in the game itself, code for a value that ships in an SDK export, wiki for community writing, and save for something read out of a real account. A claim you can confirm in game outranks one taken from a guide, and the graph records that ranking.

Sources Are Ranked, So Disagreement Is Visible

When two sources give different values for the same thing, the graph keeps both and says which one it believes. This is the part you cannot get from a wiki page or a spreadsheet.

import { findContradictions } from 'thetowersdk/knowledge'

for (const conflict of findContradictions()) {
  console.log(conflict.subject, conflict.predicate, '->', conflict.likelyValue)
  for (const claim of conflict.claims) {
    console.log(`   ${claim.value}  (${claim.origin}, rank ${claim.authorityRank}) ${claim.ref}`)
  }
}

// module.subEffect.slot8 opensAtLevel -> 241
//    241  (game, rank 0) Observed in game
//    242  (wiki, rank 5) The Tower: Early Game Tower Guide (community, 2026-08)

A lower authorityRank wins, and likelyValue is the graph's answer. crossSource marks a disagreement between different kinds of source rather than two readings of the same one.

Fact And Opinion Stay Separate

claimType divides measured behaviour from community consensus. Both are worth having — players ask what is good, not only what is true — and keeping them apart means a build planner can use the measurements and quote the opinions as opinions.

import { allNodes, GAME_KNOWLEDGE } from 'thetowersdk/knowledge'

const nodes = allNodes(GAME_KNOWLEDGE)
console.log(nodes.filter((node) => node.claimType === 'objective').length)   // 311
console.log(nodes.filter((node) => node.claimType === 'sentiment').length)   // 10

// 'Coins per kill versus coins per wave' — the community consensus, marked as such
// rather than presented as a measurement.

Traps: What Goes Wrong Here

1005 recorded misreadings across 325 nodes — the specific mistakes people have actually made, kept next to the mechanic they belong to. Asking a mechanic what goes wrong with it is a different question from asking what it is, and the answer is the more useful one when you are about to model it.

import { trapsFor } from 'thetowersdk/knowledge'

// Walks the node and everything it connects to, so a trap that lives on a
// neighbour still reaches you. Golden Tower's own node holds one; through its
// edges to the weapon set and the economy chain, this returns 18.
const traps = trapsFor('ultimateWeapon.goldenTower')
console.log(traps.length)   // 18

console.log(traps[0])
// 'Maxing Golden Tower costs 47,256 stones. The nine weapons range from 26,296 to
//  90,536, so they are not interchangeable purchases — pricing one from another is
//  out by more than a factor of two at the extremes.'

trapsByClaimType splits the same list into objective and sentiment, which is the difference between a measurement you can rely on and a community view worth quoting as one.

Relationships Are Typed

Edges say how two mechanics relate, not merely that they do. The kinds in use are gates, derivedFrom, memberOf, scales, caps, independentOf, separatePurchaseFrom and appliedBefore — enough to answer "what does this multiply" and "what must come first" by walking the graph.

import { relationsOf, neighboursOf } from 'thetowersdk/knowledge'

for (const edge of relationsOf('ultimateWeapon.goldenTower')) {
  console.log(edge.kind, '->', edge.to)
  console.log('   ', edge.note)
}

// memberOf -> ultimateWeapon
//     Golden Tower is one of the nine weapons, described from the shipped stone catalog.
// scales -> economy.cashPerKill
//     The one term the cash and coin chains share…

console.log(neighboursOf('ultimateWeapon.goldenTower'))
// ['ultimateWeapon', 'economy.cashPerKill', 'economy.killAward']

Resolving A Name

Players type acronyms. resolve maps one to a node, and resolveWithConfidence returns how sure it is with the runners-up, so a weak match can be treated as one.

import { resolve, resolveWithConfidence, search } from 'thetowersdk/knowledge'

resolve('gt')    // 'ultimateWeapon.goldenTower'
resolve('gt+')   // 'ultimateWeaponPlus.goldenCombo' — a different thing entirely

resolveWithConfidence('golden tower')
// { id: 'ultimateWeapon.goldenTower', score: 1000, confidence: 'exact', alternatives: [] }

resolveWithConfidence('gt')
// { id: 'ultimateWeapon.goldenTower', score: 800, confidence: 'strong', … }

search('golden tower')   // ranked hits, each with its node

Confidence is exact, strong, weak or none. Reading the score lets your tool ask "did you mean…" instead of guessing.

Compartments, And How Well Covered They Are

The 29 compartments group mechanics by domain — ultimate-weapons, economy, labs, enemies, jargon and so on. Each one scores itself, so you can see how thoroughly a domain has been checked before relying on it.

import { compartmentMaturity, describeMaturity, compartmentOf, GAME_KNOWLEDGE } from 'thetowersdk/knowledge'

for (const report of compartmentMaturity('2026-08-26')) {
  console.log(report.id.padEnd(18), describeMaturity(report))
}
// attack             7 claims · 100% primary-sourced
// …

const first = compartmentMaturity('2026-08-26')[0]
console.log(first)
// { id: 'attack', domain: 'multishot, rapid fire, bounce shot, …',
//   nodeCount: 7, edgeCount: 9, sourcedPct: 100, primarySourcedPct: 100,
//   staleCount: 0, contradictionCount: 0, unverifiedCount: 0,
//   trapDensity: 2.9, assertionCount: 37, sentimentCount: 0 }

console.log(compartmentOf(GAME_KNOWLEDGE, 'ultimateWeapon.goldenTower').id)
// 'ultimate-weapons'

staleCount counts claims last checked before the date you pass, so a domain that has drifted since the last game update reports it. primarySourcedPct is the share backed by the game itself or by a shipped catalog rather than by community writing.

Build A Mechanic Briefing

Put those together and a tool can hand a reader — or an assistant — everything known about a mechanic, with its sources, before a line of code is written about it.

import { resolve, knowledgeFor, relationsOf, trapsFor } from 'thetowersdk/knowledge'

function brief(term) {
  const id = resolve(term)
  if (!id) return null

  const node = knowledgeFor(id)
  return {
    label: node.label,
    summary: node.summary,
    isOpinion: node.claimType === 'sentiment',
    implementedBy: node.implementedBy,
    relatedTo: relationsOf(id).map((edge) => `${edge.kind} ${edge.to}`),
    watchOutFor: trapsFor(id),
    sources: node.sources.map((source) => `${source.origin}: ${source.ref} (${source.verifiedAt})`)
  }
}

console.log(brief('gt'))

Reach It From An Assistant Over MCP → · Patch Notes → · Catalogs →