# v0.3 Structural Review Contract
This document freezes the public contract for the v0.3 structural-review
epic. It is a design-and-acceptance contract: commands described here are
**not** available until their implementation tasks land. A later task may add
fields, but may not rename, remove, or change the meaning of a field or
human-visible section specified here without a new contract version.
## Scope and compatibility
v0.3 turns the existing local index into a bounded, read-only review surface.
It answers: is this index safe to use; what did the Git change touch; what
graph impact follows from those symbols; which tests support that conclusion;
and which references remain unresolved. It does not compile the project,
infer runtime dispatch, fetch network state, edit Git state, or execute a test
suite.
Existing `search`, `callers`, `callees`, `node`, `explore`, `status`, and
`howto` behaviour remains compatible. `rg` remains the baseline for literal
text/regular-expression searches, comments, generated text, and any result
that is not a parsed symbol or structural relationship. Ochna is the baseline
for symbol identity, callers/callees, changed-symbol mapping, and bounded
graph traversal. A review must state when an `rg` result is evidence rather
than a graph fact.
All new commands accept global `--workspace`/`-C`, `--json`, and `--no-tests`.
Diagnostics go only to stderr. JSON stdout is exactly one UTF-8 JSON document;
success, empty results, and partial results are never represented by prose on
stdout.
## Shared JSON rules
Every new command emits this envelope in JSON mode:
```json
{
"contract_version": "0.3",
"command": "doctor",
"ok": true,
"data": {},
"warnings": [],
"truncated": false,
"next_action": "none"
}
```
- `contract_version` is the literal string `"0.3"`.
- `command` is one of `doctor`, `unresolved`, `tests-for`, `impact`, or
`diff`.
- `ok` means the command completed with evidence adequate for its declared
scope. It does **not** mean the reviewed change is correct.
- `data` is command-specific and always present.
- `warnings` is an ordered array of objects with `code` and `message`.
- `truncated` is true whenever any collection reached its command budget.
- `next_action` is a stable action token: `none`, `ochna init`, `ochna sync`,
`provide a base revision`, or `narrow the query`.
Each symbol object has `id`, `name`, `qualified_name`, `kind`, `file_path`,
`start_line`, `end_line`, and `is_test`. Relationship-bearing objects also
have `relationship`, `resolution_kind`, and `confidence`; `confidence` is an
integer 0 through 100. An unresolved target never masquerades as a symbol:
its target is `null` and its reason is explicit.
The human form is a compact, deterministic rendering of the same result:
title line, zero or more labelled sections, then `Warnings:` when needed.
Paths are workspace-relative, symbols use `qualified_name` when available,
and source text is never printed by these commands. Empty sections say `none`.
## Command contracts
### `ochna doctor`
`doctor` is the review preflight and supersedes no existing `status` output.
It inspects the current workspace and index without modifying either. Human
output has `Index:`, `Schema:`, `Git baseline:`, `Freshness:`, `Graph trust:`,
`Resolution tiers:`, `Unresolved:`, `Collision-prone names:`,
`Low-quality locations:`, `Unsupported extensions:`, and `Action:` lines in
that order.
`data` is:
```json
{
"index": {"present": true, "path": ".ochna/ochna.db"},
"schema": {"expected": 6, "actual": 6, "match": true},
"git": {"head": "<40-hex-or-null>", "baseline": "<40-hex-or-null>", "dirty": false},
"freshness": "fresh",
"graph_quality": {
"trust_verdict": "trusted",
"resolution_tiers": [{"kind": "receiver_type", "confidence": 90, "edges": 12}],
"unresolved": {"missing_target": 0, "ambiguous_target": 0, "total": 0},
"collision_prone_names": [{"name": "run", "definitions": 18, "low_confidence_edges": 42}],
"low_quality_locations": [{"path": "pkg/controller", "reason": "high_unresolved_rate", "rate": 0.2}],
"unsupported_extensions": [{"extension": ".py", "files": 3}]
}
}
```
`freshness` is `fresh`, `stale`, `missing`, or `incompatible`. A missing or
incompatible index is `ok: false`; a stale but readable index is `ok: false`
with `next_action: "ochna sync"`. Freshness compares each indexed source file
to its indexed content hash, so unrelated dirty files are warnings rather than
failures; a changed or deleted indexed source is stale. `trust_verdict`
is `trusted`, `degraded`, or `unusable`; it is a machine-readable summary of
schema/freshness health and graph-quality diagnostics, not a claim of semantic
correctness. `unusable` always sets `ok: false` and exits nonzero. `degraded`
may return data but exits nonzero when the requested review relies on a stale
or incompatible index. Collision-prone names are names with multiple candidate
definitions or at least one `name_only`/`same_file` edge. Low-quality locations
are files and their parent packages with at least five raw call sites and an
unresolved/ambiguous rate of at least 20%; rows sort by path. Collision rows
sort by name, resolution tiers sort by descending resolution kind, and
unsupported extensions sort lexically. Unsupported extensions are known source
suffixes not parsed by Ochna (`.py`, `.js`, `.jsx`, `.ts`, `.tsx`, `.rb`, `.php`,
`.cs`, `.kt`, `.kts`, `.swift`, `.scala`, `.sh`, `.lua`) outside
ignored/library directories. `expected` is the compiled `SCHEMA_VERSION` (6 at
this contract revision), never a copied literal in implementation tests.
`trusted` additionally requires no low-quality location and no unsupported
source extension. Either diagnostic makes the verdict `degraded`; schema
absence or mismatch is `unusable`.
The command must report zero-count diagnostic arrays explicitly rather than
omitting them.
### `ochna unresolved [--in <path-prefix>] [--limit <n>]`
Lists call sites that could not be represented as trusted edges. Default
`--limit` is 50, maximum accepted value is 200. Rows sort by file path, line,
column, then specifier. Human output begins `Unresolved references (<count>):`.
`data` is `{ "references": [...] }`; each reference contains `source`,
`specifier`, `kind`, `file_path`, `line`, `column`, and `reason`. `reason` is
one of `missing_target`, `ambiguous_target`, `macro_or_function`, or
`indirect_call`. `--in` filters the source file path. A zero-result query is
successful and returns an empty array.
### `ochna tests-for <symbol> [--in <path-prefix>] [--limit <n>]`
Returns test symbols with an explicit structural path to the requested
production symbol. Default `--limit` is 30, maximum 100. It never claims a
test proves runtime behaviour. Human output begins `Tests for <symbol>:` and
prints the test symbol, relationship path, and confidence.
`data` is `{ "target": <symbol-or-null>, "tests": [...] }`. Each test item
contains `test`, `path`, `confidence`, and `evidence`. `path` is a non-empty
array of relationship objects using only `calls`, `tests`, `contains`, or
`references`. `evidence` is exactly one of `direct_call`,
`same_module_candidate`, or `name_heuristic`. A `direct_call` contains a graph
path and may be described as structural evidence; a `same_module_candidate`
only establishes colocation; a `name_heuristic` only establishes a naming
candidate and may not score above 60. Neither candidate/heuristic result is
proof of coverage or runtime behaviour. The default result is
confidence-descending; lower confidence is retained unless an explicit filter
requests otherwise. `--no-tests` explicitly suppresses test results: it
returns the resolved target with an empty `tests` array and does not claim
that no tests exist.
### `ochna impact <symbol> [--depth <n>] [--direction <callers|callees|both>] [--min-confidence <n>] [--limit <n>]`
Traverses trusted graph relationships from one resolved symbol. Defaults are
`--depth 2`, `--direction both`, and `--limit 50`; maxima are depth 5 and 200
reported nodes. A traversal must stop at the result budget and set
`truncated: true`; it must not silently explore an unbounded common-name fanout.
`data` is `{ "root": <symbol-or-null>, "direction": "both", "depth": 2,
"min_confidence": 30, "nodes": [...], "edges": [...], "paths": [...],
"affected_tests": [...], "unresolved_boundaries": [...] }`. Every edge
carries relationship, resolution kind, and confidence. `paths` preserves every
distinct root-to-node traversal path as an ordered edge array; an edge list is
not a substitute for paths. `affected_tests` contains test symbols reached by
those paths and their path IDs. `unresolved_boundaries` contains unresolved or
ambiguous call sites encountered at a frontier, including source, specifier,
reason, and the path that reached the source. Default traversal includes only
confidence 30 or higher, the lowest stored unique-name resolution tier;
`--min-confidence` is explicit, appears in output, and each edge/path retains
its actual confidence so a low-confidence result is never portrayed as stronger;
and a lower threshold labels every affected result. Human output has `Impact
for <symbol>:`, `Nodes:`, `Paths:`, `Affected tests:`, `Unresolved boundaries:`,
and `Edges:` sections. Result and edge budgets cap all sections and set
`truncated: true`; truncation must identify which path frontier was not walked.
### `ochna diff (--base <git-revision> [--head <git-revision>] | --files <path>...) [--limit <n>]`
Maps a Git range or explicit changed paths onto reviewed-side indexed symbols.
`--base` may be a commit, tag, or ref accepted by local Git; omitting `--head`
uses the current worktree, including staged and unstaged changes. `--base` plus
`--head` compares two named revisions. `--files` accepts one or more
workspace-relative paths and maps their current indexed symbols without asking
Git for a range. The two selectors are mutually exclusive. Default `--limit`
is 100, maximum 500 changed symbols, edges, and newly unresolved callers. Git
ranges use disposable local historical indexes; base-to-worktree checks the
current index first.
`data` is:
```json
{
"base": "<resolved-40-hex>",
"head": "<resolved-40-hex-or-null>",
"files": [{"path": "src/lib.rs", "status": "modified"}],
"symbols": [{"symbol": {}, "change": "modified", "hunks": [12]}],
"edges": [{"source": {}, "target": {}, "relationship": "calls", "change": "added"}],
"newly_unresolved_callers": [{"source": {}, "specifier": "missing_renderer", "reason": "missing_target"}],
"unmapped_hunks": [{"path": "README.md", "line": 8}]
}
```
`status` is `added`, `modified`, `deleted`, or `renamed`; symbol and edge
`change` is `added`, `modified`, `removed`, or `unknown`. For a Git range,
Ochna indexes the base revision in a bounded temporary archive workspace and
compares it with the reviewed side (a second temporary archive when `--head`
is named). It reports real removed symbols and edges, never guesses them from
a same-named current symbol, and removes both temporary workspaces before
returning. `data.historical_snapshot` reports aggregate and per-side temporary
storage and elapsed time; its `cleanup` value is `removed` only after explicit
successful cleanup. If a requested object is absent (as in
a shallow clone), JSON returns `ok: false` with
`base_revision_unavailable`; if archive indexing cannot proceed, the command
preserves task 4's current-index mapping with a
`historical_snapshot_unavailable` warning. A named-head snapshot failure is
`ok: false` and never falls back to the worktree graph.
`newly_unresolved_callers` records call sites that became missing or ambiguous
on the reviewed side. Base-to-worktree mode is `ok: false` when the current
index is stale. Symbol, edge, and unresolved collections each use the diff
limit and set `truncated: true`. Human output sections are
`Changed files:`, `Changed symbols:`, `Changed edges:`, `Newly unresolved
callers:`, and `Unmapped hunks:`.
Linux acceptance remains a manual, read-only benchmark because the bundled
`clones/linux` checkout may be shallow. When both revisions are locally
available, archive the checkout into a temporary directory, run `ochna init`
there, then run `ochna diff --base <parent> --head <merge> --json`. Otherwise,
run the installed CLI against the existing index only to capture the structured
`base_revision_unavailable` result; do not fetch, initialize, or otherwise
modify the submodule.
## Extensible relationship kinds
All graph relationship values are stable lower-case tokens. v0.3 implements
only those its parsers can evidence, but its JSON accepts these forward
compatible kinds: `calls`, `tests`, `contains`, `references`, `route_handler`,
`injected_into`, `configuration_binds`, `publishes_event`, `consumes_event`,
`feign_calls`, and `grpc_calls`. Spring routes, dependency injection,
configuration binding, and events must carry the source and target symbol
coordinates when both are indexed; otherwise their missing endpoint is `null`
with a warning. Selected Feign and gRPC relationships must identify their
declared client/service/method coordinates. Every such edge carries
`resolution_kind` and `confidence`, so framework conventions are never
presented as compiler-proven calls. `framework_annotation` (85),
`framework_declaration` (80), and `framework_convention` (70) are parser
evidence tiers; none is equivalent to `exact`. `injected_into` points from a
dependency type to its consuming component, `consumes_event` points from event
type to listener, and route/configuration/declared endpoint nodes are
synthetic indexed symbols anchored at their source annotation or method.
## Read-only MCP adapter (deferred)
**Status: deferred from v0.3 (2026-07-29).** The installed CLI plus its
stable JSON contracts already cover the current read-only automation surface;
no concrete MCP-only consumer, discovery/allow-list requirement, or
persistent-service need has been demonstrated. This section remains frozen
design material for a future adapter and is not implemented in v0.3. Reopen it
only against a concrete MCP-only requirement. See
[ADR-0005](adr/0005-v0.3-contract-freeze-and-mcp-adapter-deferral.md) for the
decision record and [issue #9](https://github.com/azusachino/ochna/issues/9).
The MCP adapter exposes no write, shell, network, indexing, sync, or Git
mutation tool. It calls the installed `ochna` binary against an explicit
workspace and returns the corresponding JSON envelope without reformatting it.
| `ochna_status` | `status --json` | `workspace` |
| `ochna_search` | `search --json` | `workspace`, `query`, optional `limit` |
| `ochna_node` | `node --json` | `workspace`, exactly one supported node selector |
| `ochna_callers` | `callers --json` | `workspace`, `symbol`, optional confidence/scope flags |
| `ochna_callees` | `callees --json` | `workspace`, `symbol`, optional confidence/scope flags |
| `ochna_tests_for` | `tests-for --json` | `workspace`, `symbol`, optional `in`, `limit` |
| `ochna_doctor` | `doctor --json` | `workspace` |
| `ochna_impact` | `impact --json` | `workspace`, `symbol`, optional `depth`, `direction`, `min-confidence` (default 30), `limit` |
| `ochna_diff` | `diff --json` | `workspace`, one diff selector, optional `limit` |
The adapter must reject a workspace outside its configured allow-list and
arguments above the CLI maxima. Its tool descriptions must say that results
are static structural evidence, not test execution or semantic compilation.
These are the adapter's exact tools; it does not expose `unresolved` directly
because `doctor`, `diff`, and `impact` surface unresolved boundaries in their
stable review contracts.
## Fixture and acceptance user stories
`scripts/verify_clis.py` covers these stories directly: each scenario builds
its own temporary Git repository inline (baseline commit, then a mutation),
rather than relying on a single committed fixture tree. This keeps the
acceptance data next to the assertions that consume it.
1. A reviewer runs `doctor` before using graph evidence. A fresh fixture index
reports `ok: true`; modifying `after/src/lib.rs` without syncing reports
`freshness: "stale"` and `next_action: "ochna sync"`.
2. A reviewer runs `diff --base HEAD` after applying `after`. The output maps
`render` and `render_page` to changed symbols, records the deleted
`legacy_render` as a removed symbol requiring history, records the removed
`render -> legacy_render` edge where a historical index is available, emits
the newly unresolved `render_page -> missing_renderer` caller, and leaves a
prose-only README hunk unmapped.
3. A reviewer runs `unresolved`. The `missing_renderer()` call appears once
with `reason: "missing_target"`, source `src/lib.rs`, and a stable line.
4. A reviewer runs `tests-for render`. The test
`tests/render_tests.rs::render_page_uses_render` is returned with a
direct-call evidence path. A same-module candidate and a name heuristic
remain separately labelled candidates, never proof of correctness.
5. A reviewer runs `impact render --direction callers --depth 2`. It reaches
`render_page` and the test under the default bounded result budget, retains
both root-to-result paths, and reports `missing_renderer` as an unresolved
boundary rather than traversing through it.
6. A reviewer searches a comment or literal with `rg`, and uses `ochna diff`
for changed-symbol attribution. Acceptance fails if Ochna claims the `rg`
hit is a graph relationship without structural evidence.
## Corpus benchmarks and budgets
The acceptance corpora are pinned by the recorded shallow-submodule commits;
tests must use exactly these revisions or state that the corpus is unavailable.
Enforced by `make case-sim` (`scripts/case_simulation.py`), which also
smoke-checks every other test giant (see `AGENTS.md`'s "Test giants" section).
| Netty | `ec4efdbbeebf024b64e0fb782184989835c9ab92` | PR 16959 queued-write cleanup | `releaseAndFailQueuedWrite` maps to its three `handlerRemoved` callers and the named regression test |
| Kubernetes | `b58546d7b34d0217171dd0d36a6e60c2eb603a77` | PR 139848 watch-cache tests | changed test symbols map to their files; common `GetList` traversal obeys confidence and output budgets |
| Linux | `1a3746ccbb0a97bed3c06ccde6b880013b1dddc1` | `strncpy` removal | current tree confirms absence; deleted API report is explicitly historical, never guessed from prefix matches |
Benchmark runs record machine, binary commit, corpus pin, wall time, and the
JSON `truncated` value. They do not impose a cross-machine time ceiling. They
must pass these output ceilings: doctor 1 record; unresolved 200 references;
tests-for 100 tests; impact 200 nodes/400 edges; diff 500 symbols/500 files.
The default ceilings are the smaller command defaults above. Any result that
hits a ceiling is a partial answer and must provide the prescribed narrowing
action rather than printing more output.
## Verification gate for later tasks
Each implementing task must add fixture-backed assertions for its command,
run `make check`, `make test`, and `make verify-clis`, and exercise the
installed global `ochna` binary for the applicable fixture user story. Corpus
benchmarks are a separate acceptance gate: skip only a missing pinned checkout,
record that skip, and never initialize, update, or modify a user-owned clone as
part of verification.