json-output-contract
Summary
Section titled “Summary”Added --json output mode to vir query and vir doctor commands in the vir CLI (v0.7.1), creating a stable machine-readable contract for the upcoming vir-obsidian plugin. Pure builder functions were extracted into src/output/json.ts and tested in isolation using TDD before wiring into the CLI. A subsequent security review traced all changed files from entry point to sink and found no vulnerabilities — the change is read-only structured JSON reporting for search queries and diagnostics.
What Was Learned
Section titled “What Was Learned”JSON output contract for CLI commands (vir query --json / vir doctor --json)
query --jsonemits a JSON array to stdout ([]when empty, exit 0). On failure: stdout is empty, a single-lineVirErrorPayloadgoes to stderr, exit non-zero. Ollama being unavailable is not a failure — the retriever degrades to TF-IDF, so results still return.doctor --jsonemits one JSON object, always exits 0. Health lives in thedaemonfield (ok/stale/downvia a 2× polling-interval window).query --jsonskips synthesis (no LLM token spend), returns raw retriever hits mapped to the wire schema.
Architecture: pure builders for CLI output modes
- To unit-test a new CLI output mode without spawning the binary, extract the domain→wire-schema mapping into pure functions in their own module. The command
.action()becomes a thin wrapper: gather side-effectful inputs (config, DB, filesystem, network), call the builder, serialize, set exit code. src/output/json.tsfollowed this pattern: 14 fast unit tests cover the schema and daemon-health classification (injectablenow). The untestable wiring (empty-stdout-on-error, exit codes) was verified with one live smoke run.
Unreachable enum entries in error contracts
- A wire-contract error
kindenum may legitimately include a value you never emit.ollama_unavailableexists inVirErrorPayloadfor the consumer’s exhaustiveness check, but the CLI never emits it because Ollama-down is handled gracefully (TF-IDF fallback). Document why at the definition site, or a future maintainer may “fix” the gap by making Ollama-down an error and silently break graceful degradation.
prepublishOnly as a publish guard
- Added
"prepublishOnly": "npm run build && npm test"topackage.json. This fires on bothnpm publishandnpm publish --dry-run, preventing a staledist/or red tests from shipping. The manualnpm run buildbefore publish is still recommended so the tarball preview reflects the current state.
Security: stdout/stderr contract for CLI ↔ plugin IPC
- When a plugin parses
JSON.parse(stdout)without guards, the CLI must guarantee stdout is empty on failure. Enforce this by routing all error payloads exclusively to stderr, and verify that no transitive dependency (e.g., embedder, search modules) writes to stdout orconsole.log.
Security: deliberate secret exclusion from diagnostic payloads
runDoctorJsonintentionally omits any API key health check so credentials are never serialized into the JSON output. Diagnostic endpoints should emit only operational metadata (paths, db size, version, model name), never secrets — even when the secret probe exists elsewhere in the codebase.
Security: same-user local surface reduces cross-principal risk
- CLI ↔ plugin trust boundaries where both sides run as the same OS user do not constitute a cross-principal boundary. File paths in stderr error messages are conventional in this context and not a reportable leak.
Retrieval ranking: verified-note boost applied pre-slice
- Notes with
verified: truein YAML frontmatter receive a flat+0.2score boost applied before thetopKslice in both embedding and TF-IDF paths. This ensures human-approved notes can outrank unverified ones that would otherwise fall just inside the window. In TF-IDF, the boost is only applied whenscore > 0(lexical match required), preventing verified notes from surfacing on zero overlap.
Retrieval ranking: MMR reranking for embedding diversity
- The embedding search path uses Maximal Marginal Relevance (MMR) to rerank results. The user-facing
retrievalDiversityconfig (1.0 = pure diversity) is inverted before passing tommrRerank, wherelambdais the relevance weight — a UX convention inversion that should be documented at the call site.
Context
Section titled “Context”- Project: vir
- Category: decision
- Date: 2026-05-27 (query JSON contract design: 18:22 UTC; security review: 18:55 UTC)
Details
Section titled “Details”src/output/json.ts— pure JSON builder, no I/O; the canonical builder module and wire schema types (VirQueryResult,VirDoctorResult,VirErrorPayload)src/output/json.test.ts— 14 unit tests covering schema shape, daemon health classification, and error payloadssrc/cli.ts—runQueryJson,runDoctorJsonentry pointssrc/diagnostics/doctor.ts— diagnostic payload, secret exclusion patternsrc/search/retriever.ts— MMR rerank, verified boost, embedding/TF-IDF fallback logicsrc/search/embedder.ts— Ollama embedding client (nomic-embed-text)src/mcp/server.ts— existingparseFrontmatter/ `categoryFromTitle
Related
Section titled “Related”- cost-logging-architecture
- anchor-plugin-scaffold
- hybrid-model-routing
- schema-enumeration-stops-drops
- v1-architecture-and-sequencing
Archived Duplicates
Section titled “Archived Duplicates”- json-output-contract-82ea5cef