Skip to content

feat: add DataAPI — rich read-only data access for plugins#123

Merged
tomymaritano merged 11 commits into
developfrom
feature/data-access-api
Mar 12, 2026
Merged

feat: add DataAPI — rich read-only data access for plugins#123
tomymaritano merged 11 commits into
developfrom
feature/data-access-api

Conversation

@tomymaritano

@tomymaritano tomymaritano commented Mar 11, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • DataAPI namespace (context.data): Dedicated rich query interface for plugins, separate from the existing AppAPI
  • Query capabilities: Filtered/sorted/paginated note queries, notebook tree, tag colors, graph data, outgoing links, full-text search with snippets
  • Events: onNotesChanged, onNotebooksChanged, onTagsChanged with typed change events (created/updated/deleted/renamed)
  • Error handling: All bridge calls wrapped in DataAccessError with method name for clean plugin-side error handling
  • Backward compatible: AppAPI unchanged — existing plugins keep working
  • 21 new tests: DataAccessError, createDataAPI delegation, tag filtering, graph filtering, events, error wrapping

Architecture

Plugin → context.data.getNotes({tag: 'x', sortBy: 'updatedAt'})
       → createDataAPI() — options handling, error wrapping
         → DataAPIBridge — thin IPC mapping
           → window.readied.notes.list({tag: 'x', sortBy: 'updatedAt'})

Key types

  • NoteQueryOptions — filter by notebook, tag, status, pin; sort by title/date/wordCount; paginate
  • TagQueryOptions — include colors, case-insensitive substring filter, pagination
  • GraphQueryOptions — scope to notebook (client-side filtering)
  • DataChangeEvent<T> — typed events with action + previousName for renames

Test plan

  • pnpm typecheck — 18/18 packages pass
  • pnpm test — 174 plugin-api tests pass (21 new)
  • DataAccessError construction and formatting
  • Bridge delegation for all query methods
  • Tag client-side filtering and pagination
  • Graph filtering by notebookId
  • Event subscribe/unsubscribe/notify

🤖 Generated with Claude Code

Summary by CodeRabbit

Release Notes

  • New Features

    • Added a Data API for plugins to query and filter notes, notebooks, tags, and graph data with options for sorting and pagination.
    • Added event subscriptions for tracking changes to notes, notebooks, and tags.
  • Tests

    • Added comprehensive test coverage for the new Data API functionality and error handling.
  • Documentation

    • Added design and implementation documentation for the Data API.

tomymaritano and others added 8 commits March 11, 2026 12:33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ctory

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: c4544e0d-8bbc-4e29-91e2-b47213588bb7

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

This pull request introduces a new DataAPI layer providing structured query methods for accessing notes, notebooks, tags, and graph data. The DataAPI is integrated into the plugin system via PluginRegistry and PluginHost, wired through App.tsx, and exposed to plugins through PluginContext, enabling data querying alongside existing APIs.

Changes

Cohort / File(s) Summary
Data API Core
packages/plugin-api/src/data/createDataAPI.ts, packages/plugin-api/src/data/dataTypes.ts
New DataAPI interfaces (DataAPI, DataAPIWithEvents, DataAPIBridge) with methods for querying notes, notebooks, tags, links, and graph data. Includes factory function createDataAPI, safe error wrapping via DataAccessError, and event subscription handling. Companion dataTypes.ts defines all query options, result shapes, and event structures.
Plugin System Integration
packages/plugin-api/src/lifecycle/PluginRegistry.ts, packages/plugin-api/src/lifecycle/PluginHost.tsx, packages/plugin-api/src/types.ts
Extended PluginRegistry.activate() to accept dataAPI parameter and wrap event listeners. Updated PluginHost to accept and pass dataAPI prop. Added data field to PluginContext exposing the wrapped DataAPI to plugins.
Public API Exports
packages/plugin-api/src/index.ts
Re-exports DataAPI types, interfaces, factory function, and data-related types (NoteQueryOptions, NotebookDetailInfo, GraphData, etc.) from data module, providing single entry point for host apps.
App Host Integration
apps/desktop/src/renderer/App.tsx
Creates DataAPI instance via createDataAPI factory using window.readied bridge, wires it into PluginHost, and triggers notifications on note changes via dataAPI._notifyNotesChanged().
Documentation
docs/plans/2026-03-11-data-access-api-design.md, docs/plans/2026-03-11-data-access-api-implementation.md
Design document specifying DataAPI surface, query options, result types, error handling, and integration approach. Implementation document outlining code structure, bridge behavior, and testing strategy.
Test Coverage
packages/plugin-api/tests/createDataAPI.test.ts, packages/plugin-api/tests/dataTypes.test.ts, packages/plugin-api/tests/registry.test.ts
New test suites validating DataAPI delegation, result transformation, error wrapping, event subscriptions, and tag filtering/pagination. Registry tests updated to pass mockDataAPI to activation calls.

