Skip to content

feat: surface recall metadata in brv query --format json#608

Merged
danhdoan merged 2 commits into
proj/surface-curate-query-claudefrom
feat/ENG-2537
May 5, 2026
Merged

feat: surface recall metadata in brv query --format json#608
danhdoan merged 2 commits into
proj/surface-curate-query-claudefrom
feat/ENG-2537

Conversation

@RyanNg1403
Copy link
Copy Markdown
Collaborator

@RyanNg1403 RyanNg1403 commented May 5, 2026

Summary

  • Problem: brv query --format json collapses everything into a plain-text result field, so downstream consumers (brv-bridge, the ByteRover Claude plugin) can't tell which docs informed the answer or how the answer was reached. ByteRover's "memory used" evidence stays invisible.
  • Why it matters: The activation-funnel work needs visible evidence of recall in the user's loop. The visible 🧠 ByteRover returns N memories: … line in Claude Code consumes structured retrieval metadata; this PR is what surfaces it at the source.
  • What changed: Extend the JSON envelope with matchedDocs, tier, durationMs, topScore. Plumb via the existing lifecycle-hook pattern.
  • What did NOT change (scope boundary): Text mode (--format text) byte-identical. No new search/scoring logic — only exposes what QueryExecutor already computes. No MCP tool changes.

Type of change

  • New feature

Scope (select all touched areas)

  • CLI Commands (oclif) — query.ts
  • Server / Daemon — query-log-handler.ts
  • Shared (constants, types, transport events) — task-client.ts types

Linked issues

N/A — Linear linkage is via branch name and commit subjects.

Root cause (bug fixes only, otherwise write N/A)

N/A.

Test plan

  • Coverage added:
    • Unit test
  • Test file(s):
    • test/unit/infra/process/query-log-handler.test.ts — 4 new cases for getTaskCompletionData (all-fields, no-task, no-result, partial cache-hit shape).
    • test/commands/query.test.ts — 2 new cases for the JSON envelope (fields surfaced when present, gracefully omitted when absent — older daemons or non-query tasks).
  • Key scenario(s) covered: rich Tier 2 response with all fields; cache-hit (Tier 0/1) shape with empty matchedDocs; absent-field graceful degradation.

User-visible changes

brv query --format json "X" now returns:

```json
{
"command": "query",
"data": {
"event": "completed",
"matchedDocs": [{"path": "auth/jwt-tokens.md", "score": 0.92, "title": "JWT tokens"}, …],
"result": "…synthesised prose…",
"status": "completed",
"taskId": "uuid",
"tier": 2,
"durationMs": 184,
"topScore": 0.92
},
"success": true,
"timestamp": "…"
}
```

--format text (default) output is byte-identical to before.

Evidence

  • Failing test before / passing after — npx mocha test/unit/infra/process/query-log-handler.test.ts: 17/17 pass; npx mocha test/commands/query.test.ts: 23/23 pass.

Checklist

  • Tests added or updated and passing (npm test)
  • Lint passes (npm run lint)
  • Type check passes (npm run typecheck)
  • Build succeeds (npm run build)
  • Commits follow Conventional Commits format
  • No breaking changes — all new fields are optional; existing consumers reading only data.result keep working
  • Branch is up to date with proj/surface-curate-query-claude

Risks and mitigations

  • Risk: Older brv-bridge versions reading the JSON envelope ignore the new fields silently.
    • Mitigation: Additive only; bridge upgrade is in the sibling brv-bridge PR.
  • Risk: Field-shape divergence from the bridge's proposed schema.
    • Mitigation: Field names match the bridge's RecallResult exactly (durationMs, topScore flat). The internal QueryLogEntry keeps its nested timing.durationMs / searchMetadata.topScore shape — only the public task:completed event payload and CLI envelope are flat.

Implementation notes

  • Decision: title field on QueryLogMatchedDoc is required (not optional) — QueryExecutor.buildMatchedDocs() always populates it. Tightened from the proposed schema. Easy to relax if a future executor change changes the contract.
  • Decision: dropped seven additional optional fields proposed in the bridge schema (snippet, lastCurated, symbolKind, symbolPath, backlinkCount, relatedPaths, overviewPath, origin) — QueryExecutor doesn't populate them today. Additive — easy to add back.

