ochna 0.0.5

A structural code graph indexing and analysis CLI using Tree-sitter and SQLite
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
# Call Resolution Dataflow Plan

## Executive Summary

Ochna is already useful for large-codebase archaeology: it can index Linux, Kubernetes, and Netty at useful scale and answer symbol/file questions quickly. The recent corpus experiments exposed the next ceiling: call edges are sometimes too broad because the current resolver receives call sites with too little semantic context.

The current dataflow records raw calls mostly as simple names, then resolves those names against a project-wide symbol index. This works for many direct calls and for small/medium codebases. It breaks down on common method names in large corpora:

- Go/Kubernetes: `GetList`, `Run`, `Add`, `Stop`, `Update`, `Watch`.
- Java/Netty: Java performs better because class-qualified methods are already indexed, but overridden/common names can still fan out.
- C/Linux: direct C function symbols work well; deletion-heavy API removals still need patch metadata because the important fact may be that a symbol no longer exists.

The proposed redesign is not "add full type checking." That would be a different product. The proposal is to preserve more cheap, AST-derived call-site context and resolve in staged passes with confidence levels. This keeps ochna lightweight while making graph answers more trustworthy.

## Why This Matters

Ochna is intended to be an agent-grade navigation tool. An agent does not need a perfect compiler graph for every language. It needs a graph that is honest about certainty and good enough to narrow investigation from "millions of lines" to "the 3-10 relevant definitions and callers."

The current resolver can produce a plausible but noisy edge for a common method name. That is worse than a clean unresolved reference in some workflows because it looks authoritative. A confidence-aware resolver gives the user and agent a better contract:

- exact edges are high signal;
- receiver/package edges are useful but inspectable;
- name-only edges are hints, not proof;
- unresolved references are preserved instead of silently dropped.

This plan keeps the current successful properties:

- fast fresh indexing;
- selective incremental re-resolution;
- SQLite portability;
- JSON query output;
- language support that can grow one parser at a time.


## Evidence From Recent Corpus Runs

Full write-ups live in `docs/experiments/` and the asobi project log; this is the
distilled signal. All runs used the installed `ochna` binary against the
benchmark submodules. The submodules are shallow to a single commit, so local
parent diffs are unavailable. The reliable workflow was:

1. verify the local index with `ochna status --json`;
2. use `git log --oneline -1` only to identify the current commit/PR;
3. use `gh api` for PR/commit metadata and patch files;
4. use ochna for current-tree symbols, line spans, callers, and callees.

| Corpus | Change | What ochna did well | Where it fell short | Root cause |
| --- | --- | --- | --- | --- |
| Kubernetes (PR 139848) | watch-cache test rewrite | located exact symbols (`ShouldDelegateList`, `GetExactSnapshotLocked`, the new tests) | broad callees of a big Go test mixed in unrelated `Run`/`Add`/`Stop`/`GetList` | Go selector calls store only the field name, not the receiver expr/type |
| Netty (PR 16959) | traffic-shaping write leak fix | `callers releaseAndFailQueuedWrite` returned exactly the 3 `handlerRemoved` cleanups | overloaded/common Java method names still fan out at scale | name-only fallback is accurate only when the name is distinctive |
| Linux (`strncpy` removal) | core API + arch impls deleted | confirmed `strncpy` gone from `lib/string.c`/`string.h`/FORTIFY while `strncpy_from_user` etc. remain | could not *explain* the removal from a symbol graph alone | current index models only the current tree; no prior tree / removed symbols |

See the standalone experiment reports for the full traces:

- `docs/experiments/kubernetes-pr-139848.md`
- `docs/experiments/linux-strncpy-removal.md`
- `docs/experiments/netty-pr-16959.md`

The Netty `callers` result is the positive model for this redesign: when names
are distinctive and ownership is clear, ochna already behaves like a strong
structural graph. The goal is to make common-named calls behave like that case,
and to be honest when they can't.


## Current Dataflow

Today, parser modules emit:

```text
source file
  -> AST traversal
  -> nodes: id, name, kind, qualified_name, file_path, span
  -> raw_calls: caller_id, callee_name, callee_simple, callee_scope, line, column
```

`run_init` then builds a `SymbolIndex` from nodes:

```text
by_name
by_name_file
by_id
namespace from qualified_name
```

`resolve_calls_global` does:

1. find candidates by simple name;
2. prefer same file;
3. disambiguate by explicit namespace or caller namespace;
4. otherwise choose the first candidate;
5. record an edge, or unresolved ref if there are no candidates.

This design is fast and compact. The problem is the last step: when candidates remain ambiguous, choosing the first candidate creates false authority.

## Target Contract

The redesigned resolver should satisfy these rules:

1. A call edge must carry how it was resolved.
2. Name-only matches must be distinguishable from exact or receiver-qualified matches.
3. Query commands should default to high-signal edges, while still allowing broad hints.
4. Incremental sync must remain selective and correct.
5. Parsers should add cheap facts first; no full compiler/typechecker dependency is required for the first version.

## Proposed Schema Direction

### Enrich `raw_calls`

Add fields that preserve AST-derived context:

```text
raw_calls
  caller_nid INTEGER NOT NULL
  callee_name TEXT NOT NULL
  callee_simple TEXT
  callee_scope TEXT
  call_kind TEXT               -- function, method, constructor, static_method, macro, unknown
  receiver_expr TEXT           -- ctx, cacher, this, TypeName, pkg, namespace
  receiver_type TEXT           -- best known type, if cheap to infer
  package_or_namespace TEXT    -- Go package, Java package, C++ namespace, Rust module-ish
  import_hint TEXT             -- Java import / Go package alias target, when available
  line INTEGER NOT NULL
  column INTEGER NOT NULL
```

Keep the existing fields for compatibility and indexing. New fields can be nullable at first.

### Add Resolution Metadata

Carry one `resolution_kind` column directly on `edges`:

```text
edges
  source_nid
  target_nid
  kind
  resolution_kind INTEGER NOT NULL DEFAULT 0   -- enum, see below
  PRIMARY KEY (source_nid, target_nid, kind)   -- unchanged, WITHOUT ROWID
```

`resolution_kind` is a small integer enum, not text, to stay consistent with the
0.0.4 interning/size work (the alternative — a `TEXT` label repeated across
~2.3M Linux edges — would undo it):

```text
0 name_only      30
1 same_file       60
2 namespace        80
3 package           80
4 receiver_type      90
5 exact               100
```

**Confidence is derived, not stored.** It is a pure function of
`resolution_kind` (the mapping above), so storing it as a second column would
duplicate data on every edge for no gain. Query code maps the enum to a
confidence number on read.

Why columns on `edges` rather than a side `edge_metadata` table:

- `.ochna/ochna.db` is a derived cache, rebuilt on schema-version bump — there
  is no precious DB to migrate, so the side-table "lower migration risk"
  argument does not apply here.
- *Every* edge carries a `resolution_kind`, so a side table saves no rows; it
  only duplicates the `(source_nid, target_nid, kind)` key per row and adds a
  join on the hot query path, fighting the recent size/latency work.
- Keeping the PK at `(source_nid, target_nid, kind)` is intentional: one edge
  per relation, holding its best `resolution_kind` — not multiple evidence rows.

### Add Symbol Ownership Indexes

The in-memory `SymbolIndex` should include:

```text
by_qualified_name
by_type_and_method
by_package_and_name
by_file_and_name
by_kind_and_name
```

For persisted lookup/debugging, consider a lightweight table later:

```text
symbol_owners
  node_nid
  owner_kind TEXT       -- class, struct, interface, package, namespace
  owner_name TEXT
  package_or_namespace TEXT
```

This table is optional for phase 1. The in-memory index is enough to prove the design.

## Language-Specific Context To Capture

### Java

High-value facts:

- package declaration;
- imports, including wildcard imports;
- class/interface/enum ownership;
- method invocation receiver expression;
- constructor type;
- `this.method()` and bare `method()` inside a class;
- superclass/interface names when cheap.

Resolution examples:

- `ReferenceCountUtil.safeRelease(msg)` -> static method on imported/qualified class.
- `promise.tryFailure(cause)` -> receiver expression `promise`; if method parameter type is `ChannelPromise`, prefer `ChannelPromise::tryFailure`.
- bare `releaseAndFailQueuedWrite(...)` inside subclass -> inherited/static helper on `AbstractTrafficShapingHandler` if class hierarchy is known; otherwise same class/package fallback.

Phase 1 does not need full generic typing. Method parameters, local variable declarations, fields, and `new Type(...)` assignments cover many practical cases.

### Go

High-value facts:

- package name;
- imports and aliases;
- selector expression receiver, e.g. `delegator.GetList`;
- receiver type for methods;
- local variables assigned from constructors when obvious;
- parameters and short declarations with explicit type.

Resolution examples:

- `cacher.watchCache.Add(...)` should not match every `Add` in Kubernetes. The raw call should preserve receiver chain `cacher.watchCache` and ideally type `watchCache`.
- `delegator.GetList(...)` should prefer `CacheDelegator::GetList` if `delegator := NewCacheDelegator(...)` is visible.
- `storage.ValidateListOptions(...)` should resolve through import/package alias rather than global simple name.

Phase 1 can implement receiver expression and package alias capture without solving every local type.

### C / C++

High-value facts:

- namespace/class parent already matters for C++;
- function pointer calls should be marked `indirect` instead of guessed;
- macro-like invocations should be marked `macro_or_function` or left unresolved when ambiguous;
- deleted-symbol explanations require a previous index or patch input, not only current AST.