Sequence Diagram(s)

sequenceDiagram
    participant App as App.tsx
    participant Registry as PluginRegistry
    participant Context as PluginContext
    participant Plugin as Plugin
    participant Bridge as DataAPIBridge
    participant Server as Server

    App->>App: createDataAPI(bridge)
    App->>Registry: activate(id, ..., dataAPI)
    Registry->>Registry: trackedData = wrap(dataAPI)
    Registry->>Context: data: trackedData
    Context-->>Plugin: context.data
    Plugin->>Plugin: user calls context.data.getNotes()
    Plugin->>Bridge: delegated call
    Bridge->>Server: IPC call (window.readied)
    Server-->>Bridge: results
    Bridge-->>Plugin: transformed results
    Plugin->>Plugin: handle data + events
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and concisely summarizes the main change: adding a DataAPI for rich, read-only data access to plugins, which is the primary objective of the changeset.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/data-access-api

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@tomymaritano

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@tomymaritano

Copy link
Copy Markdown
Collaborator Author

@coderabbitai full review

@coderabbitai

coderabbitai Bot commented Mar 11, 2026

Copy link
Copy Markdown
✅ Actions performed

Full review triggered.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/plugin-api/tests/registry.test.ts (1)

103-127: 🧹 Nitpick | 🔵 Trivial

Assert context.data in the activation contract test.

This PR’s main surface addition is PluginContext.data, but the context-shape test still never checks for it. A regression that drops or misroutes the DataAPI would still pass here.

Suggested assertion
       expect(ctx).toHaveProperty('config');
       expect(ctx).toHaveProperty('log');
       expect(ctx).toHaveProperty('app');
+      expect(ctx).toHaveProperty('data', mockDataAPI);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-api/tests/registry.test.ts` around lines 103 - 127, The test
"it('passes context with all expected APIs')" never asserts PluginContext.data;
update the test after receiving receivedContext (cast to ctx) to assert ctx has
a data property and that it is the DataAPI passed into registry.activate (use
the existing mockDataAPI), e.g. add assertions like
expect(ctx).toHaveProperty('data') and expect(ctx.data).toBe(mockDataAPI) so the
activation contract verifies the DataAPI is routed through
registry.activate/activate.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/desktop/src/renderer/App.tsx`:
- Around line 238-249: getNotebookTree currently hardcodes noteCount to 0 in
mapNode; instead read the real count exposed by the bridge or omit the field.
Update the TreeNode type and the mapNode mapping inside getNotebookTree to use
the bridge-provided value (e.g., node.notebook.noteCount) for noteCount and keep
childCount computed from node.children.length (or if the bridge does not provide
noteCount, remove noteCount from the returned shape). Ensure you reference
getNotebookTree, the local TreeNode type, mapNode and
window.readied.notebooks.tree() when making the change.
- Around line 223-233: searchNotes currently ignores options.notebookId and
countNotes ignores options entirely; update searchNotes (the async
searchNotes(query, options) method) to pass through options.notebookId (and
preserve options?.limit ?? 20) to window.readied.notes.search so the backend is
scoped correctly, and change countNotes to accept an options parameter and
forward it to window.readied.notes.count (e.g.,
window.readied.notes.count(options)) so counts match the same query scope; keep
existing return shapes unchanged.
- Around line 196-216: getNotes is currently applying limit/offset when calling
window.readied.notes.list and then filtering the page, which yields incorrect
paging and total; change getNotes to fetch the unpaged list (call
window.readied.notes.list without limit/offset or with a flag to fetch all),
apply notebookId/status/isPinned filters to produce the full filtered array,
compute total = filtered.length, then apply limit/offset slicing to the filtered
array before mapping to the returned notes shape—refer to getNotes and
window.readied.notes.list to locate the call to adjust.
- Around line 197-203: The code replaces sortBy: 'wordCount' with 'updatedAt'
before calling window.readied.notes.list, changing requested semantics; instead
keep the original options when calling window.readied.notes.list and, if
options.sortBy === 'wordCount', post-process the returned notes array (the notes
variable) to sort by each note's wordCount field according to options.sortOrder
before using it, or alternatively return an error/throw when the bridge doesn't
support wordCount sorting; update the block around window.readied.notes.list and
the variable notes to implement client-side sorting (or rejection) rather than
remapping to 'updatedAt'.
- Around line 325-327: The code only calls dataAPI._notifyNotesChanged on
create/delete; update paths (title edits, content saves, moves, pin/archive
toggles, status changes) must also emit notifications so plugin caches stay in
sync: locate all handlers that mutate note data (the createNote callback and the
other mutation handlers referenced near the create block and around the region
flagged at 434-437), and add calls to dataAPI._notifyNotesChanged({ kind:
'note', action: '<appropriate-action>', id: note.id }) after each successful
mutation — using action values like 'updated' for edits/saves, 'moved' for
moves, 'pinned'/'unpinned', 'archived'/'unarchived' or more specific actions as
appropriate — and ensure appAPI._notifyNoteCreated / deleted patterns are
mirrored for these update handlers so subscribers receive updates consistently.