Extend the `brv query --format json` envelope with `matchedDocs`, `tier`,
`durationMs`, and `topScore` so downstream consumers (brv-bridge, the
ByteRover Claude plugin) can surface evidence of memory retrieval to
the user.

The data already exists in QueryExecutor and is persisted to query-log
entries — this just exposes it on the public CLI surface.

Implementation:
- QueryLogHandler.getTaskCompletionData() exposes the structured payload
  via the existing lifecycle-hook contract; TaskRouter automatically
  merges it into the task:completed event sent to the originating client.
- task-client.ts extends TaskCompletedWithLogId and TaskCompletionResult
  with the new optional fields; older daemons that don't emit them
  degrade gracefully (fields stay undefined).
- query.ts conditionally spreads the new fields into the JSON envelope
  so consumers don't see undefined keys for non-query tasks.

Tests:
- 4 new cases for getTaskCompletionData (all-fields, no-task, no-result,
  partial cache-hit shape).
- 2 new cases for the query command's JSON envelope (fields surfaced
  when present; gracefully omitted when absent).

Parent ticket: ENG-2537.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Claude finished @RyanNg1403's task in 2m 55s —— View job


PR Review: feat: [ENG-2615] surface recall metadata in brv query --format json

Tasks

  • Gather context and read changed files
  • Review src/oclif/lib/task-client.ts
  • Review src/server/infra/process/query-log-handler.ts
  • Review src/oclif/commands/query.ts
  • Review test files
  • Post final review

Overall Score: 4 / 5

Summary: Clean, well-scoped implementation. The lifecycle-hook pattern for surfacing metadata is a good fit, backward compatibility is handled correctly (conditional spreads ensure older daemons / non-query tasks emit unchanged JSON), and the test coverage is thorough. Three items worth addressing — one is a doc inaccuracy that will confuse future readers, one is a dead-code guard, and one is an architectural concern about a cross-boundary import that deserves a follow-up ticket.


Findings

src/server/infra/process/query-log-handler.ts

nitpick — dead timing !== undefined guard (line 79)
timing is required in QueryResultMetadata (Omit<QueryExecutorResult, 'response'>), so the guard is always true and the else branch is unreachable. Either inline the assignment unconditionally or add a comment explaining why you're being defensive against future interface relaxation. See inline comment.

src/oclif/lib/task-client.ts

issue — JSDoc/type mismatch in TaskCompletedWithLogId comment (line 34)
The JSDoc names timing and searchMetadata as the fields merged from QueryLogHandler, but the type actually declares the flattened names durationMs and topScore. A reader comparing the comment to the type block will be confused. See inline comment.

suggestion (non-blocking) — cross-boundary import (lines 19-22)
QueryLogMatchedDoc and QueryLogTier are imported from server/core/domain/entities/query-log-entry.js into oclif/lib/task-client.ts. CLAUDE.md explicitly states that oclif/ should not import from server/ — commands are daemon-routed and only shared/ is the canonical cross-module exchange layer. The existing TaskErrorCode import at line 24 is a pre-existing violation; this PR widens it by adding two more types. With ENG-2617 (visible recall in the Claude plugin) likely needing these types in tui/ as well, this is the right moment to move QueryLogMatchedDoc / QueryLogTier into src/shared/. Flagging as non-blocking for this PR but recommend filing a follow-up.

src/oclif/commands/query.ts

nitpick — repeated conditional spread (lines 186-193)
Four ...(x === undefined ? {} : {x}) spreads in a row are noise-heavy. A small inline defined() helper (or moving the conditional logic into task-client.ts before the callback is invoked) would collapse this to one expression. Not a blocker — but worth noting if this pattern propagates to other commands. See inline comment.

Tests

The test coverage is solid:

  • query-log-handler.test.ts — 4 cases correctly exercise all-fields, no-task, no-result, and cache-hit shape (absent searchMetadata).
  • query.test.ts — 2 cases verify JSON envelope fields are present when populated and omitted when absent.

