Why Infrawise Uses Deterministic Analysis Instead of an LLM Infrawise, an MCP server providing infrastructure context to AI coding assistants, uses deterministic analysis instead of large language models to answer fact-based questions. The tool extracts data from code via AST parsing, database schema introspection, and graph correlation, ensuring accurate answers about infrastructure like DynamoDB indexes. This approach avoids the hallucination risks of generative models when answering factual queries. 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 https://github.com/Sidd27/infrawise npm https://www.npmjs.com/package/infrawise — 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 https://ts-morph.com — 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 https://github.com/Sidd27/infrawise · npm https://www.npmjs.com/package/infrawise . 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.