In `@docs/plans/2026-03-11-data-access-api-design.md`:
- Around line 15-23: The fenced code block showing the data-access flow is
unlabeled and triggers MD040; add a language tag (e.g., "text") to the opening
fence so the block is a labeled plain-text snippet. Update the block that
documents createDataAPI(), DataAPIBridge, window.readied.notes.list,
ipcRenderer.invoke('notes:list'), and SQLiteNoteRepository.list to start with
```text (or another appropriate language) instead of just ``` so the markdown
linter stops flagging it.

In `@docs/plans/2026-03-11-data-access-api-implementation.md`:
- Line 636: Update the wording in the docs to use "same" instead of "exact same"
in the sentence describing the pattern for appAPI; specifically edit the
sentence that reads "Follow the exact same pattern as `appAPI` — it's passed
into `activate()`, tracked for event cleanup, and injected into the `context`
object." to "Follow the same pattern as `appAPI` — it's passed into
`activate()`, tracked for event cleanup, and injected into the `context`
object." Keep references to `PluginRegistry.activate()` and the new `dataAPI`
parameter untouched.
- Around line 1-13: The markdown skips an H2 between the document title and
tasks; update the heading hierarchy by either inserting an H2 before the task
list (e.g., a section like "## Tasks" after "Data Access API Implementation
Plan") or demoting "### Task 1" (and subsequent task headings) to H2 (change
"### Task 1" to "## Task 1") so the heading progression follows H1 → H2 → H3 and
satisfies markdown lint rules.

In `@packages/plugin-api/src/data/createDataAPI.ts`:
- Around line 3-24: Combine the two import statements at the top of
createDataAPI.ts into a single import so types and the runtime value are grouped
together: merge the long type-only import list (NoteInfo, NoteSummaryInfo,
NotebookInfo, NoteQueryOptions, NoteQueryResult, SearchOptions, SearchResult,
NotebookQueryOptions, NotebookDetailInfo, NotebookTreeNode, NotebookResult,
TagQueryOptions, TagInfo, GraphQueryOptions, LinkInfo, OutgoingLinkInfo,
GraphData, DataChangeEvent) with the DataAccessError import so there is no blank
line between import groups and the linter warning is resolved.
- Around line 212-222: The _notify* methods (_notifyNotesChanged,
_notifyNotebooksChanged, _notifyTagsChanged) iterate directly over listener Sets
(notesChangedListeners, notebooksChangedListeners, tagsChangedListeners) which
can misbehave if listeners are added/removed during dispatch; fix by taking a
snapshot of each Set (e.g., Array.from(...) or [...set]) and iterate over that
snapshot so additions/removals during callbacks won’t affect the current
dispatch order or cause skipped callbacks.

In `@packages/plugin-api/src/data/dataTypes.ts`:
- Around line 68-87: GraphQueryOptions.depth is declared but ignored by
getGraphData in createDataAPI.ts; update getGraphData to respect
GraphQueryOptions.depth by performing a breadth‑first traversal from initial
nodes (filtered by notebookId if provided), track visited node IDs, stop
expanding neighbors once the specified depth is reached, and collect nodes and
edges into the GraphData result; alternatively, if you prefer not to implement
traversal now, update the GraphQueryOptions or getGraphData JSDoc comment to
state that depth is reserved for future use so callers know it is currently
ignored. Ensure you reference GraphQueryOptions.depth, getGraphData, and the
GraphData node/edge shapes when making the change.

