Skip to content

Commit a2e5521

Browse files
authored
Merge pull request #204 from aaronsb/docs/adr-203-vault-read-fidelity
docs(adr): ADR-203 — faithful-by-default content reads; concise tool results (#133)
2 parents 2decbc4 + d4c005f commit a2e5521

2 files changed

Lines changed: 158 additions & 0 deletions

File tree

docs/architecture/INDEX.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ _MCP tool design, semantic operations, graph operations, formatters_
3636
| [ADR-200](./tools/ADR-200-close-cli-parity-gaps-add-daily-notes-tasks-templates-and-properties-operations.md) | Close CLI parity gaps — add daily notes, tasks, templates, and properties operations | Draft |
3737
| [ADR-201](./tools/ADR-201-replace-new-function-with-a-sandboxed-expression-evaluator-for-bases.md) | Replace new Function with a sandboxed expression evaluator for Bases | Accepted |
3838
| [ADR-202](./tools/ADR-202-split-semantic-router-monolith-into-per-operation-modules.md) | Split semantic router monolith into per-operation modules | Accepted |
39+
| [ADR-203](./tools/ADR-203-faithful-by-default-content-reads-concise-by-default-tool-results.md) | Faithful-by-default content reads; concise-by-default tool results | Accepted |
3940

4041
## Delivery
4142
_Build, release, versioning, community plugin submission_
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
---
2+
status: Accepted
3+
date: 2026-05-19
4+
deciders:
5+
- aaronsb
6+
- claude
7+
related: []
8+
---
9+
10+
# ADR-203: Faithful-by-default content reads; concise-by-default tool results
11+
12+
## Context
13+
14+
#133 argues that because MCP is a machine-to-machine protocol, the default
15+
tool response should be structured/source data, and that forcing every
16+
client to pass `raw: true` everywhere pushes presentation concerns into
17+
every consumer. Its proposed remedy is a **global flip**: make `raw: true`
18+
behavior the default, opt into rendered output with `render: true`.
19+
20+
Empirical inspection of 0.11.29 (live calls + the response code paths in
21+
`src/tools/semantic-tools.ts` and `src/utils/file-reader.ts`) shows the
22+
real situation is sharper than the issue states, and the global flip is the
23+
wrong lever:
24+
25+
1. **The default content read is *lossy*, not merely "rendered."** Default
26+
`vault.read` does **not** return the file. `file-reader.ts` returns full
27+
content only when `returnFullFile` is set; otherwise it falls through to
28+
fragment retrieval, and the presentation facade then renders fragments
29+
with **newlines/whitespace flattened to spaces**. Measured on a tiny
30+
structured file, the default read collapsed frontmatter, headings, a
31+
fenced Python block (indentation lost) and a trailing tab onto a single
32+
line. An agent that derives an `edit.window` `oldText` from a default
33+
read gets text that **cannot match the file on disk**. This is a
34+
correctness defect in the read→edit round-trip, and a likely contributor
35+
to the `edit.window` fuzzy-match fragility the project has been
36+
compensating for elsewhere.
37+
38+
2. **The faithful path is needlessly ~2× bloated.** With
39+
`returnFullFile: true, raw: true`, the complete file body is embedded
40+
**twice**`result.content.content` and `result.metadata.content` are
41+
byte-identical — on top of `workflow`/`suggested_next`/`context`
42+
boilerplate. Roughly half of the "raw" read payload is pure duplication.
43+
44+
3. **`returnFullFile` without `raw` is broken.** The presentation formatter
45+
throws on the full-file shape and emits a literal
46+
`_Formatter error, showing raw data:_` JSON fallback. There is no working
47+
concise full-file formatted view today.
48+
49+
4. **A global flip would make tool *results* more verbose.** The default
50+
formatted path (`formatResponse`) is a deliberately condensed,
51+
token-efficient summary for action/result tools (create/edit/move/
52+
search). `raw: true` produces the full pretty-printed envelope —
53+
*more* tokens. Flipping the global default to `raw` therefore works
54+
directly against response economy for the common case (#133's own stated
55+
value — "less verbose" — is served by the current result formatting, not
56+
by the flip).
57+
58+
The intent behind #133 is correct (a machine protocol's default output
59+
must be machine-usable). The instinct that the project also wants — concise
60+
formatted output for tool *results*, but unaltered content for *editing*
61+
is also correct. The two are reconciled by separating **content reads**
62+
from **action/result rendering** rather than by one blunt format switch.
63+
64+
## Decision
65+
66+
Adopt a **two-bucket response contract**. This ADR is the decision of
67+
record; implementation is tracked separately and is *not* part of this ADR.
68+
69+
1. **Content reads are faithful by default.** `vault.read` returns the
70+
**complete, verbatim file source** by default — byte-exact as stored by
71+
Obsidian (newlines, indentation, trailing whitespace, tabs, fenced
72+
blocks, frontmatter delimiters preserved). Fragmentation / summarization
73+
becomes an **explicit opt-in** for large-file context savings via the
74+
*existing* knobs (`query`, `strategy`, `maxFragments`); `returnFullFile`
75+
is no longer required to get the truth and is retired or demoted to a
76+
no-op alias. The read→edit round-trip is correct by construction.
77+
78+
2. **Action/result outputs stay concise-formatted by default.** Tools whose
79+
result is a status / confirmation / list (create, update, delete, move,
80+
rename, copy, search, graph.*, edit.*) keep the condensed
81+
`formatResponse` presentation as the default — the token-efficient
82+
ergonomics are intentional and retained. `raw: true` remains the
83+
explicit opt-in for the full structured envelope on these. #133's
84+
global-flip proposal is **declined**; its intent is satisfied by
85+
bucket 1.
86+
87+
3. **Stop double-encoding the body.** The structured read envelope must not
88+
embed the file content twice. `metadata` carries metadata (path,
89+
wordCount, tags, frontmatter, warning) — not a second copy of the body.
90+
Orthogonal to the default change and applies regardless.
91+
92+
4. **The full-source path must not depend on the broken formatter.**
93+
Verbatim content is returned through a path that does not throw the
94+
`_Formatter error_` fallback; the failing `returnFullFile` formatted
95+
branch is fixed or removed as part of (1).
96+
97+
Backward compatibility: (1) changes the default shape/size of `vault.read`
98+
for clients that currently rely on the fragmented/flattened default. This
99+
is a deliberate breaking change to fix a correctness bug; it ships with a
100+
clear changelog entry and a documented opt-in (`strategy`/`maxFragments`)
101+
for the previous context-saving behaviour. A prerelease + functional
102+
verification of a real read→`edit.window` round-trip gates the rollout.
103+
104+
#133 is **reframed, not closed by fiat**: it remains the user-facing
105+
tracking issue for bucket 1; this ADR records the agreed direction and
106+
explicitly supersedes its specific "global `raw` flip" remedy.
107+
108+
## Consequences
109+
110+
### Positive
111+
112+
- Read→edit round-trips become correct by default: `edit.window`/`patch`
113+
`oldText` derived from a read matches disk. Removes a structural source
114+
of the fuzzy-match fragility.
115+
- Honours #133's intent (machine-usable default) without the verbosity
116+
regression a global flip would cause for action/result tools.
117+
- De-duplicating the body roughly halves structured-read payloads — a free
118+
win for every `raw` consumer, independent of the default change.
119+
- Fixes a latent crash (`returnFullFile` formatter throw).
120+
121+
### Negative
122+
123+
- Breaking change for clients depending on the current fragmented/flattened
124+
default `vault.read` (smaller, lossy payloads). Mitigated by changelog,
125+
the existing `strategy`/`maxFragments` opt-in for context savings, and a
126+
gated prerelease test.
127+
- Large files now return full content by default → higher per-call context
128+
cost for the read-everything pattern. Mitigated: fragmentation is still
129+
one explicit parameter away, and a `wordCount`/size `warning` is retained
130+
to nudge large-file callers toward it.
131+
132+
### Neutral
133+
134+
- Two-bucket contract should be documented in the README and tool
135+
descriptions so the default semantics are discoverable.
136+
- `returnFullFile` becomes redundant; retire or alias with a deprecation
137+
note.
138+
- No change to `IObsidianAPI` or the MCP tool surface beyond the default
139+
response shape/size of `vault.read` and the de-duplicated envelope.
140+
141+
## Alternatives Considered
142+
143+
- **#133 as written — global flip to `raw` default, `render:true` opt-in.**
144+
Declined: makes every action/result return more verbose (against the
145+
project's "concise tool results" goal), and still wouldn't fix the
146+
newline-flattening fidelity bug — the default read is lossy *before* it
147+
reaches the format switch.
148+
- **Status quo (require `returnFullFile:true` + `raw:true` for fidelity).**
149+
Rejected: read→edit correctness must not depend on two non-obvious
150+
opt-ins; this is the exact friction #133 documents and the latent cause
151+
of silent edit mismatches.
152+
- **Keep fragmented default but stop flattening newlines.** Rejected as
153+
insufficient: a fragment is still a *subset* of the file; an agent
154+
editing needs the whole, exact source, not faithfully-formatted excerpts.
155+
- **New dedicated `vault.source` tool, leave `read` lossy.** Rejected:
156+
splits the surface and leaves the obvious tool (`read`) a footgun; the
157+
principled fix is making the obvious tool correct by default.

0 commit comments

Comments
 (0)