Commit graph

592 commits

Author SHA1 Message Date
Aaron Bockelie
ef97fcd1d5 docs: exempt security fixes from the 7-day supply-chain hold
The blanket 7-day hold delayed exactly the vuln patches Obsidian's plugin
review and npm audit flag — leaving us knowingly exposed to a named CVE to
guard against a hypothetical malicious republish. Split the rule: routine
version bumps still age 7 days; security fixes (dependabot security PRs,
npm audit advisories, Obsidian-review dependency warnings) land immediately
once green.
2026-06-09 11:31:06 -05:00
Aaron Bockelie
dc6f476856 chore: Bump version to 0.11.33 2026-06-09 11:16:22 -05:00
Aaron Bockelie
fad7adb1cc
Merge pull request #227 from aaronsb/fix/226-window-timers-globalthis
fix: pass Obsidian source-code review — window timers + globalThis (closes #226)
2026-06-09 11:15:18 -05:00
Aaron Bockelie
c65fcf5d28 fix(lint): use window timers + window.console; drop forbidden eslint-disables (closes #226)
Obsidian's source-code review forbids disabling obsidianmd/prefer-window-timers
and obsidianmd/no-global-this (and flags undescribed directive comments). The
0.11.32 review failed on five such disables.

- router.ts, obsidian-api.ts, session-manager.ts: global set/clearInterval and
  setTimeout → window.* equivalents.
- debug.ts: drop the globalThis console fallback → window.console.
- tests/setup.ts: alias window to globalThis in the node test env so the
  window.* timers/console resolve under Jest (Obsidian always provides window
  at runtime).
- session-manager cleanupInterval typed number (window.setInterval's return).

All five eslint-disable directives removed. Lint clean, build green, 336 tests pass.
2026-06-09 11:13:48 -05:00
Aaron Bockelie
882a10c332
Merge pull request #225 from aaronsb/chore/217-lint-debt
chore: adopt dev-dependency bump + clear lint debt (supersedes #217)
2026-06-09 11:10:03 -05:00
Aaron Bockelie
7276d7dd21 fix(lint): clear debt surfaced by typescript-eslint 8.60 + obsidian 1.13.0
The dev-dependency bump tightened lint and exposed pre-existing debt.
Resolved without blanket rule suppression:

- no-unnecessary-type-assertion (15): removed redundant assertions
  (eslint --fix) across formatters, mcp-server, security, utils, validation.
- no-base-to-string (6): the removed assertions un-masked String(unknown)
  calls. All were already guarded (object→JSON.stringify, primitive→String),
  so not bugs — narrowed via a typed primitive local, which also satisfies
  no-unnecessary-type-assertion (whose receiver String() accepts anything).
- no-deprecated (15): obsidian 1.13.0 deprecates PluginSettingTab.display()
  in favor of getSettingDefinitions(). Kept display() as the supported
  cross-version fallback and routed internal re-renders through a new
  render() method, so no deprecated symbol is referenced and no future
  deprecation in main.ts is masked. Full declarative migration: #224.
- no-deprecated (1, ButtonComponent.setWarning): the replacement
  setDestructive() requires 1.13.0 > our minAppVersion 1.6.6, so it's kept
  with one scoped eslint-disable + comment. Revisit with #224.
- no-unused-vars (2): dropped now-unused http type imports in mcp-server.
- prefer-active-doc (5): document → activeDocument for popout-window
  compatibility (main settings DOM queries, image-handler canvas).

Lint clean, build green, 336 tests pass.
2026-06-09 11:06:16 -05:00
Aaron Bockelie
ac8946aa95 chore(deps-dev): bump development-dependencies group
Supersedes #217 (dependabot). Brings the dev toolchain current:
@types/node 25.3→25.9, @typescript-eslint/utils + typescript-eslint
8.56→8.60, esbuild 0.27→0.28, globals 17.3→17.6, jest 30.2→30.4,
jiti 2.6→2.7, obsidian 1.12.2→1.13.0, ts-jest 29.1→29.4.

The typescript-eslint 8.60 and obsidian 1.13.0 type bumps surface new
lint findings; cleared in the follow-up commit.
2026-06-09 11:06:05 -05:00
Aaron Bockelie
b8c7f53885
Merge pull request #223 from aaronsb/docs/readme-install-reorg
docs(readme): lead with install + emoji .mcpb→Claude Desktop banner
2026-06-09 10:53:21 -05:00
Aaron Bockelie
bf6e29ae73
Merge pull request #222 from fa1k3/fix/graph-ignore-exclusion
fix(graph): honor MCP ignore exclusions in graph traversal
2026-06-09 10:53:18 -05:00
Aaron Bockelie
cda1b1b4a7 fix(graph): count filtered seed set in root-traverse message
The '/' traverse summary reported Math.min(10, getFiles().length) using the
unfiltered file count, while the traversal actually seeds from the
.mcpignore-filtered set (graph-traversal.ts). With exclusions enabled this
overcounted and leaked that hidden files exist via an inflated number. Count
the same filtered set the seed logic uses.
2026-06-09 10:46:00 -05:00
Aaron Bockelie
a09e667b33 docs(readme): lead with install + emoji .mcpb→Claude Desktop banner
Move Quick Start above the conceptual sections so install is the first
thing readers hit, and relocate 'Why Semantic MCP?' down to before Core
Tools. Add an inline-emoji hero banner (box → robot) for the .mcpb
drag-into-Claude-Desktop flow — glyphs render on the Obsidian listing
where embedded images get stripped, making the one-click path obvious
to non-power-users.
2026-06-08 16:09:35 -05:00
fa1k3
42ac8b361e fix(graph): honor MCP ignore exclusions in graph traversal
Graph operations (neighbors/backlinks/forwardlinks/traverse/path/
statistics) read metadataCache.resolvedLinks and vault.getFiles()
directly and never consulted the MCPIgnoreManager. With path
exclusions enabled, .mcpignore-ignored paths surfaced as neighbors/
edges of non-ignored queries, and querying an ignored note directly
disclosed its relationships.

- GraphSearchTool rejects an excluded query root (treated as
  "File not found", matching ObsidianAPI's read-path behavior).
- GraphTraversal filters excluded paths out of the link primitives and
  every file enumeration (neighbors, root traversal, tag connections,
  vault statistics, all-nodes).
- Add ObsidianAPI.getIgnoreManager() accessor + regression tests.

No behavior change when path exclusions are disabled.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-08 14:50:14 +02:00
Aaron Bockelie
7b6dd03367 chore: Bump version to 0.11.32 2026-06-05 17:42:50 -05:00
Aaron Bockelie
c0735f9c13
Merge pull request #219 from aaronsb/fix/216-dataview-query-status-formatter
fix(dataview): unwrap query() Result monad + bridge status shape (closes #216)
2026-06-05 17:42:23 -05:00
Aaron Bockelie
913c58e6aa fix(dataview): unwrap query() Result monad + bridge status shape (closes #216)
Dataview's `query()` resolves to a `Result` monad —
`{ successful, value: { type, values, headers }, error }` — where
`value.values` is a plain array. The tool modelled the return as a flat
`{ type, values }` object with a wrapped `DataArray`, so:

- `formatQueryResult()` switched on `result.type` (undefined → `result.value.type`),
  collapsing every query to the `unknown` branch → `# Dataview: UNKNOWN`.
- it called `result.values?.array()` (undefined → `result.value.values`,
  already a plain array) → `[]` → "No results found".

`dataview.status` had a separate shape mismatch: the detector returns
`{ installed, enabled, apiReady, version }` but `formatDataviewStatus`
reads `available`, so it always rendered "not available".

Fixes:
- Re-model `DataviewQueryResult` as the real monad; read payload from `.value`.
- Add `toPlainArray()` helper (accepts plain arrays and wrapped DataArrays).
- Add a `dataview.status` normalizer mapping apiReady → available.
- Correct the test mock, which encoded the same wrong flat+wrapped shape —
  the reason CI stayed green while real Dataview v0.5.68 failed.
- Add formatted-output regression tests through formatResponse() for
  query (LIST/TABLE) and status.
2026-06-05 17:37:21 -05:00
Aaron Bockelie
b402db8e59
Merge pull request #194 from aaronsb/dependabot/github_actions/actions/attest-build-provenance-4
ci: Bump actions/attest-build-provenance from 2 to 4
2026-06-05 17:07:27 -05:00
Aaron Bockelie
9830341804
Merge pull request #209 from aaronsb/dependabot/npm_and_yarn/qs-6.15.2
chore(deps): Bump qs from 6.14.2 to 6.15.2
2026-06-05 17:07:24 -05:00
Aaron Bockelie
15f39e262e
Merge pull request #195 from aaronsb/dependabot/npm_and_yarn/production-dependencies-d4197686fa
chore(deps): bump yaml from 2.8.4 to 2.9.0 in the production-dependencies group across 1 directory
2026-06-05 17:07:22 -05:00
Aaron Bockelie
741eff5412
Merge pull request #213 from aaronsb/feat/132-graph-statistics-vault-wide
feat(graph): vault-wide statistics when sourcePath omitted (closes #132)
2026-06-05 17:06:43 -05:00
dependabot[bot]
43786033a2
chore(deps): bump yaml
Bumps the production-dependencies group with 1 update in the / directory: [yaml](https://github.com/eemeli/yaml).


Updates `yaml` from 2.8.4 to 2.9.0
- [Release notes](https://github.com/eemeli/yaml/releases)
- [Commits](https://github.com/eemeli/yaml/compare/v2.8.4...v2.9.0)

---
updated-dependencies:
- dependency-name: yaml
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: production-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-02 03:25:15 +00:00
Aaron Bockelie
9191bbdee8 feat(graph): vault-wide statistics when sourcePath omitted (closes #132)
Before: graph.statistics threw 'Source path is required' when called
without sourcePath, blocking the baseline use case (vault health
snapshots, dashboards). Users had to pick an arbitrary seed and reason
about its neighborhood — not representative.

After: when sourcePath is omitted, return a new vaultStatistics shape:

  totalNotes            — count of .md files in the vault
  totalLinks            — sum of resolved link occurrences (Obsidian
                          semantics: A→B referenced 3× counts as 3)
  orphanCount           — singletons (no links in either direction)
  averageDegree         — 2 * totalLinks / totalNotes
  largestComponentSize  — biggest connected subgraph (undirected)
  isolatedClusters      — total connected-component count,
                          inclusive of singletons (so non-trivial =
                          isolatedClusters - orphanCount)

Per-node statistics behaviour is unchanged when sourcePath is provided.

Implementation: GraphTraversal.getVaultStatistics does one O(V+E)
pass over the resolvedLinks adjacency, plus BFS for components.
Treats the graph as undirected for component analysis (a link from A
to B means A and B are in the same component) while keeping link
counts directed (matches how metadataCache exposes them).

Tests: tests/graph-statistics-vault-wide.test.ts covers the
vault-wide path (the topology described above), per-node fallback,
empty vault, repeated occurrences, non-md filtering, and the
directed-edges-undirected-components invariant.
2026-05-25 16:21:24 -05:00
Aaron Bockelie
37d4a2ee02
Merge pull request #206 from fa1k3/fix/forwardlinks-include-unresolved
Honor includeUnresolved for graph forward links
2026-05-25 16:16:44 -05:00
Aaron Bockelie
943de668f9 test(graph-search): match TFile mock to post-#207 getNodeTitle contract
PR #207 (merged 2026-05-25) switched forwardlinks node titling from
`file.name.replace(/\.md$/, '')` to `getNodeTitle(file)`, which reads
`file.basename`. This PR was authored before that landed, so the test
mock set `name` but not `basename` and now produces `title: undefined`
on integration. Add `basename` to the mock to match the real TFile API.
2026-05-25 16:16:16 -05:00
Aaron Bockelie
4fc596b9eb
Merge pull request #207 from fa1k3/fix/graph-index-titles
Use parent folder titles for index graph nodes
2026-05-25 16:12:49 -05:00
Aaron Bockelie
37dd230b85 chore: Bump version to 0.11.31 2026-05-25 14:58:16 -05:00
Aaron Bockelie
7e9552137a
Merge pull request #212 from aaronsb/fix/210-dispatch-param-guards
fix(dispatch): require params at the MCP boundary (closes #210)
2026-05-25 14:57:48 -05:00
Aaron Bockelie
a08347f4be fix(dispatch): address PR #212 review findings
- vault.update content-guard hint pointed at vault.patch, which does not
  exist — patch lives at edit.patch. Bad recovery hint is worse than no
  hint.
- edit.window guard test claimed "before searching the file" but only
  asserted on mutations; the mock's 'original' content doesn't contain
  'undefined' so even a leaked guard wouldn't show a write. Add a reads
  recorder and assert getFile is never called, making the test honest
  about what it proves.
- Strip a stray space from the lock-key guard comment.
2026-05-25 08:31:37 -05:00
Aaron Bockelie
000f69551a fix(dispatch): require params at the MCP boundary (closes #210)
#210 reported that `vault.update` with no `content` writes the literal
9-byte string `"undefined"` to the target file — `String(undefined)`
coerced silently, the validator accepted the 9-byte string, and the
formatter rendered the misleading heading `✓ Updated: undefined`.

The same `String(params.x)` footgun existed at every vault-mutating
dispatch site: `edit.append` (would append "undefined"), `edit.window`
(would search for or replace with "undefined"), plus path-only cases
that produced cryptic "File not found: undefined" errors instead of a
clean dispatch-level rejection.

Fix the class, not just the instance:

- Add `requireParamStr(params, key, action, hint?)` in operations/shared.
  Throws at the boundary when a required string param is missing or
  wrong-typed, so a malformed MCP call cannot reach a vault sink.
- Convert the corruption-class sites (`vault.update`, `vault.create`,
  `vault.delete`, `edit.append`, `edit.window`) and the noisy path-only
  sites (`edit.patch`, `edit.at_line`, `edit.from_buffer`, `view.file`,
  `view.window`, `view.open_in_obsidian`) to use the helper.
- `vault.update`'s `content` guard carries a hint pointing at
  `vault.patch` / `edit.window` — that's the wrong-tool path that
  produced the #210 corruption in the wild.
- Lift `edit.*`'s path guard above `FileLockManager.withLock` so a
  missing path can't take a lock on the literal string "undefined".
- `updateFile` / `deleteFile` now return `{ success, path }` so
  `formatFileWrite` renders `Updated: <path>` — belt-and-suspenders for
  the second issue called out in #210.

Tests: `tests/dispatch-param-guards.test.ts` records every mutation a
mock API receives; for each malformed call we assert the route rejects
AND no mutation was recorded, proving the guard runs before any sink.
2026-05-25 07:54:04 -05:00
Aaron Bockelie
6358d31fdd fix(ci): harden release.yml against shell injection in notes
The previous template inlined ${{ inputs.release_notes }} directly into
a double-quoted bash NOTES="..." assignment, so backticks (or $vars) in
the notes were evaluated by the shell as command substitution. Today's
0.11.30 release hit this — backticks in the ADR-107 release notes blew
up the heredoc and failed the workflow before tag creation.

Switch the templated values to env-var bindings (RELEASE_NAME,
RELEASE_VERSION, RELEASE_NOTES_INPUT) and assemble the markdown via
printf so the notes are treated as data, not code.
2026-05-24 17:11:06 -05:00
Aaron Bockelie
4fddaab955 chore: Bump version to 0.11.30 2026-05-24 17:08:33 -05:00
Aaron Bockelie
1bd6798e01
Merge pull request #211 from aaronsb/feat/107-network-exposure-state-machine
feat(network): ADR-107 — network exposure as classified state machine
2026-05-24 17:07:39 -05:00
Aaron Bockelie
2b4ef6a9b4 fix(network): address PR #211 self-review findings (ADR-107)
Blocking — userSuppliedCert discriminator:
- Was: certificateConfig.selfSigned === false (never set false in UI;
  rows 5/7/9 of the ADR-107 table unreachable, defaulting to over-warn).
- Now: !!(certPath && keyPath) — actual cert provenance signal.
  Fixed in src/mcp-server.ts and src/main.ts settings UI badge.

Non-blocking #1 — migration Notice race:
- Was: isExistingPreBindModeInstall() re-read data.json after
  loadSettings() may have already persisted bindMode='loopback' as
  part of the apiKey auto-save.
- Now: snapshot raw loadData() before loadSettings(), test the snapshot.

Non-blocking #2 — loopback host matcher tightened:
- Was: startsWith('127.') accepted '127.evil.com'.
- Now: strict dotted-quad regex /^127(\.\d{1,3}){3}$/ with octet range.
  New tests cover hostile inputs (127.evil.com, 127.0.0.1.attacker.tld,
  127.300.0.1, 1270.0.0.1).

Non-blocking #3 — Reconfigure hint:
- ADR-107 specifies the 🔴 badge includes a Reconfigure hint;
  implementation now renders 'Reconfigure: switch the bind address
  below to Loopback, or enable HTTPS.' beneath the verdict reason.

Non-blocking #4 — empty custom host UX:
- When bindMode='custom' && customBindHost is empty, render an
  explicit Notice that the server will fall back to loopback until a
  host is entered (resolves the silent 🟢 OK in that state).

Lint clean, 296/296 tests pass.
2026-05-24 17:06:16 -05:00
Aaron Bockelie
104b40af90 feat(network): wire classifier into server, settings, UI (ADR-107)
Replaces the implicit 0.0.0.0 Node-default bind with a deliberate,
classified network exposure model.

Server: MCPHttpServer.start() now resolves the listen host from
settings (bindMode + customBindHost), passes it to listen(), and
classifies the combined (protocol × bind × certSource) state. On 🔴
fires a 15s Notice + Debug.error; on 🟡 logs Debug.warn; on 🟢 silent.
The verdict-derived agent-visible warning is pushed into MCPServerPool
which conditionally injects it into the MCP initialize.instructions
field for every session created after bind, so an LLM client sees the
warning on its first turn against the server. node-mcp-server.ts (the
unused fallback) is hardcoded to loopback for consistency.

Settings: MCPPluginSettings gains bindMode ('loopback' | 'all' |
'custom') default 'loopback', customBindHost default '', and a
one-shot hasShownBindMigrationNotice flag. Existing installs with a
data.json that predates bindMode see a one-time 20s Notice on next
load pointing them at the new setting; fresh installs are silent.

UI: a new "Network binding" section between server config and the
HTTPS section, with a live colored verdict badge that re-classifies
on every change, the bind dropdown (Loopback / All interfaces /
Custom), an inline red caution under "All interfaces" whose wording
adapts to whether HTTPS is enabled, and a custom-host text field
that normalizes loopback/wildcard aliases on blur (typing 127.0.0.1
collapses the dropdown to Loopback; 0.0.0.0 collapses to All).

Tests: 7 integration assertions verifying the source wires listen()
to the resolved host, pushes verdict-derived instructions into the
pool, conditionally injects on 🔴 only, and that the pool spread-
pattern omits the field on ok/warn.

Inspired by PR #208 (fa1k3) — preserves the security instinct while
keeping the LAN/remote escape hatch the hardcoded approach removed.
2026-05-24 16:56:24 -05:00
Aaron Bockelie
ef4f6c0814 feat(network): network exposure classifier + tests (ADR-107)
Pure functions consumed by the bind-time guard, settings UI, and MCP
initialize.instructions injector: classify(NetworkState) → Verdict,
resolveListenHost(), resolveBindAxis(), normalizeBindInput(),
classifyFromSettings(), agentInstructionsForVerdict(). Single source
of truth for "how exposed is the current network configuration?".

The 9-cell ADR-107 table maps (protocol × bind × certSource) → one of
🟢 ok / 🟡 warn / 🔴 jail. Custom-input normalization collapses
loopback aliases (127.x.x.x / localhost / ::1) and wildcard aliases
(0.0.0.0 / ::) to the corresponding explicit BindMode so the same
intent expressed three ways converges to one state.

40 unit tests cover all 9 table cells, all normalization paths, the
defensive empty-custom fallback, and the agent-instructions formatter.
2026-05-24 16:55:50 -05:00
Aaron Bockelie
5884ac492d docs(adr): accept ADR-107 — network exposure as classified state machine
Models the combined (protocol × bind × cert) state as a 9-cell table
with three verdicts (🟢 OK / 🟡 WARN / 🔴 JAIL). Server always starts;
🔴 surfaces a Notice + log + MCP `initialize` instructions warning so
the LLM client also sees it. New settings: `bindMode` discriminated
enum + `customBindHost`. Migration flips the implicit `0.0.0.0` to a
real `127.0.0.1` default, making ADR-103's "loopback by default"
premise actually true. Inspired by community PR #208 (fa1k3) —
preserves the security instinct while keeping the LAN/remote escape
hatch the hardcoded approach would have removed.
2026-05-24 16:44:35 -05:00
Aaron Bockelie
f6c108e625
Merge pull request #205 from aaronsb/feat/203-faithful-paginated-reads
feat(vault.read): faithful-by-default reads with char-budget pagination (ADR-203, #133)
2026-05-24 16:33:07 -05:00
dependabot[bot]
c760a4d113
chore(deps): Bump qs from 6.14.2 to 6.15.2
Bumps [qs](https://github.com/ljharb/qs) from 6.14.2 to 6.15.2.
- [Changelog](https://github.com/ljharb/qs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/ljharb/qs/compare/v6.14.2...v6.15.2)

---
updated-dependencies:
- dependency-name: qs
  dependency-version: 6.15.2
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-05-23 12:47:43 +00:00
fa1k3
2754545a99 Use parent folder titles for index graph nodes 2026-05-20 07:40:12 +02:00
fa1k3
19a8f7f0f3 Honor includeUnresolved for forward links 2026-05-20 07:36:30 +02:00
Aaron Bockelie
1f83465d3b fix(formatter): emit verbatim content in default vault.read (PR #205 blocking review)
formatFileRead truncated the string branch to a 50-line preview; since raw
defaults false, the DEFAULT vault.read was that truncated view — an agent
reading any >50-line file could not derive a byte-matching edit.window
oldText without raw:true, the exact #133 friction ADR-203 retires. The data
layer was already byte-faithful; the presentation default undercut it.

- string branch: emit full content verbatim, no 50-line slice, no wrapping
  code fence (body may contain fences — a wrapper would corrupt round-trip)
- add non-text/binary passthrough guard (closes a pre-existing latent
  formatter-error path; reviewer non-blocking note)
- test now asserts the FORMATTED (raw:false) output is byte-exact, not just
  not.toThrow

make check green: 0 errors, baseline 5 warnings, 244/244.
2026-05-19 00:38:29 -05:00
Aaron Bockelie
f3dac1847e feat(vault.read): faithful-by-default reads with char-budget pagination (ADR-203, #133)
Default vault.read no longer fragments-and-flattens. New contract:
- fits READ_PAGE_CHARS (50000) → whole file, byte-exact, one load (common case)
- exceeds → verbatim page 1, single contiguous block, with line bookends
  (lineStart/lineEnd/totalLines + nextPage); page=N to continue. Absolute
  line numbers preserved so edit.at_line still works on large files.
- returnFullFile:true → entire file verbatim (explicit large override; param
  retained & repurposed, not retired)
- query/strategy/maxFragments → semantic fragments (unchanged)
- structured envelope no longer double-encodes the body (metadata sans body)

Budget is char-based on purpose: line count is an invalid proxy for context
cost; only bookends are line-based (for at_line). Hard invariant: a default
read must never hand the agent a context-breaking raw dump.

Also fixes the latent formatFileRead crash (_Formatter error_) on full-file
shapes; formatter now renders verbatim + a Pagination section.

src/utils/file-reader.ts rewritten; vault.ts threads `page`; formatter
hardened; tool description + CHANGELOG (breaking) updated.
make check green (0 errors, baseline 5 warnings, 243/243 incl. 8 new
ADR-203 round-trip/pagination/fidelity tests).
2026-05-19 00:30:31 -05:00
Aaron Bockelie
150cf5c071 docs(adr): amend ADR-203 — char-budget pagination with line bookends
Refined through design dialogue:
- paginate/no-paginate boundary is a CHARACTER budget (READ_PAGE_CHARS,
  default 50000), not a line count (line count is an invalid proxy for
  context cost — 1500 short vs 1500 long lines differ by orders of magnitude)
- bookends stay line-based (lineStart/lineEnd/totalLines) so edit.at_line
  on a large file still works; a page = longest whole-line run within the
  char budget
- fits budget → whole file verbatim, one load (common case, no ceremony)
- exceeds → bookended page 1 (single contiguous block, not chunk array) +
  nextPage hint; sequential paging via new `page` param
- returnFullFile:true RETAINED and repurposed as the explicit
  whole-large-file override (not retired)
- hard invariant recorded: a default read must never hand the agent a
  context-breaking raw dump
Alternatives added: no-guard full dump, line-count budget, chunk-array — all rejected with rationale.
2026-05-19 00:24:56 -05:00
Aaron Bockelie
a2e552104c
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)
2026-05-19 00:16:45 -05:00
Aaron Bockelie
d4c005f1a6 docs(adr): accept ADR-203 (reviewed & approved) 2026-05-19 00:16:11 -05:00
Aaron Bockelie
2be1998e12 docs(adr): ADR-203 — faithful-by-default content reads; concise tool results (#133)
Synthesizes #133's intent (machine-usable default for a machine protocol)
with the project instinct (concise formatted tool results, but unaltered
content for editing). Empirically grounded: default vault.read is lossy
(fragments + flattens newlines → broken read→edit round-trip), the raw
envelope double-encodes the body (~2x), and returnFullFile-without-raw
crashes the formatter.

Decision (two-bucket contract): content reads faithful/verbatim by default
(fragmentation = explicit opt-in via existing strategy/maxFragments);
action/result tools stay concise-formatted by default; stop double-encoding
the body; verbatim path must not route through the broken formatter.
#133's global-raw-flip remedy explicitly declined; its intent satisfied by
bucket 1. Status: Proposed (design of record; implementation tracked
separately, not in this ADR).
2026-05-19 00:14:29 -05:00
Aaron Bockelie
2decbc4f08
Merge pull request #203 from aaronsb/refactor/199-split-router-monolith
refactor(router): extract vault operation into operations/ module (ADR-202, #199 stage 1)
2026-05-19 00:00:09 -05:00
Aaron Bockelie
767bbd7a49 refactor(router): drop unused ctx param from pure helpers (PR #203 review nit)
splitContent/sortFiles don't touch RouterContext — the uniform ctx-first
param was a mechanical-extraction artifact. Removed from both defs and
their two call sites. Indentation nit deliberately NOT addressed: the
retained class-method indentation is what makes the extraction a
byte-verifiable behaviour-preserving move (per the review's normalized
diff); reflowing would destroy that property for zero functional gain.

tsc clean, lint baseline, 235/235.
2026-05-18 23:14:12 -05:00
Aaron Bockelie
c2d6f8b325 refactor(router): extract vault operation into operations/ module (ADR-202, #199 stage 1)
router.ts was a 2,228-line monolith (~2.8x the 800-line threshold). Extract
the largest unit — executeVaultOperation + its helpers — into
src/semantic/operations/vault.ts behind a RouterContext interface.

- operations/router-context.ts: RouterContext (api/app/fragmentRetriever/
  validator); SemanticRouter implements it and passes itself, so shared
  state propagates with no indirection
- operations/shared.ts: Params, SearchResultItem, paramStr/Num/Bool
  (router-private → shared by all handlers)
- operations/vault.ts: executeVaultOperation + splitContent/sortFiles/
  copyFile/copyDirectoryRecursive; mechanical this.→ctx. (tsc is the
  safety net — a missed rewrite fails compile)
- router fields api/app/fragmentRetriever/validator → public readonly
- removed a fully unreachable pre-existing dead subtree (combineSearchResults,
  isDirectory, performFileBasedSearch, indexVaultFiles, getSearchWorkflowHints,
  extractContext, getFileType — zero call sites anywhere; eslint doesn't flag
  unused class methods, which hid them). ~290 dead lines gone.

router.ts 2228→988; vault.ts 948. Behaviour-preserving: tsc clean, lint at
the exact pre-existing baseline (0 errors, 5 unrelated warnings), 235/235
tests unchanged.

Staged per the issue's "extract vault first in isolation" guidance: #199
stays open for the remaining handlers (edit/view/graph/system/bases →
router thin dispatcher <300 lines). ADR-202.
2026-05-18 23:07:40 -05:00
Aaron Bockelie
759ad09a9d chore: Bump version to 0.11.29 2026-05-18 22:50:53 -05:00
Aaron Bockelie
b38b7ff4e5
Merge pull request #201 from aaronsb/fix/190-client-driven-reinit-signal
fix(mcp): client-driven session re-init via spec 404 (ADR-106, closes #190)
2026-05-18 22:50:09 -05:00