Ask your AI coding assistant which Global Secondary Indexes exist on your Orders
table. It will read your repository, find a few QueryCommand
calls, and answer β fluent, specific, and confident. It also has no way to know. GSI definitions live in AWS, not in your source files. The model isn't lying; the fact simply isn't available to it, so it generates the most statistically plausible substitute and delivers it in the same tone it uses for things it actually knows.
That failure mode is why Infrawise (npm) β an MCP server that gives AI coding assistants infrastructure context β contains no LLM calls at all. Every answer it serves comes from AST parsing, schema introspection, rule-based analyzers, and graph correlation. The LLM is only ever a consumer of that context, never a producer of it. This post is about why that boundary exists, and what it looks like in code.
There are two kinds of questions you can ask a tool. "How should I model sessions in DynamoDB?" is a judgment question β many defensible answers, context matters, an LLM is genuinely useful. "Does the Sessions
table have a GSI on userId
?" is a fact question. It has exactly one correct answer, and that answer is sitting in a DescribeTable
response.
When you route a fact question through a generative model, you convert a lookup with a perfectly accurate source into a prediction with an unknown error rate. The motivating examples in the Infrawise README are all of this shape: an assistant suggesting a .scan()
on an Orders
table with 50 million rows, recommending a GSI on status
that already exists, or not noticing that five functions are already hammering the same partition key. None of these are reasoning failures. They are missing-fact failures, and no amount of model quality fixes them β a better model just produces a more convincing wrong answer.
So Infrawise draws a hard line: facts get extracted deterministically, and the model receives them through MCP tool calls instead of guessing.
Infrawise builds its picture of your system from three sources, none of which involve a model.
Your code, through the compiler's eyes. scanRepository()
in src/context/index.ts
loads the repo with ts-morph β using your own tsconfig.json
when one exists β and walks every CallExpression
node in every source file. It doesn't regex for the word "scan". It matches call structure against known client patterns: a DYNAMO_OPERATIONS
set covering both SDK v2 method names (query
, scan
, getItem
) and SDK v3 command classes (ScanCommand
, QueryCommand
, PutItemCommand
), query
/execute
/exec
calls on PostgreSQL and MySQL clients, and MongoDB collection methods β where find
and aggregate
are classified as scan-type operations and the rest as queries. The output is a list of extracted operations: this function performs this operation type against this table.
Your databases, through their own catalogs. The PostgreSQL adapter doesn't ask a model to summarize your schema. It runs the same introspection queries you would run by hand β information_schema.tables
for tables, `information_schema.columns`
for columns, `pg_indexes`
for indexes, and the constraint tables for keys. The docs recommend pointing it at a dedicated read-only user, and the DynamoDB side needs only `dynamodb:ListTables`
and dynamodb:DescribeTable
permissions. What comes back isn't a description of your schema; it is your schema.
Correlation, through a graph. Both streams land in a SystemGraph
: typed nodes for tables, functions, indexes, queues, topics, lambdas, buckets, secrets, parameters, and log groups, connected by typed edges like query
, scan
, and uses_index
. The graph is what turns two boring fact lists into something an analyzer can interrogate β not just "this table exists" and "this function scans something," but "listAllOrders()
scans the Orders
table, and no index covers that access."
The analysis layer is where most tools would reach for a model β and where Infrawise stays deterministic. The analyzer index exports 27 rule classes covering DynamoDB, PostgreSQL, MySQL, MongoDB, SQS, S3, Lambda, RDS, secrets, log retention, and Terraform drift. Each one is an ordinary class with an analyze(graph)
method that walks the graph and emits findings.
FullTableScanAnalyzer
follows scan-type edges to DynamoDB table nodes and emits a high-severity finding naming the table and every calling function. MissingGSIAnalyzer
flags tables that receive query edges but have no uses_index
edge β medium severity, because it might be intentional. HotPartitionAnalyzer
fires when a table is accessed by five or more distinct code paths (the threshold is a constructor parameter, defaulting to 5).
Two properties fall out of this design that a model can't give you:
Findings are testable. Every analyzer is a pure function of the graph. Feed it a fixture, assert on the output, done. There's no eval harness, no sampling temperature, no "run it three times and hope." If FullTableScanAnalyzer
regresses, a unit test catches it.
Failures are contained and honest. runAllAnalyzers()
wraps each analyzer in its own try/catch β one analyzer crashing logs a warning while the rest keep running. The combined findings are then sorted by a fixed severity order: high
, medium
, low
, and notably verify
β a severity that exists precisely so a deterministic system can say "I detected a pattern but can't confirm the intent" instead of bluffing. An LLM has no equivalent of verify
; everything it says arrives with the same confident fluency.
None of this means LLMs are useless here. It means they belong at a specific layer. Infrawise exposes the graph and findings through 15 MCP tools: get_infra_overview
for a quick snapshot, analyze_function
to trace a single function's tables, queues, secrets, and trigger event shapes, suggest_gsi
to generate a ready-to-use GSI definition for a table and attribute, postgres_index_suggestions
for index advice, and so on. The assistant decides when to ask and what to do with the answer. It never produces the answer.
The plumbing is deliberately boring: analysis results are cached as JSON files under .infrawise/cache
, and the infrawise stdio
process your editor spawns re-runs the analysis when the cache is older than 24 hours. Run infrawise start --claude
once and it writes .mcp.json
so Claude Code reconnects automatically on every future launch.
This division of labor generalizes well beyond one project. The model handles intent ("the user wants this query to be cheaper") and synthesis ("given these findings, here's the migration plan"). The deterministic layer handles every claim that has a ground truth. The test is simple: if asking the same question twice should yield the same answer, don't generate the answer β look it up.
If your AI assistant writes code against AWS or a database, give it facts instead of letting it guess: GitHub Β· npm.
CallExpression
nodes) catches what schema introspection alone can't see β which function scans which table, and how.verify
severity when it isn't sure. A model can't reliably tell you when it's guessing.