For Linux, the biggest next win is not full C type resolution. It is better honesty:

- direct known function calls -> exact/name;
- macro-heavy calls -> lower confidence;
- removed APIs -> compare two indexes or use patch metadata.

### Rust / Zig

Keep current behavior, but align with the same confidence model:

- explicit `Type::method` or namespace path -> exact/namespace;
- bare method calls -> receiver/same-impl if known;
- fallback -> name-only.

## Resolver Algorithm

Replace the current single candidate selection with staged matching:

```text
for each raw_call:
  candidates = by simple name
  if none:
    unresolved

  stage 1: exact qualified hint
  stage 2: receiver static type + method
  stage 3: package/import/namespace + name
  stage 4: same caller namespace/type
  stage 5: same file
  stage 6: unique global name
  stage 7: ambiguous name-only
```

Each successful stage emits:

```text
edge(source, target, "calls", resolution_kind)   -- confidence derived on read
```

For stage 7, there are two policy options:

1. Emit no edge and record an unresolved/ambiguous ref.
2. Emit a low-confidence edge but mark it `name_only`.

Recommendation: emit an `ambiguous_refs` record for multi-candidate name-only cases, and only emit a `name_only` edge when the global candidate is unique. This makes default graph output more trustworthy.

## Query UX

Add filters without breaking existing commands:

```bash
ochna callers GetList --min-confidence 80   # opt-in filter
ochna node --symbol Foo --include-code --show-resolution
ochna explore watchCache --show-resolution
```

**Default behavior must not silently hide edges.** That would contradict the
"preserve, don't silently drop" contract above and regress existing `callers`
workflows that rely on broad results. Instead, the default **rank** edges
high-confidence-first and annotate each with its kind; `--min-confidence` is the
explicit opt-in for filtering. JSON output always includes the resolution kind
and its derived confidence:

```json
{
  "resolution_kind": "receiver_type",
  "confidence": 90
}
```

This is important for agents. They can decide when to trust an edge and when to inspect source directly — but nothing is hidden from them without asking.

## Migration Plan

### Phase 0: Baseline And Fixtures

Goal: make the current failure mode measurable.

Add fixtures for:

- Go: two unrelated types with `GetList`, `Add`, `Run`, plus one receiver-typed call.
- Java: two classes with `release`, `tryFailure`, `run`, plus an imported static/qualified call.
- C: one direct function, one macro-like invocation, one function pointer call.

Add corpus probes:

- Kubernetes: PR 139848 symbols.
- Netty: PR 16959 symbols.
- Linux: `strncpy` removal surface check.

Success criteria:

- tests demonstrate current noisy behavior before resolver changes;
- benchmark scripts can count high/low confidence edges after changes.

### Phase 1: RawCall Context Without Resolution Changes

Goal: enrich raw call records while preserving existing edge output.

Implement nullable raw-call fields:

- `call_kind`;
- `receiver_expr`;
- `receiver_type`;
- `package_or_namespace`;
- `import_hint`.

Populate only cheap facts:

- Java package/imports, receiver identifier, constructor type, parameter/local explicit types.
- Go package/import aliases and selector receiver text.
- C/C++ explicit namespaces and macro/indirect classification where obvious.

Success criteria:

- `make check` and `make test`;
- raw-call rows contain useful context on Netty/Kubernetes sample files;
- no edge-count parity requirement yet because resolver behavior is unchanged.

### Phase 2: Confidence-Aware Resolver

Goal: stop presenting weak guesses as strong graph facts.

Extend `SymbolIndex`:

- `by_qualified_name`;
- `by_owner_and_name`;
- `by_package_and_name`;
- keep existing `by_name`, `by_name_file`, `by_id`.

Implement staged resolution.

Add the `resolution_kind` column to `edges` and bump `SCHEMA_VERSION`. Because
the DB is a derived cache that rebuilds on a version bump, this needs no data
migration — old DBs are dropped and re-indexed from source. Confidence is
derived from `resolution_kind` on read, not stored.

Success criteria:

- Netty PR 16959 still resolves `releaseAndFailQueuedWrite` callers exactly.
- Kubernetes common-name callees drop from default high-confidence output.
- Unique global functions still resolve as before.
- Low-confidence/ambiguous refs are visible in JSON for audit.

### Phase 3: Query Output And Defaults

Goal: make improved data useful to humans and agents.

Add:

- `--min-confidence` (opt-in filter);
- `--show-resolution`.

Default command behavior:

- text output ranks high-confidence edges first and annotates each with its kind;
- JSON includes `resolution_kind` + derived `confidence`;
- no edges are hidden by default — name-only matches are ranked last, not dropped.

Success criteria:

- existing user workflows still work;
- agents can choose high-confidence-only exploration;
- Netty/Kubernetes examples are easier to explain from graph output.

