Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@mdbase-dev/ui": "0.1.0-beta.123",
"@mdbase-dev/ui": "0.1.0-beta.124",
"@mdbase-reader/connect": "workspace:*",
"@mdbase-reader/core": "workspace:*",
"@mdbase-reader/platform": "workspace:*",
Expand Down
2 changes: 1 addition & 1 deletion apps/reader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
},
"dependencies": {
"@citation-js/plugin-csl": "0.8.2",
"@mdbase-dev/ui": "0.1.0-beta.123",
"@mdbase-dev/ui": "0.1.0-beta.124",
"@mdbase-reader/connect": "workspace:*",
"@mdbase-reader/core": "workspace:*",
"@mdbase-reader/markdown-editor": "workspace:*",
Expand Down
29 changes: 26 additions & 3 deletions apps/reader/scripts/audit-annotation-workbench.mjs
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { expect } from "@playwright/test";

export async function auditAnnotationWorkbench(page, { screenshot, open }) {
export async function auditAnnotationWorkbench(page, { screenshot, open, blockWrites }) {
const tools = page.getByRole("complementary", { name: "Source workspace" });
await tools.getByRole("button", { name: "Edit", exact: true }).first().focus();
await page.keyboard.press("Enter");
await expect(page.getByRole("button", { name: "Back to reading position" })).toHaveCount(0);
const editor = tools.getByRole("textbox", { name: "Comment" });
blockWrites(true);
await editor.fill("[test] Dock-safe annotation draft.");
await expect(tools.getByRole("alert")).toContainText("offline");
const annotationId = await tools
.locator(".annotation-card.is-editing")
.getAttribute("data-annotation-id");
Expand All @@ -16,13 +18,24 @@ export async function auditAnnotationWorkbench(page, { screenshot, open }) {
.filter({ has: page.locator(".dv-default-tab-content", { hasText: "Annotations —" }) });
const panelId = await tab.getAttribute("data-panel-id");
const workbench = page.locator(`[data-session-id="${panelId}"]`);
// Promotion deliberately closes the native sidebar and releases its editor lease.
// The memory-only buffer survives; there is no mounted writer to transfer from yet.
await expect(tools).toHaveCount(0);
await workbench
.locator(`[data-annotation-id="${annotationId}"]`)
.getByRole("button", { name: "Edit here", exact: true })
.getByRole("button", { name: "Resume edits", exact: true })
.click();
const otherEditor = workbench.getByRole("textbox", { name: "Comment" });
await expect(otherEditor).toHaveValue(/Dock-safe annotation draft/u);
await expect(editor).toHaveCount(0);
blockWrites(false);
await workbench.getByRole("button", { name: "Retry save", exact: true }).click();
await expect(otherEditor).toHaveCount(0);
await workbench
.locator(`[data-annotation-id="${annotationId}"]`)
.getByRole("button", { name: "Edit", exact: true })
.click();
await expect(otherEditor).toHaveValue(/Dock-safe annotation draft/u);
const menu = async (label) => {
await tab.click({ button: "right" });
await page.getByRole("menuitem", { name: label, exact: true }).click();
Expand All @@ -35,6 +48,16 @@ export async function auditAnnotationWorkbench(page, { screenshot, open }) {
await tab.click();
if (!(await tools.isVisible()))
await page.getByRole("button", { name: "Toggle right sidebar" }).click();
// Reopening the sidebar remounts its locally selected editor. Establish the workbench's
// ownership explicitly before testing transfer back, rather than assuming a hidden lease.
await expect(editor).toHaveValue(/Dock-safe annotation draft/u);
await expect(otherEditor).toHaveCount(0);
await workbench
.locator(`[data-annotation-id="${annotationId}"]`)
.getByRole("button", { name: "Edit here", exact: true })
.click();
await expect(otherEditor).toHaveValue(/Dock-safe annotation draft/u);
await expect(editor).toHaveCount(0);
await tools
.locator(`[data-annotation-id="${annotationId}"]`)
.getByRole("button", { name: "Edit here", exact: true })
Expand Down Expand Up @@ -76,7 +99,7 @@ export async function auditAnnotationWorkbench(page, { screenshot, open }) {
await page.reload();
await expect(tools.locator(".annotation-card")).toHaveCount(1);
return [
"Annotation editing transfers explicitly between inspector and workbench; docking preserves the draft without simultaneous writers",
"Annotation promotion resumes an uncommitted buffer after the sidebar closes; Edit here transfers the single editor back after docking",
"Linked note insertion, deletion warning, cancellation, and deletion persistence work",
];
}
2 changes: 1 addition & 1 deletion apps/reader/scripts/audit-annotations.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function auditAnnotations(page, { screenshot, blockWrites }) {
completed.push(
"Local annotation search, comment filtering, sort controls, and return-to-reading work",
);
completed.push(...(await auditAnnotationWorkbench(page, { screenshot, open })));
completed.push(...(await auditAnnotationWorkbench(page, { screenshot, open, blockWrites })));
await expect(page.frameLocator("iframe.html-viewer:visible").locator("h1")).toBeVisible();
await page.emulateMedia({ colorScheme: "dark", reducedMotion: "reduce" });
await screenshot("annotation-dark-inspector");
Expand Down
28 changes: 21 additions & 7 deletions docs/annotation-loading-performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@ selection performs no annotation queries or reads. Changing structural filters l
collection for local filtering; typing a search does not broaden the saved view's selection.
If view execution fails, Reader explains the fallback and applies its filters locally.

The Connect annotation repository queries in bounded scopes of 100 paths. Whole-record reads
remain limited to four concurrent workers. It hydrates a small initial batch of 16, then
The Connect annotation repository queries in bounded scopes of 100 paths. With beta.124,
`read-many-documents-v1` authorities hydrate editable annotations with revision-bearing
`readMany` projections, removing redundant per-record revision reads. Older authorities retain
whole-record reads limited to four concurrent workers. It hydrates a small initial batch of 16, then
batches of up to 64, hydrating each query page before requesting the next. Early cumulative
snapshots are published through 128 annotations; afterward, publication waits for roughly 25%
growth, with an immediate final snapshot. This bounds cumulative copying without delaying the
Expand All @@ -20,10 +22,14 @@ source panes. Concurrent readers of the same path share one request. Cancelling
does not cancel others; cancelling the last subscriber aborts the underlying read. Failures and
abandoned responses are not cached.

The cache holds at most 2,000 records for at most 15 seconds. Query results do not expose record
revisions, so this is bounded reuse, **not** indefinite revision-validated caching. Queries still
The legacy cache holds at most 2,000 records for at most 15 seconds. Revisionless query rows
must match every observable content/file fact before reuse; an advertised revision must also
match. This is bounded reuse, **not** indefinite semantic-cache validation. Qualified batches
install their own content/revision pair without casting projections into full documents. Queries still
run for each load to determine membership. Local creates and body updates install their returned
record revisions immediately; deletes evict records and abort pending reads. A save overtaking a
record revisions immediately; deletes evict records and abort pending reads. A source reference changed
between discovery and hydration is resolved again before mapping. Qualified batch errors remain
visible; missing records are omitted rather than retried as point reads. A save overtaking a
read cannot be overwritten by that read's older response.

Use **View options → Refresh annotations** to rerun the saved view and bypass cached bodies
Expand All @@ -43,5 +49,13 @@ source-cache completeness, count subscriptions, and search within saved selectio
Opt-in Connect diagnostics now time `list-views` and `execute-view` alongside record reads and
query pages. Compare cold and warm loads, time to first visible batch, total time, and whole-record
read counts. For a saved view selecting 20 records from a collection of 1,000, cold hydration now
requires 20 whole-record reads rather than 1,000. This is a request-count reduction, not a measured
latency claim; authority query costs and the active transport still need runtime profiling.
requires 20 legacy whole-record reads rather than 1,000. On qualified authorities, the 16/64
progressive batching fixture hydrates 130 annotations in three `readMany` calls and zero point
reads. These are consumer fixture counts, not a measured latency or wire-request claim (typed
batches also preselect membership on the authority).

Path/ID and annotation-source discovery uses `output: "metadata"` only after positive
`supportsAuthorityFeature("query-metadata-v1")` evidence; legacy ordinary queries remain.
Library views still retain full frontmatter for arbitrary columns. Document reopening and file
exports use SDK `files.stat`, including its negotiated legacy listing fallback, instead of Reader
folder enumeration/caches. Every lookup refreshes metadata; downloads remain revision-pinned.
21 changes: 21 additions & 0 deletions docs/reader-improvement-audit.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,27 @@ arrangement recovery. The active annotation stripe is also removed.
This follow-up is deployed as `ca73f7765cee-production-mu3t04ql`; live metadata,
manifest, HTML, entry assets and production backend configuration were verified.

## SDK beta.124 / consumer canary

The pre-existing annotation-workbench `Edit here` timeout was a **stale audit expectation**,
not a Connect regression. Promoting a tool deliberately closes the native sidebar
(`ReaderWorkspaceView.onPromote`), unmounting its editor and releasing the lease. The retained
memory-only buffer therefore offers **Resume edits**, not **Edit here**. Reopening the sidebar
remounts its selected editor and claims the lease again.

The audit now blocks saves to prove that promotion retains an uncommitted buffer, explicitly
resumes/retries it, and establishes ownership after reopening the sidebar before asserting
**Edit here** transfers in both directions. The single-textbox and exact-buffer assertions remain;
no app behavior, timing allowance, or SDK failure is bypassed.

Validation: full fixture audit **40 scenarios passed** (`/tmp/reader-audit-10fLSX`);
annotation-only **13 passed** (`/tmp/reader-audit-KHOR5i`); both PDF-touch scripts passed
(`/tmp/pdf-touch-handles-f7AizA`, `/tmp/reader-audit-Qlo2WE`). An earlier full run hit the native
touch-docking assertion and an earlier PDF-touch run missed the selection toolbar; reruns passed
without changes to those checks. Accessibility still reports one command-palette contrast finding
(`button[role="option"][type="button"]:nth-child(2) > span > small`), outside this change.
These are isolated fixture audits, not authenticated LAB acceptance. Owned browsers are closed.

## Validation and limits

Workspace tests, typechecking, production builds, architecture checks and spec
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"devDependencies": {
"@callumalpass/mdbase": "0.3.0-rc.5",
"@eslint/js": "^9.39.2",
"@mdbase-dev/connect-dev": "0.1.0-beta.123",
"@mdbase-dev/connect-dev": "0.1.0-beta.124",
"@types/node": "^22.19.11",
"eslint": "^9.39.2",
"eslint-import-resolver-typescript": "^4.4.5",
Expand Down
2 changes: 1 addition & 1 deletion packages/connect/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
"typecheck": "tsc -p tsconfig.json"
},
"dependencies": {
"@mdbase-dev/connect": "0.1.0-beta.123",
"@mdbase-dev/connect": "0.1.0-beta.124",
"@mdbase-reader/core": "workspace:*",
"@mdbase-reader/migration": "workspace:*",
"yaml": "^2.9.0"
Expand Down
8 changes: 8 additions & 0 deletions packages/connect/src/annotation-concurrency.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import { ConnectAnnotationRepository } from "./annotation-repository.js";
import type { ReaderConnectClient } from "./repository-client.js";
import type { ConnectOutcome, QueryPage, RecordDocument } from "@mdbase-dev/connect";

const legacyAuthorityFeatures = {
supportsAuthorityFeature: vi.fn(() =>
Promise.resolve({ ok: true as const, value: false, diagnostics: [] }),
),
};

function success<Value>(value: Value): ConnectOutcome<Value> {
return { ok: true, value, diagnostics: [] };
}
Expand Down Expand Up @@ -61,6 +67,7 @@ describe("Connect annotation concurrency", () => {
return success(annotationDocument(input.path));
});
const repository = new ConnectAnnotationRepository({
...legacyAuthorityFeatures,
queryPages,
read,
} as unknown as ReaderConnectClient);
Expand Down Expand Up @@ -104,6 +111,7 @@ describe("Connect annotation listing", () => {
);
});
const repository = new ConnectAnnotationRepository({
...legacyAuthorityFeatures,
queryPages,
read,
} as unknown as ReaderConnectClient);
Expand Down
24 changes: 18 additions & 6 deletions packages/connect/src/annotation-loading.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,13 @@ import { describe, expect, it, vi, type Mock } from "vitest";
import { ConnectAnnotationRepository } from "./annotation-repository.js";

import type { ReaderConnectClient } from "./repository-client.js";
import type { ConnectOutcome, QueryPage, RecordDocument } from "@mdbase-dev/connect";
import type { ConnectOutcome, QueryInput, QueryPage, RecordDocument } from "@mdbase-dev/connect";

const legacyAuthorityFeatures = {
supportsAuthorityFeature: vi.fn(() =>
Promise.resolve({ ok: true as const, value: false, diagnostics: [] }),
),
};

const collection = collectionId("reading");
function document(index: number): RecordDocument {
Expand Down Expand Up @@ -33,19 +39,25 @@ function page(documents: RecordDocument[]): ConnectOutcome<QueryPage> {

function repository(documents: RecordDocument[]): {
read: Mock<ReaderConnectClient["read"]>;
queryPages: Mock<ReaderConnectClient["queryPages"]>;
queryPages: Mock<(input: QueryInput) => AsyncGenerator<ConnectOutcome<QueryPage>>>;
repo: ConnectAnnotationRepository;
} {
const read = vi.fn<ReaderConnectClient["read"]>(({ path }) =>
Promise.resolve(ok(documents.find((entry) => entry.path === path)!)),
);
const queryPages = vi.fn<ReaderConnectClient["queryPages"]>(async function* () {
yield await Promise.resolve(page(documents));
});
const queryPages = vi.fn<(input: QueryInput) => AsyncGenerator<ConnectOutcome<QueryPage>>>(
async function* () {
yield await Promise.resolve(page(documents));
},
);
return {
read,
queryPages,
repo: new ConnectAnnotationRepository({ read, queryPages } as unknown as ReaderConnectClient),
repo: new ConnectAnnotationRepository({
...legacyAuthorityFeatures,
read,
queryPages,
} as unknown as ReaderConnectClient),
};
}

Expand Down
15 changes: 13 additions & 2 deletions packages/connect/src/annotation-query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@ import { annotationPathsForSource } from "./annotation-query.js";
import type { ReaderConnectClient } from "./repository-client.js";
import type { QueryInput } from "@mdbase-dev/connect";

const legacyAuthorityFeatures = {
supportsAuthorityFeature: vi.fn(() =>
Promise.resolve({ ok: true as const, value: false, diagnostics: [] }),
),
};

it("asks mdbase which links reach the source, keeps legacy IDs, and follows renames", async () => {
let sourcePath = 'sources/A "quoted" title.md';
// How mdbase resolves each link: a record path, or null when it reaches no record.
Expand All @@ -31,11 +37,13 @@ it("asks mdbase which links reach the source, keeps legacy IDs, and follows rena
const references = Object.keys(links);
const results =
id !== undefined
? [{ path: sourcePath, effectiveFrontmatter: { id: "src_1" } }]
? [{ path: sourcePath, effectiveFrontmatter: { id: "src_1" }, file: {}, types: [] }]
: references
.map((source, i) => ({
path: `annotations/${String(i)}.md`,
effectiveFrontmatter: { source },
file: {},
types: ["reader-annotation"],
}))
.filter(({ effectiveFrontmatter: { source } }) =>
target !== undefined
Expand All @@ -44,7 +52,10 @@ it("asks mdbase which links reach the source, keeps legacy IDs, and follows rena
);
yield await Promise.resolve({ ok: true, value: { results }, diagnostics: [] });
});
const client = { queryPages } as unknown as ReaderConnectClient;
const client = {
...legacyAuthorityFeatures,
queryPages,
} as unknown as ReaderConnectClient;
const controller = new AbortController();
const options = { signal: controller.signal };
expect(await annotationPathsForSource(client, sourceId("src_1"), options)).toEqual([
Expand Down
Loading