---

Outside diff comments:
In `@packages/plugin-api/tests/registry.test.ts`:
- Around line 103-127: The test "it('passes context with all expected APIs')"
never asserts PluginContext.data; update the test after receiving
receivedContext (cast to ctx) to assert ctx has a data property and that it is
the DataAPI passed into registry.activate (use the existing mockDataAPI), e.g.
add assertions like expect(ctx).toHaveProperty('data') and
expect(ctx.data).toBe(mockDataAPI) so the activation contract verifies the
DataAPI is routed through registry.activate/activate.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: dbe0d8d5-5555-4109-8c3d-054be1b4a1a1

📥 Commits

Reviewing files that changed from the base of the PR and between 73f9938 and 589fe17.

📒 Files selected for processing (12)
  • apps/desktop/src/renderer/App.tsx
  • docs/plans/2026-03-11-data-access-api-design.md
  • docs/plans/2026-03-11-data-access-api-implementation.md
  • packages/plugin-api/src/data/createDataAPI.ts
  • packages/plugin-api/src/data/dataTypes.ts
  • packages/plugin-api/src/index.ts
  • packages/plugin-api/src/lifecycle/PluginHost.tsx
  • packages/plugin-api/src/lifecycle/PluginRegistry.ts
  • packages/plugin-api/src/types.ts
  • packages/plugin-api/tests/createDataAPI.test.ts
  • packages/plugin-api/tests/dataTypes.test.ts
  • packages/plugin-api/tests/registry.test.ts