Minor duplication: both new query.test.ts cases duplicate the eventHandlers + callsFake boilerplate verbatim. A shared makeCompletionStub(payload) helper would halve the noise, but this is cosmetic.

What's done well

  • Correct use of ITaskLifecycleHook.getTaskCompletionData (synchronous, best-effort, merged before task:completed is emitted — verified in task-router.ts:356-368).
  • Lifecycle ordering is safe: getTaskCompletionData fires before cleanup() removes in-memory state.
  • Additive-only JSON shape: all new fields are optional; existing consumers reading only data.result are unaffected.
  • title on QueryLogMatchedDoc tightened to required — consistent with QueryExecutor.buildMatchedDocs() always populating it, and easy to relax later.
  • The flat durationMs / topScore on the public envelope vs. the nested internal shape is the right call; it keeps the consumer contract simple.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4662fb33e8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread package-lock.json Outdated
Comment thread src/server/infra/process/query-log-handler.ts Outdated
Comment thread src/oclif/lib/task-client.ts Outdated
Comment thread src/oclif/lib/task-client.ts
Comment thread src/oclif/commands/query.ts
@RyanNg1403 RyanNg1403 changed the title feat: [ENG-2615] surface recall metadata in brv query --format json feat: surface recall metadata in brv query --format json May 5, 2026
Three changes from PR review:
1. Drop the dead `if (timing !== undefined)` guard in
   QueryLogHandler.getTaskCompletionData() — `timing` is required on
   QueryExecutorResult (every executor branch sets it), so the guard
   is unreachable. Inline the assignment instead.
2. Fix the JSDoc on TaskCompletedWithLogId in task-client.ts — it
   listed the QueryExecutorResult-shaped names (`timing`/`searchMetadata`)
   but the actual type uses the flattened public-contract names
   (`durationMs`/`topScore`). Updated to match.
3. Refresh package-lock.json so the axios entry resolves to 1.16.0
   (matching the manifest pin) instead of the 1.15.0 left behind by
   the rebase. Restores deterministic-install agreement.
@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 5, 2026

Claude finished @RyanNg1403's task in 1m 55s —— View job


PR Review: feat: [ENG-2615] surface recall metadata in brv query --format json

Tasks

  • Gather context and read PR description
  • Read changed files
  • Analyze correctness, security, and code quality
  • Post inline comments for specific issues
  • Post final review summary

Overall Score: 4.5 / 5

Summary: Clean, well-scoped implementation. The refactor commit (410d1e7) proactively addressed the main issues from the first review pass — the dead timing !== undefined guard is gone, the JSDoc now correctly names the flat durationMs/topScore fields, and the package-lock.json axios divergence is fixed. Two minor items remain.


What the refactor commit fixed ✅

Issue Status
Dead if (timing !== undefined) guard in getTaskCompletionData() Fixed — inlined unconditionally, with explanatory comment
JSDoc named timing/searchMetadata instead of flattened durationMs/topScore Fixed — comment now matches the actual type
package-lock.json axios 1.15.0 vs manifest pin 1.16.0 Fixed — lockfile realigned to 1.16.0

Remaining findings

src/oclif/lib/task-client.ts — line 22

suggestion (non-blocking) — cross-boundary import

QueryLogMatchedDoc and QueryLogTier are imported from server/core/domain/entities/query-log-entry.js. CLAUDE.md explicitly prohibits oclif/ from importing server/. The existing TaskErrorCode import at line 24 is a pre-existing violation; this PR widens it by two more types. With ENG-2617 likely needing these same types in tui/, this is the right moment to move both types into src/shared/ so both consumers can reach them cleanly. Fix this →

src/oclif/commands/query.ts — lines 186–193

nitpick — repeated conditional spread

Four ...(x === undefined ? {} : {x}) spreads in a row are functional but verbose. An Object.fromEntries + .filter one-liner collapses all four. Not a blocker, but worth addressing before this pattern propagates to other commands (see inline comment).

Tests