### Phase 4: Incremental Sync Compatibility

Goal: preserve the hard-won selective re-resolution behavior.

Update selective invalidation to consider new raw-call fields:

- changed symbol name still invalidates by `callee_simple`;
- changed owner/type/package should invalidate receiver/type/package-indexed raw calls;
- ambiguous refs should be rechecked when new matching symbols appear.

Success criteria:

- selective sync edge set matches fresh rebuild for focused fixtures;
- existing parity-style test remains;
- giant-corpus validation uses `ochna status --json`, not huge file dumps.

### Phase 5: Optional Previous-Index / Patch Mode

Goal: explain deletion-heavy changes like Linux `strncpy` removal.

This is separate from call resolution and should not block phases 1-4.

Possible feature:

```bash
ochna diff --before .ochna/base.db --after .ochna/ochna.db
```

or a script-level workflow:

```bash
gh api repos/torvalds/linux/commits/<sha>
ochna search strncpy --json
```

Success criteria:

- removed symbols can be reported explicitly;
- current index stays simple and fast.

## Performance Considerations

The plan should not undo the 0.0.4 performance work.

Rules:

- intern repeated strings in memory during resolution;
- keep raw-call context nullable and indexed only where needed;
- avoid SQL lookups per call in the resolver;
- build all resolution maps once per run;
- preserve fresh-build bulk insert behavior;
- preserve selective incremental re-resolution.

Likely indexes:

```sql
CREATE INDEX idx_raw_calls_callee_simple ON raw_calls(callee_simple);
CREATE INDEX idx_raw_calls_receiver_type ON raw_calls(receiver_type);
CREATE INDEX idx_raw_calls_package ON raw_calls(package_or_namespace);
```

Add indexes only after measuring. `receiver_expr` may not need an index if it is only used in memory during a changed-file parse.

## Risk Register

| Risk | Mitigation |
| --- | --- |
| Schema change to `edges`/`raw_calls` | DB is a derived cache: bump `SCHEMA_VERSION`, drop + re-index from source — no in-place migration needed |
| `resolution_kind` bloats edge rows | Store as a small INTEGER enum, derive confidence on read — no second column |
| Resolver gets too clever and slow | Stage maps in memory; no per-call SQL |
| False confidence is worse than noisy edges | Prefer unresolved/ambiguous over low-confidence guessed edges |
| Java local type inference becomes a rabbit hole | Start with parameters, fields, explicit locals, constructor assignments |
| Go receiver inference becomes incomplete | Preserve receiver expression even when type is unknown |
| C macro/function ambiguity remains | Mark lower-confidence instead of pretending it is exact |
| Incremental sync misses incoming edges | Extend selective invalidation with raw-call context and parity tests |

## Recommended First Implementation Slice

The smallest high-value slice is Java + confidence metadata:

1. Add raw-call fields.
2. Populate Java package/import/receiver expression for method invocations.
3. Add `by_qualified_name` and `by_owner_and_name` to `SymbolIndex`.
4. Resolve:
   - exact qualified calls;
   - receiver type when parameter/local type is known;
   - same class;
   - unique global;
   - ambiguous as unresolved/ambiguous.
5. Validate on Netty PR 16959 and existing Java Spring route tests.

Why Java first:

- Netty showed the strongest immediate payoff.
- Java AST carries class/package/import structure cleanly.
- It is the user's most common ochna use case.
- Success is easy to explain and demonstrate before touching Go/C.

Second slice: Go selector receiver expression and package alias resolution.

Third slice: C/C++ confidence classification and deletion-aware workflow.

## Edge-Storage Decision

The candidate designs for where resolution metadata lives:

Option A (chosen): add a `resolution_kind` INTEGER enum directly on `edges`.

- no join on the hot query path;
- PK stays `(source_nid, target_nid, kind)` — one edge per relation, holding its
  best resolution kind;
- schema change is cheap because the DB rebuilds on a version bump.

Option B (rejected): a side `edge_metadata` table.

- its only real advantage — "lower migration risk" — does not apply: the DB is a
  derived cache with no data to migrate;
- every edge needs a `resolution_kind`, so it saves no rows; it duplicates the
  3-column key per row and adds a join, fighting the 0.0.4 size/latency work.

Decision: **Option A**, with `resolution_kind` as a small INTEGER enum and
confidence derived from it on read (never stored).

## Final Argument

Ochna should not try to become `gopls`, `javac`, `clangd`, and `rust-analyzer` in one binary. Its advantage is being fast, local, structural, and language-broad. The right next step is not full semantic compilation. It is a better data contract:

- preserve cheap context at parse time;
- resolve in stages;
- label certainty;
- expose uncertainty to users and agents.

That makes ochna more honest and more useful. It turns graph output from "a list of plausible edges" into "a ranked map of evidence." For agent workflows, that distinction matters.