Comment on lines +196 to +216
async getNotes(options) {
const notes = await window.readied.notes.list(options ? {
limit: options.limit,
offset: options.offset,
tag: options.tag,
sortBy: options.sortBy === 'wordCount' ? 'updatedAt' : options.sortBy,
sortOrder: options.sortOrder,
} : undefined);
let filtered = notes;
if (options?.notebookId) filtered = filtered.filter(n => n.notebookId === options.notebookId);
if (options?.status) filtered = filtered.filter(n => n.status === options.status);
if (options?.isPinned !== undefined) filtered = filtered.filter(n => n.isPinned === options.isPinned);
return {
notes: filtered.map(n => ({
id: n.id, title: n.title, notebookId: n.notebookId,
tags: [...n.tags], wordCount: n.wordCount,
createdAt: n.createdAt, updatedAt: n.updatedAt,
isPinned: n.isPinned, status: n.status,
})),
total: filtered.length,
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Don't apply limit/offset before notebookId/status/isPinned filtering.

This paginates the global note list and only then narrows it, so filtered queries can return short or empty pages even when more matches exist later. total also becomes page-local here, which makes hasMore wrong downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 196 - 216, getNotes is
currently applying limit/offset when calling window.readied.notes.list and then
filtering the page, which yields incorrect paging and total; change getNotes to
fetch the unpaged list (call window.readied.notes.list without limit/offset or
with a flag to fetch all), apply notebookId/status/isPinned filters to produce
the full filtered array, compute total = filtered.length, then apply
limit/offset slicing to the filtered array before mapping to the returned notes
shape—refer to getNotes and window.readied.notes.list to locate the call to
adjust.

Comment thread apps/desktop/src/renderer/App.tsx Outdated
Comment on lines +197 to +203
const notes = await window.readied.notes.list(options ? {
limit: options.limit,
offset: options.offset,
tag: options.tag,
sortBy: options.sortBy === 'wordCount' ? 'updatedAt' : options.sortBy,
sortOrder: options.sortOrder,
} : undefined);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Preserve sortBy: 'wordCount' semantics.

Rewriting wordCount to updatedAt means plugins get a different order than they requested. Either sort by wordCount client-side here or reject the option until the bridge can support it.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 197 - 203, The code replaces
sortBy: 'wordCount' with 'updatedAt' before calling window.readied.notes.list,
changing requested semantics; instead keep the original options when calling
window.readied.notes.list and, if options.sortBy === 'wordCount', post-process
the returned notes array (the notes variable) to sort by each note's wordCount
field according to options.sortOrder before using it, or alternatively return an
error/throw when the bridge doesn't support wordCount sorting; update the block
around window.readied.notes.list and the variable notes to implement client-side
sorting (or rejection) rather than remapping to 'updatedAt'.

Comment on lines +223 to +233
async searchNotes(query, options) {
const notes = await window.readied.notes.search(query, options?.limit ?? 20);
return {
results: notes.map(n => ({ id: n.id, title: n.title })),
total: notes.length,
};
},
async countNotes() {
const counts = await window.readied.notes.count();
return counts.total;
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Honor the query options in searchNotes and countNotes.

searchNotes ignores options.notebookId, and countNotes ignores options entirely. Any plugin that scopes a query and then asks for the matching count will get inconsistent results.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 223 - 233, searchNotes
currently ignores options.notebookId and countNotes ignores options entirely;
update searchNotes (the async searchNotes(query, options) method) to pass
through options.notebookId (and preserve options?.limit ?? 20) to
window.readied.notes.search so the backend is scoped correctly, and change
countNotes to accept an options parameter and forward it to
window.readied.notes.count (e.g., window.readied.notes.count(options)) so counts
match the same query scope; keep existing return shapes unchanged.

Comment on lines +238 to +249
async getNotebookTree() {
type TreeNode = { id: string; name: string; parentId: string | null; noteCount: number; childCount: number; children: TreeNode[] };
const tree = await window.readied.notebooks.tree();
const mapNode = (node: { notebook: { id: string; name: string; parentId: string | null }; children: unknown[] }): TreeNode => ({
id: node.notebook.id,
name: node.notebook.name,
parentId: node.notebook.parentId,
noteCount: 0,
childCount: node.children.length,
children: (node.children as typeof tree).map(mapNode),
});
return tree.map(mapNode);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't hardcode noteCount to zero in tree results.

NotebookTreeNode exposes counts, so returning 0 for every node makes getNotebooks({ tree: true }) lie about notebook metadata. This needs real counts from the bridge, or the field should stay out of the public result until it can be populated.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 238 - 249, getNotebookTree
currently hardcodes noteCount to 0 in mapNode; instead read the real count
exposed by the bridge or omit the field. Update the TreeNode type and the
mapNode mapping inside getNotebookTree to use the bridge-provided value (e.g.,
node.notebook.noteCount) for noteCount and keep childCount computed from
node.children.length (or if the bridge does not provide noteCount, remove
noteCount from the returned shape). Ensure you reference getNotebookTree, the
local TreeNode type, mapNode and window.readied.notebooks.tree() when making the
change.

Comment on lines 325 to +327
appAPI._notifyNoteCreated({ id: newNote.id, title: newNote.title, content: newNote.content });
}, [createNote, selectedNotebookId, clearSearch, appAPI]);
dataAPI._notifyNotesChanged({ kind: 'note', action: 'created', id: newNote.id });
}, [createNote, selectedNotebookId, clearSearch, appAPI, dataAPI]);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Emit onNotesChanged for update paths too.

This file only notifies DataAPI subscribers on create/delete. Title edits, content saves, moves, pin/archive changes, and status changes in the handlers below mutate note data without any corresponding event, so plugin caches will drift out of date.

Also applies to: 434-437

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/desktop/src/renderer/App.tsx` around lines 325 - 327, The code only
calls dataAPI._notifyNotesChanged on create/delete; update paths (title edits,
content saves, moves, pin/archive toggles, status changes) must also emit
notifications so plugin caches stay in sync: locate all handlers that mutate
note data (the createNote callback and the other mutation handlers referenced
near the create block and around the region flagged at 434-437), and add calls
to dataAPI._notifyNotesChanged({ kind: 'note', action: '<appropriate-action>',
id: note.id }) after each successful mutation — using action values like
'updated' for edits/saves, 'moved' for moves, 'pinned'/'unpinned',
'archived'/'unarchived' or more specific actions as appropriate — and ensure
appAPI._notifyNoteCreated / deleted patterns are mirrored for these update
handlers so subscribers receive updates consistently.

Comment on lines +1 to +13
# Data Access API Implementation Plan

> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

**Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling).

**Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat.

**Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest

---

### Task 1: Data types — query options and result types

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Fix heading level progression for markdown lint compliance.

The document jumps from # Data Access API Implementation Plan (h1) directly to ### Task 1 (h3), skipping h2. This violates markdown heading increment rules.

📝 Proposed fix

Either add a section header before the tasks:

 ---
 
+## Tasks
+
 ### Task 1: Data types — query options and result types

Or change tasks to h2 level:

-### Task 1: Data types — query options and result types
+## Task 1: Data types — query options and result types
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Data Access API Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling).
**Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat.
**Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest
---
### Task 1: Data types — query options and result types
# Data Access API Implementation Plan
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
**Goal:** Add a dedicated `context.data` namespace to the plugin API with rich query capabilities (filters, sorting, pagination, graph, events, error handling).
**Architecture:** A `DataAPI` interface with typed query options, backed by a `DataAPIBridge` that maps to existing `window.readied.*` IPC calls. The `createDataAPI()` factory handles option merging, error wrapping, and event dispatch. `AppAPI` stays unchanged for backward compat.
**Tech Stack:** TypeScript, Zustand patterns, Electron IPC, vitest
---
## Task 1: Data types — query options and result types
🧰 Tools
🪛 markdownlint-cli2 (0.21.0)

[warning] 13-13: Heading levels should only increment by one level at a time
Expected: h2; Actual: h3

(MD001, heading-increment)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-11-data-access-api-implementation.md` around lines 1 - 13,
The markdown skips an H2 between the document title and tasks; update the
heading hierarchy by either inserting an H2 before the task list (e.g., a
section like "## Tasks" after "Data Access API Implementation Plan") or demoting
"### Task 1" (and subsequent task headings) to H2 (change "### Task 1" to "##
Task 1") so the heading progression follows H1 → H2 → H3 and satisfies markdown
lint rules.

- Modify: `packages/plugin-api/src/lifecycle/PluginRegistry.ts:90-98` (add `dataAPI` param to `activate()`)
- Modify: `packages/plugin-api/src/lifecycle/PluginRegistry.ts:228-290` (add `data: trackedData` to context)

**Context:** Follow the exact same pattern as `appAPI` — it's passed into `activate()`, tracked for event cleanup, and injected into the `context` object. The `PluginRegistry.activate()` signature gets a new `dataAPI` parameter.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Minor style suggestion from static analysis.

"exact same" could be simplified to "same" for conciseness:

Follow the same pattern as appAPI...

This is a very minor style nit.

🧰 Tools
🪛 LanguageTool

[style] ~636-~636: ‘exact same’ might be wordy. Consider a shorter alternative.
Context: ...ato context) **Context:** Follow the exact same pattern asappAPI` — it's passed into ...

(EN_WORDINESS_PREMIUM_EXACT_SAME)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@docs/plans/2026-03-11-data-access-api-implementation.md` at line 636, Update
the wording in the docs to use "same" instead of "exact same" in the sentence
describing the pattern for appAPI; specifically edit the sentence that reads
"Follow the exact same pattern as `appAPI` — it's passed into `activate()`,
tracked for event cleanup, and injected into the `context` object." to "Follow
the same pattern as `appAPI` — it's passed into `activate()`, tracked for event
cleanup, and injected into the `context` object." Keep references to
`PluginRegistry.activate()` and the new `dataAPI` parameter untouched.

Comment on lines +3 to +24
import type {
NoteInfo,
NoteSummaryInfo,
NotebookInfo,
NoteQueryOptions,
NoteQueryResult,
SearchOptions,
SearchResult,
NotebookQueryOptions,
NotebookDetailInfo,
NotebookTreeNode,
NotebookResult,
TagQueryOptions,
TagInfo,
GraphQueryOptions,
LinkInfo,
OutgoingLinkInfo,
GraphData,
DataChangeEvent,
} from './dataTypes';

import { DataAccessError } from './dataTypes';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Address linting warning: Remove empty line between import groups.

Static analysis flagged an empty line between import groups. The type imports and value import should be grouped together.

🔧 Proposed fix
 import type {
   NoteInfo,
   NoteSummaryInfo,
   NotebookInfo,
   NoteQueryOptions,
   NoteQueryResult,
   SearchOptions,
   SearchResult,
   NotebookQueryOptions,
   NotebookDetailInfo,
   NotebookTreeNode,
   NotebookResult,
   TagQueryOptions,
   TagInfo,
   GraphQueryOptions,
   LinkInfo,
   OutgoingLinkInfo,
   GraphData,
   DataChangeEvent,
 } from './dataTypes';
-
 import { DataAccessError } from './dataTypes';
🧰 Tools
🪛 GitHub Check: lint

[warning] 3-3:
There should be no empty line between import groups

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-api/src/data/createDataAPI.ts` around lines 3 - 24, Combine
the two import statements at the top of createDataAPI.ts into a single import so
types and the runtime value are grouped together: merge the long type-only
import list (NoteInfo, NoteSummaryInfo, NotebookInfo, NoteQueryOptions,
NoteQueryResult, SearchOptions, SearchResult, NotebookQueryOptions,
NotebookDetailInfo, NotebookTreeNode, NotebookResult, TagQueryOptions, TagInfo,
GraphQueryOptions, LinkInfo, OutgoingLinkInfo, GraphData, DataChangeEvent) with
the DataAccessError import so there is no blank line between import groups and
the linter warning is resolved.

Comment on lines +212 to +222
_notifyNotesChanged(event) {
for (const cb of notesChangedListeners) cb(event);
},

_notifyNotebooksChanged(event) {
for (const cb of notebooksChangedListeners) cb(event);
},

_notifyTagsChanged(event) {
for (const cb of tagsChangedListeners) cb(event);
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Potential issue: Concurrent modification during event dispatch.

If a callback adds or removes listeners during _notify* iteration, JavaScript's Set iteration behavior may cause unexpected results (new additions won't be visited, but removals might cause skips depending on iteration state).

Consider iterating over a snapshot of the listeners:

🛡️ Proposed defensive fix
     _notifyNotesChanged(event) {
-      for (const cb of notesChangedListeners) cb(event);
+      for (const cb of [...notesChangedListeners]) cb(event);
     },

     _notifyNotebooksChanged(event) {
-      for (const cb of notebooksChangedListeners) cb(event);
+      for (const cb of [...notebooksChangedListeners]) cb(event);
     },

     _notifyTagsChanged(event) {
-      for (const cb of tagsChangedListeners) cb(event);
+      for (const cb of [...tagsChangedListeners]) cb(event);
     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-api/src/data/createDataAPI.ts` around lines 212 - 222, The
_notify* methods (_notifyNotesChanged, _notifyNotebooksChanged,
_notifyTagsChanged) iterate directly over listener Sets (notesChangedListeners,
notebooksChangedListeners, tagsChangedListeners) which can misbehave if
listeners are added/removed during dispatch; fix by taking a snapshot of each
Set (e.g., Array.from(...) or [...set]) and iterate over that snapshot so
additions/removals during callbacks won’t affect the current dispatch order or
cause skipped callbacks.

Comment on lines +68 to +87
export interface GraphQueryOptions {
notebookId?: string;
depth?: number;
}

export interface LinkInfo {
noteId: string;
noteTitle: string;
}

export interface OutgoingLinkInfo {
targetId: string | null;
targetTitle: string;
resolved: boolean;
}

export interface GraphData {
nodes: Array<{ id: string; title: string; notebookId: string }>;
edges: Array<{ source: string; target: string }>;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Note: depth option is declared but not implemented.

The GraphQueryOptions.depth property is defined but the createDataAPI.ts implementation only filters by notebookId. Consider either:

  1. Documenting that depth is reserved for future use
  2. Implementing depth-limited traversal in getGraphData

This isn't blocking since the type is forward-compatible.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/plugin-api/src/data/dataTypes.ts` around lines 68 - 87,
GraphQueryOptions.depth is declared but ignored by getGraphData in
createDataAPI.ts; update getGraphData to respect GraphQueryOptions.depth by
performing a breadth‑first traversal from initial nodes (filtered by notebookId
if provided), track visited node IDs, stop expanding neighbors once the
specified depth is reached, and collect nodes and edges into the GraphData
result; alternatively, if you prefer not to implement traversal now, update the
GraphQueryOptions or getGraphData JSDoc comment to state that depth is reserved
for future use so callers know it is currently ignored. Ensure you reference
GraphQueryOptions.depth, getGraphData, and the GraphData node/edge shapes when
making the change.

tomymaritano and others added 3 commits March 11, 2026 20:58
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix pagination: filter before applying limit/offset for correct totals
- Add client-side sort for wordCount (not supported by bridge)
- Use notebook.noteCount from bridge when available in tree view

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts:
#	packages/storage-sqlite/src/repositories/SQLiteNoteRepository.ts
@tomymaritano tomymaritano merged commit b429ab3 into develop Mar 12, 2026
@tomymaritano tomymaritano deleted the feature/data-access-api branch March 12, 2026 00:33
@coderabbitai coderabbitai Bot mentioned this pull request Mar 13, 2026
7 tasks
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.

1 participant