Coverage is solid and the test structure is clean. One cosmetic note: the two new query.test.ts cases at lines 433–509 duplicate the eventHandlers + callsFake setup verbatim. A shared makeCompletionStub(payload) helper would remove ~30 lines of duplication — cosmetic only.


What's done well

  • Lifecycle hook pattern: getTaskCompletionData is the right hook — synchronous, best-effort, merged before task:completed is emitted. No changes to TaskRouter needed.
  • Correct flattening: internal timing.durationMs / searchMetadata.topScore → flat public durationMs / topScore. Keeps the consumer contract simple while the internal shape stays nested.
  • Backward compatibility: all new fields are optional; existing consumers reading only data.result are unaffected. Conditional spreads correctly omit undefined keys from JSON output.
  • searchMetadata guard: correctly absent for cache hits (Tier 0/1). Test at line 328 covers this case explicitly.
  • title required on QueryLogMatchedDoc: tight contract, consistent with QueryExecutor.buildMatchedDocs() always populating it. Easy to relax later.
  • Test coverage: 4 new getTaskCompletionData cases + 2 new JSON envelope cases cover all-fields, cache-hit shape, no-task, no-result, and graceful-degradation paths.

Comment thread src/oclif/lib/task-client.ts
Comment thread src/oclif/commands/query.ts
@danhdoan danhdoan merged commit 2485646 into proj/surface-curate-query-claude May 5, 2026
6 checks passed
@danhdoan danhdoan mentioned this pull request May 6, 2026
21 tasks
bao-byterover pushed a commit that referenced this pull request May 6, 2026
* feat: [ENG-2615] surface recall metadata in brv query --format json

Extend the `brv query --format json` envelope with `matchedDocs`, `tier`,
`durationMs`, and `topScore` so downstream consumers (brv-bridge, the
ByteRover Claude plugin) can surface evidence of memory retrieval to
the user.

The data already exists in QueryExecutor and is persisted to query-log
entries — this just exposes it on the public CLI surface.

Implementation:
- QueryLogHandler.getTaskCompletionData() exposes the structured payload
  via the existing lifecycle-hook contract; TaskRouter automatically
  merges it into the task:completed event sent to the originating client.
- task-client.ts extends TaskCompletedWithLogId and TaskCompletionResult
  with the new optional fields; older daemons that don't emit them
  degrade gracefully (fields stay undefined).
- query.ts conditionally spreads the new fields into the JSON envelope
  so consumers don't see undefined keys for non-query tasks.

Tests:
- 4 new cases for getTaskCompletionData (all-fields, no-task, no-result,
  partial cache-hit shape).
- 2 new cases for the query command's JSON envelope (fields surfaced
  when present; gracefully omitted when absent).

Parent ticket: ENG-2537.

* refactor: address review feedback on getTaskCompletionData and JSDoc

Three changes from PR review:
1. Drop the dead `if (timing !== undefined)` guard in
   QueryLogHandler.getTaskCompletionData() — `timing` is required on
   QueryExecutorResult (every executor branch sets it), so the guard
   is unreachable. Inline the assignment instead.
2. Fix the JSDoc on TaskCompletedWithLogId in task-client.ts — it
   listed the QueryExecutorResult-shaped names (`timing`/`searchMetadata`)
   but the actual type uses the flattened public-contract names
   (`durationMs`/`topScore`). Updated to match.
3. Refresh package-lock.json so the axios entry resolves to 1.16.0
   (matching the manifest pin) instead of the 1.15.0 left behind by
   the rebase. Restores deterministic-install agreement.

* chore: drop esbuild peer:true entries from lockfile

These 11 entries were introduced by an earlier 'npm install' in commit
4662fb3 (PR #608) running with a different npm CLI version that
re-resolves esbuild's optionalDependencies as peer entries. They are
descriptive metadata only, do not affect npm ci installs, and are not
present on main. Drop them so proj -> main lockfile diff is empty.

Verified: 'npm ci --dry-run' passes against the reverted lockfile.

---------

Co-authored-by: Nguyễn Thuận Phát <nguyenthuanphatvl@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants