Every programming language faces the same inevitable question as it grows: where should this new thing live?
In Jac, this question has real architectural teeth. We don't have a single flat namespace where everything gets dumped. We have a deliberate three-layer abstraction hierarchy, and every new capability we introduce must land in exactly the right layer — or the whole design philosophy starts to erode.
This post is a deep guide for contributors to the Jaseci codebase. If you're adding a new feature to Jac, this is the framework for deciding where it belongs.
Jac runs a lot of graph queries. They show up everywhere in Object-Spatial code, from [root --> [?:User]] to multi-hop walks like [r ->:Authored:-> [?:Post] ->:Tagged:-> [?:Topic]]. The naïve way to answer them is to walk every edge from the origin, load the targets, and check the filters. The TopologyIndex was added in PR #5205 to skip that work for type-filtered queries -- store enough metadata on the root node to resolve the survivors without ever touching the database.
The original implementation worked, but it had two problems we couldn't ignore. It scanned a flat adjacency list on every query, so cost was independent of selectivity (a query that matched 1% of the graph cost the same as one that matched 99%). And it had a quiet correctness bug for inheritance hierarchies: parent-type queries silently returned the empty set instead of all subtype instances.
PR #5784 rebuilds the index around a type-keyed lookup map with MRO-aware fan-out. Same canonical wire format. Same API surface. But typed reads now scale with match count, parent-type queries do the right thing, and the planner knows when to skip the index entirely. This post walks through what changed, why, and what the measured numbers look like.
We built the exact same app twice. Once with FastAPI, SQLAlchemy, LangChain, React, TypeScript, Vite, and Bun - the state-of-the-art (SOTA) stack that a senior engineer would reach for today. Once in Jac. Same features, same UI, same AI-powered categorization, same persistence. Both QA-verified to behave identically.
The SOTA version is 11 files, 233 lines of application code. The Jac version is 1 file, 46 lines.
These are good tools. FastAPI is arguably the best Python API framework. SQLAlchemy is the most mature Python ORM. LangChain is the dominant LLM orchestration library. React and TypeScript need no introduction. We picked the best of each category on purpose - this isn't a comparison against straw men.
And yet, building with the best-in-class version of every tool still produces ~5x more application code across 11x more files than expressing the same idea in a language designed to handle the full stack. We're calling this the polyglot tax - the code you write not to solve your problem, but to make your tools talk to each other across language, runtime, and type-system boundaries. This article is a line-by-line anatomy of where that tax shows up and what it costs.
If you've used jac create --use client to scaffold a Jac full-stack project, you've seen jac-client-node and @jac-client/dev-deps in your jac.toml. They're npm meta-packages — packages that exist solely to declare a list of other packages as dependencies. The idea: one line in your config gives you React, Vite, TypeScript, and everything else you need.
Sounds clean. In practice, it's a trap. I think we should replace both meta-packages with direct dependency injection, and I want to make the case for why.
If you've ever worked with a graph database at scale, you've probably hit the N+1 query problem. It's one of those things that doesn't show up in development but absolutely kills you in production. We ran into it with Jac's tiered memory system and spent a week figuring out the right fix. This is the story of what we tried, what failed, and what actually worked.
It started in a Jaseci weekly sync. We were discussing the jac-mcp server when someone brought up the idea of an MCP playground — a place to test MCP servers the way Postman lets you test REST APIs. The idea stuck with me, but it didn't feel real until I ran into the problem myself.
I was connecting jac-mcp to GitHub Copilot, trying to understand how tools were being called and what they returned. The answer was: I had no idea. Copilot called tools based on whatever prompt and model it chose, and I was left guessing.
When I started building the jac-shadcn MCP server, I hit the same wall. Testing through an LLM client meant my results depended on the model and prompt, not the server itself. That's not testing. That's hoping.
Every browser automation script you've written has the same hidden bug: it doesn't know what it's doing. It knows the steps. It doesn't know the structure.
You write page.goto(), then page.fill(), then page.click(). Each call succeeds or fails on its own. The script has no idea that it just navigated to a login page, that the text field it filled was a username input, or that the button it clicked submits a form. It's running commands against a stateless API. The moment the page structure changes, a new modal shows up, an element gets renamed, there's an extra redirect, the script breaks. Not because you got the logic wrong. There just wasn't any logic. Just steps.
This is where browser automation sits in 2026. Playwright, Selenium, Browser-use, Stagehand, they all give you good primitives. None of them give you a model for what a browser session actually is. State lives in local variables. Context evaporates between function calls. The relationship between a session, its pages, and their elements exists only in your head.
I built jac-browser to try to fix this. It's a browser automation library written entirely in Jac, 153 walkers that wrap Playwright. But the wrapper isn't really the point. The point is that browser sessions are graphs, and Jac gives you graphs as a first-class language construct. The result is a library where the data model is the automation framework.
Every model demo you've ever watched -- Anthropic, OpenAI, whoever -- follows the same script. Someone types a request, the model produces something impressive, the audience applauds. It's a compelling showcase for the model. It's a terrible workflow for an engineer.
I've spent years in compilers, language runtimes, and distributed systems -- the kind of work where handwaving doesn't survive contact with reality. When AI coding assistants arrived, I fell into it like everyone else. The demos make it look so clean.
It took longer than I'd like to admit to recognize the pattern: it's the same thing you see when a developer copies from Stack Overflow without understanding the code. The demo format trains passivity. It optimizes for throughput at the expense of understanding. That tradeoff doesn't hold up in production.
Most programming paradigms ask you to learn dozens of concepts, patterns, and best practices. Object-Spatial Programming (OSP) is different. At its core, there are really just four fundamental concepts you need to understand. Master these four things, and you'll have unlocked an entirely new way of thinking about computation—one that's particularly well-suited for modern applications dealing with complex relationships, graph structures, and distributed data.
This article will teach you the complete model of Object-Spatial Programming through these four conceptual buckets. Each builds naturally on the last, and by the end, you'll understand why OSP represents a genuine paradigm shift in how we structure programs.
Python's traditional class syntax has a problem: defining any class with fields requires excessive boilerplate. After decades of developers writing the same __init__, __repr__, and __eq__ methods, Python 3.7 introduced dataclasses (PEP 557) as a decorator-based solution. But dataclasses are a retrofit—what if dataclass semantics were built into the language from the start?