Live product · Analysis · Evidence notebook · Context benchmark
The analysis page connects concept discovery, recursive clustering, structural grounding, repository-disjoint evaluation, blinded human review, the threshold decision, and the context-latency benchmark. The evidence notebook retains the full experimental workflow, plots, prediction-level audit, and reproducibility checks.
Point it at a codebase; it parses every file into a structural box tree, lets you "zoom" that tree with a slider or a highlight, and answers questions about any piece by folding a recursively-cached, content-addressed "meaning" out of the code around it. No vector database, no daemon.
This document is the implementation-level reference, not a user guide. It is
deliberately verbose and assumes you are reading it next to the source. For the
short, conceptual pitch see the Concept recap at the very bottom, or the git
history of this file. Everything here describes the code as it actually stands in
the working tree (client + server), including the parts that supersede earlier
write-ups (notably: the question-answering path now uses a frontier-peers
context model, not only the vertical ancestor fold described in
docs/highlight-to-chunk.md).
- Stack & dependencies
- Repository layout
- Runtime topology
- The core data model: box trees & identity
- Ingest: file discovery, language routing, caps
- The parse-tree layer (
parseTree.js) - The three structure front-ends
- The chunking engine (
frontier.js) - Node resolution & frontier algebra (
tree.js) - The folder/project tree (
projectTree.js) - The meaning / RAG engine (
meaning.js) - The cache (
cache.js) and invalidation - The project registry / store (
store.js) - Search-based references (
references.js) - HTTP API reference (
index.js) - The client
- Configuration: env, models, caps
- Performance characteristics
- Known limitations & honest caveats
- Roadmap
- Concept recap
| Layer | Choice | Notes |
|---|---|---|
| Server runtime | Node ≥ 20 (developed on v23), ES modules ("type": "module") |
single process, no worker threads |
| HTTP | express@4 + cors |
express.json({ limit: '50mb' }) for folder uploads |
| Parsing | web-tree-sitter@0.22 (WASM) + tree-sitter-wasms@0.1 |
no native addon, no node-gyp, no grammar-ABI conflicts |
| LLM | OpenAI JavaScript SDK (Chat Completions) | two model tiers (see §11.8) |
| Config | dotenv |
.env at repo root |
| Client | React 19 + Vite 6 | @vitejs/plugin-react; no router, no state library, no CSS framework |
| Dev orchestration | concurrently |
npm run dev runs server (node --watch) + Vite together |
The repo has a Node --test suite (npm test) covering cache/session isolation,
telemetry, source scanning, intent-analysis utilities, and UI-facing source-range
labels. It runs locally; a hosted CI workflow is not configured yet.
Procfile (web: npm start) + the build/start scripts make the repo
deployable to a Heroku-style PaaS or a container: build installs and Vite-builds
the client into client/dist, and start runs the server, which serves that
static bundle from the same origin (see §3).
.
├── package.json # root: server deps + dev/build/start scripts
├── Procfile # web: npm start
├── .env / .env.example # OPENAI_API_KEY, PROJECT_DIR, PORT, model + window overrides
├── .tree-cache/ # gitignored: one JSON per parsed file (content-addressed)
├── .meaning-cache/ # gitignored: durable basic-summaries.jsonl only
├── test/ # Node test suite (cache, telemetry, ingest, intent, source ranges)
├── docs/
│ └── highlight-to-chunk.md # deep-dive on highlight→tightest-box→path (still accurate
│ # for the box mechanics; superseded for the *meaning* fold)
├── server/
│ ├── index.js # Express app: every route, warm-up, static-serve, clamps
│ ├── store.js # in-memory multi-project registry (LRU), sample file, bare-window
│ ├── ingest/
│ │ ├── code.js # walkProject, language map, source-file filter, ingest caps, line chunker
│ │ ├── parseTree.js # web-tree-sitter → plain JSON tree; mem + disk cache
│ │ ├── codeTree.js # parse tree → box tree (drops anonymous nodes) → frontier
│ │ ├── json.js # hand-rolled JSONC parser → box tree → frontier
│ │ ├── genericTree.js # grammar-less indent/bracket/separator chunker → frontier
│ │ ├── frontier.js # THE shared engine: balance, identity, coverage, granularity cut, tightest box
│ │ ├── tree.js # fileNodes (flat id→node map), semantic walks, minimalFrontier
│ │ ├── projectTree.js # folder tree above the files; content-addressed folder hashing
│ │ └── references.js # search-based find-references (tree-sitter identifiers + text fallback)
│ └── llm/
│ ├── meaning.js # the RAG engine: bare/ctx/peers, marks, drill, folder chat, prompts, model routing
│ └── cache.js # durable basics on disk + per-session focused meanings in RAM
└── client/
├── index.html # fonts (Inter / JetBrains Mono / Crete Round / Roboto Slab), #root
├── vite.config.js # port 5174, proxy /api → :8799
└── src/
├── main.jsx # createRoot
├── App.jsx # top-level state, views (main/help), modals, localStorage, welcome animation
├── index.css # the entire hand-written design system (no framework)
├── components/
│ ├── CodeViewer.jsx # viewer, chunking modes, chat, edits/diff, refs peek
│ ├── Library.jsx # file tree + per-folder chat + inline ref linking
│ ├── ProjectSwitcher.jsx# manila-tab project bar, drag-reorder, 3 upload paths, window slider
│ └── ContextAttach.jsx # 📎 attach-one-file-as-context picker
└── lib/
├── api.js # thin fetch wrappers for every endpoint
├── richText.jsx # minimal Markdown renderer + clickable mentions/symbols
├── sourceRange.js# shared half-open offset → exact line-label conversion
└── uploadFolder.js# read a browser-picked folder (3 APIs) into {relPath, content}[]
Development (npm run dev): two processes.
- Vite dev server on :5174 serves the React app and HMRs it.
- Express API on :8799 (
PORT). client/vite.config.jsproxies/api/*from 5174 → 8799, so the browser only ever talks to one origin and there's no CORS in the loop during dev.
Production (npm run build && npm start): one process.
buildrunsvite build→client/dist(content-hashedassets/*, an unhashedindex.html).startrunsserver/index.js. At boot it checksfs.existsSync(client/dist); if present it mountsexpress.staticand an SPA fallback:assets/*(content-hashed) →Cache-Control: public, max-age=31536000, immutable.- everything else incl.
index.html→Cache-Control: no-cache(so a redeploy can't pin a staleindex.htmlpointing at asset hashes that no longer exist). - A non-API path with a file extension that misses static →
404(fail loud) rather than returningindex.html(which would make the browser try to execute HTML as a script). Extensionless paths →index.html(client routing).
The server boots by loadProject(PROJECT_DIR) (default = this repo, so it
self-demoes), then kicks off the background warm pass (§11.3). The console prints
the resolved project dir, file count, total bytes, and any cap hits.
Everything downstream — chunking, retrieval, caching — is defined over one structure: the box tree. A box is
{ start, end, depth, label, kind: 'container' | 'leaf', children, rank?, synthetic?,
id, hash, semantic, /* non-enumerable: */ parent }start/end are character offsets into the file text (half-open [start, end)).
The root box is the whole file; its children are the file's top-level units, and so
on down to tokens/operands. The three front-ends (§7) each emit a raw box tree;
buildTree (§8) then decorates it identically regardless of source language.
assignIdentity(node, text, parentId) stamps every box with two hashes — this
separation is the keystone the whole cache leans on:
id— path identity.shortHash("${parentId}:${start}:${end}"), 12 hex chars. BecauseparentIdis itself a hash of its parent's id, the id is a rolling hash of the entire root→node chain. Two boxes with the same span but different ancestry get different ids; a single-child chain that shares a span with its parent is disambiguated by theparentIdprefix. Theidis the client handle and the key for "meaning with outside context."hash— content identity.shortHash(text.slice(start, end)), 16 hex chars. Identical spans anywhere in the project collide on purpose, so "meaning without outside context" (the bare summary) is computed once and shared across every copy of that exact code.
Both identities are a pure function of (file text, parse shape). The sliders
never reshape the tree — they only choose a different cut through the same fixed
tree — so ids/hashes are identical across every chunking of a file. That is what
lets the meaning cache key on structure instead of on the (settings-dependent)
emitted chunks.
parent is defined non-enumerable so the tree never self-serializes (and so the
disk parse-cache and nodes closures stay finite/acyclic).
balance (§8.1) inserts synthetic grouping boxes to cap branching. These are
marked semantic: false (everything else is true). Synthetic boxes are pure
scaffolding: the meaning recursion and the breadcrumb walks skip through them
(semanticParentId, semanticChildren, semanticPath), so an LLM is never asked
to summarize "statements 1–20 of 40" — only real units. They still carry ids and
participate in coverage, so they cost a slightly larger nodes map and nothing else.
A frontier is a set of boxes that don't tile the file by themselves — there's glue
(whitespace, operators, punctuation, closing brackets) in the gaps. assignCoverage
turns a frontier into contiguous, non-overlapping chunks that tile the file
end-to-end by deciding, for each gap [a, b), a split point:
- characters in
LEFT_ATTACH = { } ] ) , ; : }"ride left" (attach to the preceding box); everything else (openers, identifiers, indentation) "rides right." splitGapscans the gap and puts the boundary just past the last left-attaching char.
So } and , stay with the unit they close, and the next unit's indentation/opener
starts the next chunk. Result: every character has exactly one home, chunks are
flat and contiguous, and boundaries land on structural seams. This superset rule
covers JSON (} ] : ,) and code () ; too) with one table.
server/ingest/code.js owns disk-side ingest and the language map.
walkProject(dir)recursively collects relative paths of source files, pruningSKIP_DIRS(node_modules,.git,dist,build,.next,out,coverage,.venv/venv,__pycache__, caches,.idea/.vscode,target,vendor,.turbo, …). Dotfolders are skipped only if inSKIP_DIRS(so.githubis allowed). Entries are sorted for deterministic ordering.languageFor(filename)maps extension → language label viaLANGS(js/jsx/ mjs/cjs→javascript, ts/tsx→typescript, py→python, plus ruby, go, rust, java, c/h, cpp/cc/hpp, csharp, php, swift, kotlin, scala, css/scss, html, vue, svelte, json, yml/yaml, toml, markdown, bash/sh, sql, r). Unknown ext →null(non-source).isSourceFileadditionally rejectsSKIP_FILES(lockfiles:package-lock.json,yarn.lock,pnpm-lock.yaml,composer.lock,poetry.lock,Cargo.lock,Gemfile.lock) — they match a source extension but drown out real code.- Caps (no silent truncation):
MAX_FILE_BYTES = 1_000_000(skip >1 MB files),MAX_FILES = 4000,MAX_TOTAL_BYTES = 150_000_000(~150 MB summed text). Each cap hit is recorded (skipped,truncatedByCount,truncatedByBytes) and surfaced through/api/files→ the UI warns rather than under-reporting.
chunkByLines also lives here — the original line-based chunker — but it's legacy:
the live endpoints route every file through one of the three structural front-ends
(§7). It survives as a fallback shape and for the start/end char-span convention.
The endpoint routes by language in a fixed order (see §15, /chunks):
- JSON → the hand-rolled JSONC parser (
json.js), not tree-sitter, even though a json grammar exists in the pack. - Tree-sitter-parseable (
isParseable) → CST chunking (codeTree.js). TheGRAMMARmap inparseTree.jslists 22 labels, but two of them (r,sql) resolve only to a localserver/grammars/*.wasmthat is not checked into the repo, and they aren't in thetree-sitter-wasmspack either — so today they fall through to the generic chunker.yamlis deliberately excluded fromGRAMMAR(its packed grammar is broken for this runtime). Net: ~20 languages get true CST chunking (js, ts, python, go, rust, java, c, cpp, csharp, ruby, php, css, html, bash, scala, swift, kotlin, lua, toml, vue). - Everything else with a recognized source extension (yaml, scss, svelte,
markdown, sql, r, …) → the generic indentation/bracket/separator chunker
(
genericTree.js).
So every ingested file gets a real component hierarchy; there is no flat-lines mode in the live path.
Turns (text, language) into a plain, serializable parse tree
{ type, start, end, children? } with character offsets, via web-tree-sitter.
- WASM, lazy, cached.
Parser.init()once;Language.loadper language, cached inlangCache.loadLanguageprefersserver/grammars/tree-sitter-<base>.wasm(local override dir), else thetree-sitter-wasms/outpack. Missing wasm →null→ caller falls back to generic. - Named nodes only.
toNodekeepsn.isNamedchildren and drops anonymous nodes (operators, punctuation, keywords like&&,(,;,const). Those become the gaps coverage attaches to a neighbor — which is exactly why the deepest granularity bottoms out at operands/identifiers (~word level) without operators ever being their own boxes. Dropping them also shrinks the persisted tree. - Offset caveat. web-tree-sitter reports UTF-16 code-unit indices; JS string indexing matches for the BMP, and the same JS string is sliced with the same indices everywhere, so astral chars (emoji) stay consistent.
Parsing is the expensive structural step, and it's invariant across the sliders, so it's parsed at most once ever per content version:
- In-memory
memCache: Map<sha1(language\0content), tree>. - On disk
.tree-cache/<sha1>.json(in this repo, never the inspected project; gitignored). On miss,loadTreereads the disk file; on disk miss it parses, then writes the JSON (best-effort; persistence failure is non-fatal).
The key is sha1(language\0content), so a single edited character yields a new key
and the stale tree is simply never read again — no invalidation logic. Helpers:
treeCacheKey, clearTreeCache (wipe both layers), clearTreeCacheForKeys (drop a
specific project's trees on close).
Each front-end's only job is to produce a raw box tree; they all hand off to
buildTreeCached + cutTree (§8) so granularity / depth-spread / coverage behave
identically across languages.
boxFromParse recurses the parse tree, keeps named children, and marks a node
container iff it has child boxes (else leaf). label = node.label || node.type.
Box-built tree is memoized under code:${hashKey(text)}.
A small recursive-descent parser (parseJsonTree) that tolerates // and /* */
comments and trailing commas, emitting nodes with char spans. buildBox turns it
into boxes with a deliberate rank (split priority, lower cracks first):
- object/array container →
rank 0 - a member whose value is itself a container →
rank 1 - a scalar member (the
key | valuecolon split) →rank 2
So nested structure is always revealed before scalar key/value pairs are split apart
by their colon. Object members become a container of [keyBox, valueBox]; array
elements each become a box. Labels build a parent ▸ key / parent ▸ index path.
Memoized under json:${hashKey(text)}.
For any file without a (working) grammar. Builds a hierarchy from the three signals reliable across all languages, no per-language rules:
- Indentation → line nesting (a more-indented line is a child; maintained with
a stack keyed on
_indent). - Brackets
() [] {}→ nested groups inside a line (matchBracket, best-effort, depth-counted). - Separators
; ,→ segment boundaries inside a line (splitInline).
finalize assigns depth, sets container/leaf, and unions child spans. It skips //
and /* */ comments but deliberately does not treat quotes as strings
(apostrophes in prose would wreck it). Memoized under generic:${hashKey(text)}.
Source-agnostic. Takes any raw box tree and the three controls and emits flat
chunks. buildTree is the build step (slider-invariant, cached);
cutTree is the cut step (per request).
balance— cap each node's branching toMAX_BRANCH = 2(binary). A node with 40 statements becomes a small balanced binary subtree of synthetic grouping boxes instead of a 40-way fan-out. Why binary: each crack then splits one box into exactly two, so each granularity notch adds exactly one chunk — chunk count == granularity across the whole range, no dead zones, no doublings. (Higher branch caps plateau at the low end.) The cost is deeper synthetic nesting over wide nodes, paid only in thenodesmap, never in LLM calls (synthetics are skipped).setDepth— number every node by tree depth.assignIdentity— the id/hash/semantic stamping from §4.1.
Returns { root, maxBoxes: countLeaves(root) }. Memoized by
buildTreeCached(key, text, makeRoot) in builtCache (keyed
${kind}:${contentHash}), so /chunks ticks and fileNodes calls share one built
tree; a content change yields a new key (self-invalidating, unbounded but bounded by
distinct file contents).
Read-only on built, so one built tree serves any number of cuts. Two modes:
Highlight mode (opts.around = { start, end }):
tightestBox(root, start, end) walks down from the root, at each level descending
into the unique child that fully contains [start, end); it stops when no single
child contains the range (the range straddles a gap). Returns just that one box —
the rest of the file is intentionally left unchunked, because the answer is about
that box alone. (Guards: a collapsed caret is widened to 1 char; the range is clamped
inside the file.) targetNodeId = that box's id. This is the descent that guarantees
exactly one root→target path (see docs/highlight-to-chunk.md for the full proof).
Granularity mode (opts.granularity, opts.depthSpread):
target box count = clamp(granularity, 1, maxBoxes). Start frontier = [root] and
repeatedly crack the chosen box until frontier.length == target:
splittable= frontier boxes that are containers with children.minDepth= shallowest splittable depth;limit = minDepth + depthSpread(depthSpread === Infinity⇒ no limit).eligible= splittable withinlimit.- Pick: lowest
rankwins (e.g. JSON braces before colons); size (end - start) is the tiebreaker (largest first).rankdefaults to 0. - Replace the picked box in-place with its children.
depthSpread (D) shapes how detail spreads: D = 0 ⇒ even breadth (the whole
file refines uniformly); D = ∞ ⇒ pure largest-first drill-down (dive deep into
one part while the rest stays coarse). The client exposes D on 0..6 where 6 maps
to 50 (≈∞).
Finally frontier.sort(by start) and assignCoverage (§4.3) tiles the file.
Returns { maxBoxes, chunks, nodes: collectNodes(frontier), targetNodeId }.
collectNodes walks each frontier box up to the root via parent, recording each
ancestor once (the while stops at the first already-seen ancestor), producing the
minimal closure the client needs to reconstruct any chunk's full ancestry —
shared prefixes stored once.
The chunking endpoints return only the closure for the chunks they emitted. The
meaning engine needs to resolve an arbitrary nodeId and walk around it, so
tree.js rebuilds the complete flat node map.
fileNodes(file)re-runs the same front-end selection as/chunksbut atFULL = { granularity: 1_000_000, depthSpread: 50 }, i.e. expanded to every leaf, so the returnednodesclosure spans the whole tree (every node is an ancestor of some leaf). The ids are byte-for-byte identical to the ones the client already holds. Result: a flat, statelessid → { id, parent, label, start, end, depth, semantic, hash }map, resolvable with no tree plumbing.childrenIndex(nodes)invertsparentpointers into aMap<id, childId[]>(the flat map only storesparent).- Semantic walks (skip synthetic groups):
semanticParentId(nearest real ancestor),semanticPath(root-first breadcrumb of real labels),semanticChildren(nearest real descendants on each branch, source-sorted),maxSemanticDepth(how many real levels exist below a node — bounds "more detail" drilling).
Given a set of marks (the boxes the user has asked about), return the coarsest tiling antichain in which every mark still appears as its own box, and every mark-free stretch is one big box. This is the inverse of the per-highlight tightest-box descent: that finds one box; this fills the rest of the file around a set of them with as few boxes as possible.
- Resolve each mark up to its nearest semantic box; drop stale ids (from an older
file version, absent from
nodes). - Finer wins: drop any mark that is a proper ancestor of another mark (keep only the antichain of deepest marks).
splitThrough= the union of all proper ancestors of the effective marks (the boxes that must be cracked open to expose a mark).- Emit top-down from the root: keep a box whole unless it's in
splitThrough, in which case recurse into its children. - Sort by source position. Zero / all-stale marks ⇒
[root](the whole file is one box).
This is what makes retrieval steerable and visible: the frontier you see banded in the viewer is exactly the partition the answer is read against (§11.2).
The layer above the per-file box trees: files are leaves, directories are internal nodes, the project root is the top. This is what lets a file's "meaning" keep climbing past the file into its folders, up to the whole project.
Content-addressed, like everything else:
- file leaf hash =
sha1(file content)— identical to the in-file root node's hash, so a file's folder-level bare and its whole-file bare share one cache key (bareKey = b:<hash>). - folder hash =
sha1(sorted child hashes joined by '|')— changes if any descendant changes, so a future edit would invalidate a folder and its ancestors (the keys are ready for edit-invalidation even though edits are out of scope today).
buildProjectTree(rootName, files) is a pure function (hashes depend only on
content + structure, not the root name), so it can rebuild a background project's
tree just to compute its cache hashes on close. setProjectTree/getProjectTree
hold the single active tree module-level. Chain helpers return the root→target node
list a file or folder inherits context from: folderChainTo(relPath) (root →
dir-of-file, for a file's chunks), folderChainToDir(dirPath) (root → the folder
itself, for chatting about a folder).
The largest and most important module (~800 lines). It answers a question about a selected unit by building a recursively-cached "meaning" of that unit and handing it, plus the code, to an answer model. There are two distinct context models in the codebase — know which is which:
bare(node) = meaning WITHOUT outside context
= summarize(node text) — keyed by content hash (b:<hash>)
contextualized(n) = meaning WITH outside context
= fold(bare(n), contextualized(semanticParent(n)))
— recurses up to the file root, then folds in folder context
— keyed by node id within a context namespace (c:<ctxNS>:<id>)
The recursion skips synthetic groups, so depth tracks real nesting. At the file root
(no semantic parent) it folds into rootContext (the folder chain's context),
continuing the climb into the folders. Trie-like reuse: every chunk in a file
shares the same ancestor prefix, so those ancestors' contextual meanings are computed
once and reused. This is the model docs/highlight-to-chunk.md describes. Today it
is used to describe sub-parts when drilling (drillDetail), not as the primary
context for the asked node.
This is what ask() and suggestEdits() actually use for the selected unit's
contextual meaning. Instead of folding the ancestor chain, it interprets the asked
node against the other regions of the minimal frontier — i.e. against the other
things you've asked about in this file. The distribution itself is the context.
- Marks are per-file and per-tab in memory:
fileMarks: Map<sessionId, { files: Map<ctxNS, Set<id>> }>. Asking about a noderecords its id as a mark (suggestEditspassesrecord: false— an edit isn't a question). minimalFrontier(nodes, idx, rootId, marks)(§9.1) gives the current tiling.foldPeers(node, selfBare, peerBares, rootContext)folds the asked node's bare against the bares of the other frontier members (its "peers"), with the folderrootContextas low-weight background. Model =MEANING_MODEL(gpt-5.1by default).- Cache key
cp:<ctxNS>:<askedId>:<frontierSig>wherefrontierSig = sha1(frontierIds). A new mark changes the signature and recomputes exactly onefoldPeerscall; the peer bares stay cached (content-addressed), so a fresh mark is cheap on a warm file. Cached kindctxpeers, held only in that tab's session cache.
After each answered question the viewer bumps marksVersion and refetches the
highlight-mode frontier. That refined structure updates the internal target, Trace
view, and chunk count; the project viewer deliberately keeps the code surface clean
by showing only the user's exact selection rather than persistent chunk bands.
Project load/upload always builds the local folder tree, but whole-project LLM
warming is off by default. Existing content-addressed summaries persist on disk,
and a cache miss for a selected unit is computed lazily by the ask path. Set
EAGER_PROJECT_WARM=1 to restore the detached, bottom-up pass that computes every
file's bare and every folder's bare up to the root. The pass is idempotent, slot-bounded
(§11.9), and a no-op without an API key. bareFile keys a file's bare under its
content hash (== its folder-leaf bareKey); bareFolder summarizes its direct
children's bares under the folder hash.
ctxNSForFile(file, tree) = sha1( sha1(folderChain bareKeys) : sha1(file content) ).
So a node's contextual meaning is keyed by its full upward context (the folder
chain and the file content), not just its file. Editing the file — or moving it
under different folders — yields a new ctxNS, so its contextual/peers entries and
its marks self-invalidate. The marks store is keyed by ctxNS for the same reason.
A unit longer than the bare window (DEFAULT_BARE_WINDOW = 48_000 chars,
per-project overridable, server-clamped to [4_000, 200_000]) can't be summarized in
one shot, so:
- map:
splitWindowscuts it into the fewest line-aligned windows that each fit (n = ceil(len / window), capped atMAX_WINDOWS = 24; a single over-long line is hard-split at char boundaries — the minified-blob fallback). Each window gets its own content-addressed bare (b:<hash(window)>, kindbarewindow), so identical windows reuse the cache. - reduce:
foldWindowsmerges the ordered per-window summaries into one unified bare, cached under the unit's ownb:<hash>(kindbare).
Windows are ephemeral scaffolding for one summary — never chunks, never in the
tree. Within the window the unit takes a single computeBare call. The window size
only changes how big files are summarized; it never changes how much code the
answer model sees (that's the separate fixed MAX_SNIPPET = 48_000, with
explicit …[truncated] marking so the model never silently guesses past a cut).
A question is answered at the level of the selected unit by default. A follow-up can push deeper into the same unit:
- Explicit "More detail" button ⇒
intent: 'deepen'⇒effDepth = min(depth + 1, maxDepth). - Typed message ⇒
classifyIntent(transcript, question)(a 1-wordMEANING_MODELcall,reasoning_effort: none, max 16 tokens) decidesdeepen(dissatisfaction / "more specific about the same thing") vsnew(resets to depth 0).
drillDetail descends effDepth semantic levels, then describes that level's units
concurrently — each via the vertical contextualizedMeaning (§11.1), folding off
the already-warm parent. Semantic leaves contribute raw code (≤600 chars) instead of
a too-thin "meaning." Fan-out is bounded by DRILL_CAP = 24 and surfaced as
truncated (no silent caps). The reader can also highlight part of the previous
answer (focus) to steer the elaboration at exactly that phrase.
| Function | Used by | Returns |
|---|---|---|
ask({file, nodeId, question, depth, intent, transcript, contextFile, bareWindow, focus, highlight}) |
POST /api/files/:id/ask |
{ answer, meaning, path, depth, atBottom, maxDepth } |
suggestEdits({file, nodeId, instruction, transcript, baseCode, contextFile, bareWindow}) |
POST /api/files/:id/suggest-edits |
{ code, original, path } |
askFolder({dirPath, question, transcript, contextFile}) |
POST /api/folders/ask |
{ answer, summary, path } |
frontierChunks(file, around) |
GET /chunks?rangeStart&rangeEnd |
{ kind, maxBoxes, chunks, nodes, targetNodeId, chunkCount } |
fileFrontier(file) |
GET /api/files/:id/frontier |
{ marks, frontier } (debug/demo, no LLM) |
warmProjectBares, projectCacheHashes, ctxNSForFile |
index.js warm-up / cache mgmt |
— |
ask flow, in order: resolve fileNodes and validate nodeId → compute
ctxNS and rootContext (folder fold) → decide effDepth (intent/classify) →
meaning = frontierPeersMeaning(..., record: true) → path = semanticPath →
snippet = clip(node code) → optional drill = drillDetail(effDepth) → optional
short literal highlight quote (≤ HIGHLIGHT_QUOTE_CAP = 200) → assemble the
answer prompt (recap of last 4 turns, file path, location breadcrumb, the contextual
meaning, the sub-part breakdown, the code, any attached context file, any
answer-focus, any highlight quote, the question) → ANSWER_MODEL call with
reasoning_effort: medium and a 4,096-token completion ceiling.
suggestEdits reuses the same frontier-peers context (but record: false),
revises baseCode if supplied (so iterative edits stack) else the unit's source,
and returns the full revised unit with any stray code fences stripped
(stripFences). It never edits the attached context file (prompt says so explicitly).
askFolder folds the folder chain to its ctx (chainContext), lists the
folder's direct children's bares, and answers. dirPath = '' is the project root.
frontierChunks is the highlight-mode chunker that the viewer hits (not
cutTree's highlight branch — that's the fallback): it unions the session marks with
highlightMarks(range) (the topmost semantic boxes the range overlaps, so a
cross-unit highlight isolates those units instead of collapsing to the whole file),
builds the minimalFrontier, tiles it with assignCoverage, and returns the band
the highlight's start lands in as targetNodeId, plus a lean ancestor closure and
maxBoxes (leaf count). The viewer auto-selects targetNodeId.
| Role | Default model | Knobs |
|---|---|---|
bare / window / folder bares / combine |
BARE_MODEL = gpt-5-mini |
cost-efficient, reasoning_effort: low |
classify / vertical fold / peers fold |
MEANING_MODEL = gpt-5.1 |
reasoning_effort: low |
| user-facing answers / edits / folder answers | ANSWER_MODEL = gpt-5.1 |
reasoning_effort: medium |
supportsEffort(m) adds OpenAI's reasoning_effort only for GPT-5-family models.
The local compatibility adapter translates the engine's internal message shape into
OpenAI Chat Completions and maps max_tokens to max_completion_tokens; legacy
thinking and cache_control fields in the internal call shape are not sent to
OpenAI. Cost goes where it is visible: the many small content-addressed bare-summary
passes use gpt-5-mini, while the answer the user reads uses gpt-5.1 by default.
- Global slot ceiling
CONCURRENCY = 8: every model call routes throughwithSlot, so an enabled eager whole-project pass and interactive requests share one ceiling instead of each spawning unbounded fan-out. - In-flight dedup
barePending/ctxPending: concurrent requests for the same cache key share one promise (and one model call), so parallel sibling drills can't each kick off the same shared-ancestor computation. Focused pending keys also include the browser-session id, so concurrent users never share contextual work.
The answer/folder system prompts embed a long, deliberate VOICE style guide whose
goal is a product-level explanation from a person who knows the code. Answers lead
with the capability, workflow, user-visible behavior, constraint, or failure mode;
implementation details support that explanation instead of becoming the headline.
Explicitly technical questions still get direct technical answers, tied back to their
product effect. The guide also avoids the hedged, summary-closing register an LLM
defaults to. It bans the autopilot tells: stance
adverbs ("Notably/Crucially"), "-ing" tack-on tails, "it's not just X, it's Y",
"In essence/Ultimately" wrap-ups, "Moreover/Furthermore" signposting, flattery
openers, both-sides hedging, and restating sentences. The internal scaffolding
(contextual meaning, sub-part breakdown) is explicitly framed as the model's own
understanding it must never refer to ("the summary", "the provided context"). Answers
target ≤ ~500 chars.
There are two cache lifecycles, separated by what can safely be reused:
- Basic summaries (
bare,barewindow,folderbare) are pure functions of content. They are shared across sessions and persisted in the append-only.meaning-cache/basic-summaries.jsonllog. - Focused summaries (
ctx,ctxpeers,dirctx) depend on the path one user has explored. They live only in an in-memory map keyed by the browser tab'ssessionStorageid. They are neither read from nor written to disk, expire after inactivity, and disappear on server restart.
On first load, an old mixed meanings.jsonl is migrated: only its basic rows are
copied into basic-summaries.jsonl; its focused rows are discarded.
- Append-only. A cache miss is the only write, and it's an O(1) append of
{ k: key, v: value, f: fileHash, t: kind }. Crash-tolerant: a torn final line is skipped on load.load()reads the whole log into the Map on first access (last write wins). - Content/identity keys mean a changed file produces new keys; stale lines simply stop being read. There is no invalidation logic in the hot path — only explicit GC (below).
Kind (t) |
Key shape | f (fileHash) |
What it is |
|---|---|---|---|
bare |
b:<contentHash> |
ctxNS (in-file) / contentHash (file) | unit/file meaning without context |
barewindow |
b:<windowHash> |
ctxNS | one window of an oversized bare |
folderbare |
b:<folderHash> |
folderHash | a directory's own summary |
ctx |
c:<ctxNS>:<id> |
ctxNS | vertical contextual meaning |
ctxpeers |
cp:<ctxNS>:<askedId>:<sig> |
ctxNS | horizontal frontier-peers meaning |
dirctx |
c:dir:<folderHash> |
folderHash | a folder's folded contextual meaning |
projectCacheHashes(files, tree) collects every f a project's entries were stored
under: each file's ctxNS (covers in-file bares, windows, ctx, ctxpeers) plus
every folder/file node hash in the tree (covers file bares, folder bares, dirctx).
cacheDropByFileHash(hashSet) rewrites the durable basic-summary JSONL without the
matching lines (temp-file + atomic rename). focusedCacheDropByFileHash independently
drops focused values from one tab session or, for integrity invalidation, every live
session.
- "Clear cache" (
DELETE /api/tree-cache,full: false): drops only the calling tab's focused values and exploration marks. Keeps parse trees and all basics — the durable, expensive building blocks. The next question just re-folds context off the kept basics. This is the UI's reset. - "Close project" (
DELETE /api/projects/:id,full: false): drops focused values for the calling tab but deliberately keeps parse trees and all file/folder basics. Reopening the same content therefore reuses the expensive summaries.
A window-size change (PUT /bare-window) drops the project's entire meaning set
(all kinds), because the bares were built at the old window. It re-warms immediately
only when EAGER_PROJECT_WARM=1; otherwise the next selected-unit cache miss is
computed lazily.
In-memory, multi-project, single active project. The file endpoints operate on the active one, so a single-project client works unchanged.
- Registry
Map<projectId, { id, absPath, name, files: Map|null, scan, lastUsed, uploaded?, bareWindow }>.files === null⇒ registered but evicted (re-scan on activate).makeProjectId=slug(basename)-sha1(absPath)[:8](stable across sessions, so the client can namespacelocalStorageby it). - Two residency caps.
MAX_RESIDENT = 3projects keep their file text in memory (LRU;evictLRUnever evicts the active or an uploaded project — uploads have no disk to re-scan).MAX_PROJECTS = 50caps registry metadata (enforceRegistryCapdrops the oldest non-active). - Disk projects (
registerProject/activateProject):scanIntowalks + reads with the same caps as ingest, recording skip/truncate stats. - Uploaded projects (
registerUploadedProject): built from posted{ relPath, content }[]with the ingest filters re-applied server-side (buildFilesFromUpload— never trust the upload). The id is a content fingerprint (upload-<slug>-<sha1>[:8]), so re-uploading the same folder reuses itslocalStoragenamespace (chunk-size overrides survive). - Per-project bare window (
getBareWindow/setBareWindow, clamped[4_000, 200_000]), threaded into everyask/suggestEdits/warm call. - Built-in sample file (
sample/rate-limiter.js, a tiny rate limiter) is always resolvable viagetFileregardless of the active project, so the help/demo page works before anything is loaded.countLinescounts newlines without allocating.
"Find references" without a language server — the approach GitHub code-nav, Sourcegraph search-based intel, and aider's repo map use.
- For a parseable file: walk the cached parse tree, push a hit for every identifier
leaf whose text exactly equals the name, excluding literals/comments
(
EXCLUDEDset: strings, chars, comments, numbers, regex, escapes). Classify each hit from its parent node type:definition(DEF_PARENTS: function/class/method/ struct/enum/trait/const/… declarations),call(CALL_PARENTS: call/new/method- invocation),import(IMPORT_TYPES), elsereference. - For a non-parseable / parse-failed file: a word-boundary regex text scan
(
(?<![\w$])name(?![\w$])), kindtext— like an IDE's plain-text search. - Approximate by design: name-based, not scope-resolved, so unrelated same-named
symbols match too — which is why the definition hits are flagged separately.
Bounded by
MAX_HITS = 500(surfaced astruncated).fromorders the origin file first; results group by file, hits sorted by offset, each carrying{ offset, line, kind, lineText }.
This quietly delivers the roadmap's old "project-wide where-is-this-used" item via search rather than vectors (see §19).
All JSON unless noted. Errors are { error } with an appropriate status (handlers
honor an e.status set by the lower layers, e.g. 503 when OPENAI_API_KEY is
missing, 404 for an unknown/stale nodeId). Focus-aware browser requests include
X-CodeArchitect-Session, generated once per tab in sessionStorage; headerless API
calls remain functional but intentionally receive no cross-request focused-cache reuse.
| Method | Route | Body / query | Returns |
|---|---|---|---|
| GET | /api/health |
— | { ok, projectDir, files, scan } |
| GET | /api/sample |
— | { file: { id, relPath, language } } (demo file) |
| GET | /api/files |
— | { projectDir, projectId, files[], scan } (metadata only) |
| GET | /api/files/:id/raw |
— | text/plain source |
| GET | /api/files/:id/chunks |
granularity, depthSpread or rangeStart,rangeEnd |
{ kind, maxBoxes, chunks, nodes, targetNodeId, chunkCount, … } |
| GET | /api/files/:id/frontier |
— | { marks, frontier } (debug, no LLM) |
| GET | /api/references |
name (2+ chars), from? |
{ name, count, truncated, definitions[], files[] } |
| POST | /api/files/:id/ask |
{ nodeId, question, depth, intent, transcript, contextFileId, focus, highlight } |
{ answer, meaning, path, depth, atBottom, maxDepth } |
| POST | /api/files/:id/suggest-edits |
{ nodeId, instruction, transcript, baseCode, contextFileId } |
{ code, original, path } |
| POST | /api/folders/ask |
{ dirPath, question, transcript, contextFileId } |
{ answer, summary, path } |
| GET | /api/projects |
— | { activeId, projects[] } |
| POST | /api/projects |
{ path } (local abs/rel dir) |
{ project, activeId, projects } |
| POST | /api/projects/upload |
{ name, files: [{ relPath, content }] } |
{ project, activeId, projects } |
| POST | /api/projects/:id/activate |
— | { project, activeId, projects } |
| DELETE | /api/projects/:id |
— | { activeId, projects } (durable summaries retained) |
| PUT | /api/projects/:id/bare-window |
{ chars } (active project only) |
{ ok, bareWindow, dropped, projects } |
| DELETE | /api/tree-cache |
— | { ok, trees, meanings } (calling session's focused values only) |
/chunks clamps granularity to [1, 1_000_000] and depthSpread to [0, 50];
rangeStart/rangeEnd are clamped into the file. Mode precedence inside the handler:
highlight (rangeStart/rangeEnd present → frontierChunks, falls back to
structural on error) → json → tree-sitter code → generic. Every project
mutation re-runs prepareActiveProject() to rebuild the local folder tree. The
detached LLM warm pass only runs when EAGER_PROJECT_WARM=1.
React 19, no router/state-lib. One source of truth in App.jsx; the viewer and
library own their local interaction state. All server I/O goes through lib/api.js.
- State:
files,projectDir,activeProjectId,projects,selectedId,projectLoading/loadingName, theview('main'|'help'), the sample file + its chunk slider, several modal flags (demo intro pager, "how caching works", clear-cache confirm, window-size confirm), and two persisted maps:chunkSizes— per-file granularity override,localStoragenamespaced by project (cv:chunkSizes:<projectId>), with a guard against writing back the value just loaded.edits— per-chunk edits keyed${fileId}::${nodeId}, each storing the edited text + the slider/location config at edit time (so the history carousel can jump back to that exact chunk), persisted undercv:edits.
- Welcome→work animation: a two-phase collapse (
viewerLockedhorizontal fill, thentopbarBigvertical collapse, thencontrolsReady) driven by timers, so the greeting opens into the workspace smoothly. One-way (re-expand only by refresh). - Cross-component signals:
jumpTarget(edit-history jump or a reference jump carrying a raw char offset),fileChatSignal(open a file with its chat already open, used when a folder-chat answer links a file),openReference. - Project switch/upload/remove all funnel through
loadActiveFiles()and surface the loading state on the stage.
The heart of the UI. Notable mechanics:
- Three chunking modes, last action wins: default (whole file = one chunk),
highlight (
pendingRange→fetchChunksAround), manual (manualMode→ granularity/depth sliders, now only exposed on the demo page). Re-chunk fires from one effect debounced ~80 ms on[file.id, pendingRange, manualMode, chunkSize, depthSpread, marksVersion];marksVersionbumps after every answered question so the resolved context follows the refined frontier. - Highlight → char offsets: every rendered
.segcarriesdata-cs(its absolute char start) and — by render invariant — exactly one text node, so a DOMSelectionendpoint maps to a file offset byNumber(seg.dataset.cs) + offsetInNode(segOffset).onCodeMouseUpresolves both endpoints (falls back to whichever resolves), drops the native selection, and setspendingRange. A plain click clears the selection back to whole-file. - Exact range labels: the main file toolbar, file-chat header, and saved answer
scope all call
sourceRangeLabel(text, pendingRange). The helper treats offsets as[start,end), so every surface reports the same line or line span as the visible selection instead of substituting the larger syntax node resolved around it. - Rendering:
segmentsFor(k)builds runs of same-chunk-index segments per line via a binary-searchchunkAt(pos)over the sorted[start,end)intervals (no per-char array).splitSegByHighlightoverlays the user's blue highlight band independently of chunk bands. Chunk bands use three muted alternating hues in the dedicated demo; they remain disabled in the project viewer. - Q&A chat: per-file transcripts (
chatLogs[file.id]), a genie-animated panel + FAB, "More detail" drill button (with optional answer-textfocus), and an attach-file control. The composer receives focus as soon as chat opens. The empty state keeps one explanatory starter prompt rather than multiple overlapping prompts. - Edits drawer + diff: a transparent
textareaover a backdrop that renders a char-level LCS diff (charDiff: trims common prefix/suffix, LCS only the changed middle, falls back to whole-replace past 4M cells) — inserted chars highlighted, a red gutter dot on lines with deletions. Below it, an edit-history carousel scoped to the file. Edits live inAppstate +localStorage; they never touch the real file. - References peek: clicking a clean single-identifier
codespan in an answer (linkCodegates to/^[A-Za-z_$][\w$]*$/) opens a portal listing every definition/use across the project, each row jumping to the offset (in-file scroll, or hand off toAppto switch files).
The file tree (built from flat relPaths, rendered with └/├ guide cells) plus
per-folder chat. Folder navigation has back/forward history; each folder keeps
its own conversation (chatLogs[cwd]). Answers get two kinds of linkification: a row
of jump-to buttons for every child file/folder the answer named (refsInText), and
inline clickable mentions where names appear in prose/code (via richText).
Ambiguous basenames resolve to the target closest to the current folder. Folder and
project chat also focus their composer on open and use the same single starter prompt
as file chat.
The top-bar manila-folder tabs. Tabs hold a fixed left-to-right order (selecting
only changes z-index); the active tab is drag-reorderable (draws a blue insertion
line, commits on release). Clicking the active tab opens its "Context detail"
window-size slider (8k–128k, step 8k). Adding a project opens a folder via three
paths in uploadFolder.js:
- File System Access API (
showDirectoryPicker, when same-origin top-level) — lightest prompt; <input webkitdirectory>fallback (Firefox/Safari);- drag-and-drop (older FileSystem Entries API; entries captured synchronously).
All three mirror the server's ingest filters client-side (skip dirs, lockfiles,
non-source ext, MAX_FILE_BYTES, and a 50 MB POST cap) so node_modules/binaries
never go over the wire. Capped at 4 open projects in the UI. The current demo passes
sandbox to this component, so its project-close control and add-folder button are
presentational and intentionally do nothing; clicking either shows a short custom
“Disabled for demo mode!” tooltip so the constraint is explicit.
A deliberately small Markdown renderer (not a library): ```fences → <pre>,
**bold** → <strong>, `code` → <code>, #-prefixed lines → headings.
Everything else (including newlines, via CSS white-space: pre-wrap) is left intact.
Optional mentions make known file/folder names clickable wherever they appear
(longest-name-first alternation so app.test.js beats app.js); optional linkCode
makes inline code a clickable symbol (the references peek).
Hand-written. A fixed gradient "mesh" backdrop; warm off-white surfaces; the manila
folder-tab motif (clip-path shapes with directional pseudo-element shadows) reused for
project tabs and the chunk-list drawer handle; genie-unfurl animations for the chat
panels; gradient-bordered "glass" help modals portaled to <body> so their blur
covers the top bar too. Fonts: Inter (UI, ExtraLight default), JetBrains Mono
(code/paths), Crete Round + Roboto Slab (display).
| Var | Default | Effect |
|---|---|---|
OPENAI_API_KEY |
— | required for chat / edits / folder summaries (chunking + viewer work without it) |
BRAINTRUST_API_KEY |
— | optional; records instrumented OpenAI calls, token usage, latency, and cost in Braintrust |
BRAINTRUST_PROJECT_NAME |
CodeArchitect |
Braintrust project that receives the traces |
PROJECT_DIR |
this repo | the codebase to inspect on local runs |
PORT |
8799 |
API port (Vite proxies /api here in dev) |
ANSWER_MODEL |
gpt-5.1 |
user-facing answers + edits |
MEANING_MODEL |
gpt-5.1 |
classify + context folds |
BARE_MODEL |
gpt-5-mini |
the many small bare passes (incl. the eager project pass) |
BARE_WINDOW_CHARS |
48000 |
default bare-window (per-project overridable in UI) |
EAGER_PROJECT_WARM |
unset/off | set to 1 to opt into whole-project background warming |
| Constant | Value | Where | Meaning |
|---|---|---|---|
MAX_BRANCH |
2 | frontier.js | binary balancing ⇒ granularity == chunk count |
MAX_FILE_BYTES |
1 MB | code.js | per-file skip threshold |
MAX_FILES |
4000 | code.js | per-project file cap |
MAX_TOTAL_BYTES |
150 MB | code.js | per-project summed-text cap (client upload caps at 50 MB) |
MAX_RESIDENT / MAX_PROJECTS |
3 / 50 | store.js | resident text LRU / registry metadata cap |
CONCURRENCY |
8 | meaning.js | global model-call slot ceiling |
MAX_SNIPPET |
48000 | meaning.js | code shown to the answer model (explicit truncation) |
DEFAULT_BARE_WINDOW |
48000 | meaning.js/store.js | map-reduce threshold (clamp 4k–200k) |
MAX_WINDOWS / DRILL_CAP |
24 / 24 | meaning.js | map-reduce window cap / drill fan-out cap |
HIGHLIGHT_QUOTE_CAP |
200 | meaning.js | max literal-highlight quote into the answer prompt |
MAX_HITS |
500 | references.js | reference result cap |
GRAN_CAP |
500 | CodeViewer.jsx | generic-file granularity slider cap |
| UI open-project cap | 4 | ProjectSwitcher.jsx | tabs |
The durable caches (.tree-cache/, .meaning-cache/basic-summaries.jsonl) live in
this repo, are gitignored, and rebuild on demand. Focused meanings never live
there; they exist only in server memory for the current browser session.
- Dragging never re-parses and never re-builds the tree. Parse → disk/mem cache;
box-build →
builtCache. A slider tick only re-runscutTree(a frontier walk + coverage) on an already-built tree. - Meaning is computed lazily and reused along structure. Bares are
content-addressed (identical code summarized once, project-wide). Contextual reads
are keyed by node id within
ctxNS, so shared ancestor prefixes (vertical) and unchanged peer bares (horizontal) are reused; a new mark recomputes exactly one fold. - Whole-project warm-up is optional. By default the app reuses any durable basics
already present and computes missing selected-unit basics lazily. Setting
EAGER_PROJECT_WARM=1opts into a background whole-project pass. - The client doesn't window the code (it relies on contiguous chunk intervals +
binary search), so very large files render as plain DOM; the drawer/animation costs
are mitigated with
will-change/GPU-layer hints in CSS rather than virtualization. - One ~75 KB-gzipped bundle, single origin in production, immutable-cached assets.
- No hosted CI workflow. The automated Node test suite runs through
npm test, but the repository does not currently run it in GitHub Actions or another checked-in CI definition. r/sqldon't actually CST-chunk yet. They're declared inGRAMMARbut their wasms aren't bundled (noserver/grammars/) and aren't in the pack, so they use the generic chunker.yamlis intentionally generic (broken packed grammar).- Focus is in-memory and per-session. Both the frontier marks and every contextual
fold reset with the browser/server session; only content-addressed basics persist.
Keys still include
ctxNS, so a file edit self-invalidates focused values. - Edit-invalidation is plumbed but not active. Folder hashes change with content
and
cacheSetstoresfileHashfor later GC, but the app doesn't write edits back to disk, so meanings aren't recomputed from edits today. - Why no vector DB. Retrieval here is exact-key lookup along a path you pointed at, not similarity search over an unknown location — so a vector index would be answering a question nobody asked, at the cost of an index to build/sync/host. The one place vectors would genuinely earn their keep — project-wide "where is this used?" — is now served well enough by search-based references (§14); a true semantic index remains a future option, not a present need.
- Why no daemon. The same reasoning rules out Redis/Postgres: an in-memory Map in front of a plain file (per-entry JSON trees; one append-only JSONL for meanings) matches a tool meant to start instantly on one laptop. Content-addressed keys mean there's no invalidation logic to get wrong.
- Structure-aware chunking with a stable identity per piece (id + content hash)
- Layered, cached meanings (vertical ancestor fold and horizontal frontier-peers fold) that pull in folder + project context
- Steerable, per-file and per-folder question answering with depth drilling
- Edit-by-conversation with a char-level diff editor + per-file history
- Multi-project registry (local path + browser folder upload) with LRU residency
- Search-based project-wide "find references" (definition/call/import/text)
- Auto-named pieces; files clustered into modules
- A navigable map of how the project's parts depend on each other
- Write edits back + activate the already-plumbed edit cache invalidation
- Optional semantic index for fuzzy "find code like this" (the one real vector use)
Stripped of implementation: CodeArchitect parses each file into a structural tree, gives every node a slider-invariant identity, and treats "zoom" as choosing a cut through that fixed tree (one notch = one chunk). You point at a piece — by dragging the slider or, in the main app, by highlighting — and ask about it. The system reads that piece against the fewest other pieces that still cover everything you've asked about (the minimal frontier of your marks), folds in folder/project context, and answers with a strong model — while a cheap model has already summarized every unit in the background. Each result is cached by a fingerprint of the exact code it describes, so identical code is understood once, shared context is computed once, and a changed line just misses the cache and recomputes. No vector database, no daemon — because the access pattern is "walk from here," not "search for something like this."
Build: npm install && npm install --prefix client && cp .env.example .env (add
OPENAI_API_KEY), then npm run dev and open http://localhost:5174. Viewer +
chunking need no key; chat / edits / folder summaries do.
CodeArchitect MCP · SQLHarness MCP
The dedicated pages provide hosted examples, remote client configuration, and versioned local-install packages. These MCP routes make no model calls and require no model credentials. Public tools have no personal-data upload or root-selection capability. See the deployment and privacy contract.
See PRIVACY.md and THIRD_PARTY_NOTICES.md. Public web uploads/path selection are blocked server-side; use the local MCP for private projects.