Skip to content

ENG-1855 Add Roam shared-node import discovery#1214

Open
sid597 wants to merge 9 commits into
mainfrom
eng-1855-add-roam-shared-node-import-discovery-v2
Open

ENG-1855 Add Roam shared-node import discovery#1214
sid597 wants to merge 9 commits into
mainfrom
eng-1855-add-roam-shared-node-import-discovery-v2

Conversation

@sid597

@sid597 sid597 commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

https://www.loom.com/share/7210cfc4b3984f2ab927e7092dfab9c1

Summary

Adds a read-only Roam discovery surface for group-visible shared nodes.

  • Starts from the current user's group grants in ResourceAccess, so discovery reflects shared persistence rather than direct source-app access or every resource the user can read.
  • Resolves the source app, source space, direct title, optional source-local ID, and latest upstream modified time from my_spaces, my_concepts, and my_contents.
  • Requires an instance concept with a node type plus both direct and typed full content before a resource is shown.
  • Builds a stable source RID from the source-space URL and source-local ID, then marks matching discourse-graph.importedFrom.sourceNodeRid pages as imported.
  • Adds DG: Discover shared nodes with Blueprint loading, retry, empty, search, reload, and status states.
  • The dialog sets autoFocus={false}/enforceFocus={false} (same as AdvancedSearchDialog): Roam's command palette runs commands on Enter keyup, and without this the keyup clicks the autofocused close button and instantly dismisses the dialog.

Imported status

Nothing writes importedFrom yet, so every node shows Available for now. This PR defines the prop path it reads (discourse-graph.importedFrom.sourceNodeRid); the import action (ENG-1859) writes that metadata when it materializes nodes.

How to read this diff

This is 766 added lines because Roam had no discovery surface yet; it is four bounded pieces, not a broad rewrite.

Area Lines Review focus
Discovery utility 333 Group access boundary, contract filtering, stable identity
Blueprint dialog 263 Read-only required fields and ordinary UI states
Unit tests 163 Contract and identity edge cases
Existing command registry 7 Sync-gated entry point

The command registry is the only existing production file changed. There are no changes to shared packages, persistence schemas, migrations, sync behavior, or existing import code.

Suggested review order: start at getGroupSharedResources, follow the rows through buildDiscoveredSharedNodes, then scan the table and tests.

Review guide

  1. discoverSharedNodes.ts: group-backed candidate selection and contract filtering.
  2. DiscoverSharedNodesDialog.tsx: read-only presentation of the ticket's source metadata.
  3. discoverSharedNodes.test.ts: resource identity, contract completeness, imported RID matching, current-space exclusion, and sorting.
  4. registerCommandPaletteCommands.ts: sync-gated command entry only.

Scope

This PR intentionally stops at discovery. It does not materialize or refresh Roam nodes, import relations or assets, change sync behavior, or modify the database schema.

Validation

  • pnpm --filter roam test - 10 files / 53 tests passed
  • pnpm --filter roam check-types
  • pnpm --filter roam build - built with 0 errors
  • pnpm --filter roam lint - 0 errors; existing warnings unchanged
  • Prettier and git diff --check

@linear-code

linear-code Bot commented Jul 10, 2026

Copy link
Copy Markdown

ENG-1855

@supabase

supabase Bot commented Jul 10, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jul 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Jul 15, 2026 5:05am

Request Review

@sid597 sid597 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Four short guideposts through the ENG-1855 data flow.

const resources: ResourceAccess[] = [];
for (let from = 0; ; from += PAGE_SIZE) {
const response = await client
.from("ResourceAccess")

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Review starts here: candidate identities come only from ResourceAccess rows granted to the user's groups. The later my_* reads only resolve those keys.

const variants = contentByResource.get(resourceKey);
const direct = variants?.direct;
const full = variants?.full;
if (

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This is the ENG-1847 contract gate: only typed instances with both direct and typed full content reach the dialog.


return [
{
alreadyImported: importedSourceRids.has(sourceNodeRid),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Imported status joins on the stable RID, not title or local ID, so identical local IDs in different source spaces remain distinct.

<HTMLTable striped style={{ width: "100%" }}>
<thead>
<tr>
<th>Source app</th>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

This stays intentionally read-only: these columns match ENG-1855's pre-import metadata, while materialization remains in ENG-1859.

Roam's command palette runs commands on Enter keyup, which landed as a
click on the dialog's autofocused close button and dismissed it
instantly. Disable autoFocus/enforceFocus like AdvancedSearchDialog and
restore outside-click close.

Also from review: use Tailwind classes instead of inline styles, drop
the unused ValidSharedSpace.id field, and unexport the file-internal
getImportedSourceRids.

return (
<Dialog
autoFocus={false}

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Roam's command palette dispatches commands on Enter keyup. Without autoFocus={false} that keyup lands as a click on the freshly-focused close button and dismisses the dialog immediately. Same props as AdvancedSearchDialog.

@sid597 sid597 marked this pull request as ready for review July 10, 2026 12:14
@graphite-app

graphite-app Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

PR size/scope check

This PR is over our review-size guideline.

  • Recommended: ~200 lines changed
  • Acceptable limit: up to 400 lines when well-scoped/self-contained
  • Preferred file count: fewer than 5 files

Please split this into smaller PRs unless there is a clear reason the changes need to land together.

If keeping it as one PR, please add a brief justification covering:

  • What single problem this PR solves
  • Why the files/changes are coupled

@devin-ai-integration devin-ai-integration Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

✅ Devin Review: No Issues Found

Devin Review analyzed this PR and found no bugs or issues to report.

Open in Devin Review

chatgpt-codex-connector[bot]

This comment was marked as resolved.

Replaces the hand-rolled paging loop with the existing
@repo/database/lib/pagination helper, matching how
syncDgNodesToSupabase already reads ResourceAccess.

@mdroidian mdroidian left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Sorry for the delay in review.

I'm going to approve this so we can unblock the stacked PRs, but there are 2 big concerns:

  1. We shouldn't pass the entire full content to the client for all possible nodes. That should be deferred to only when a user decides to import. Let's make sure we either fix that here or create a follow up ticket to fix it. (link)
  2. The timestamp isn't normalized to UTC so it will provide different values to different clients. (link)

The RID comment is the next strongest to look at.

The rest are strong recommendations.

Comment thread apps/roam/src/utils/discoverSharedNodes.ts
Comment thread apps/roam/src/utils/discoverSharedNodes.ts
Comment thread apps/roam/src/utils/discoverSharedNodes.ts
Comment thread apps/roam/src/components/DiscoverSharedNodesDialog.tsx
Comment thread apps/roam/src/components/DiscoverSharedNodesDialog.tsx
Comment thread apps/roam/src/components/DiscoverSharedNodesDialog.tsx Outdated
Comment thread apps/roam/src/components/DiscoverSharedNodesDialog.tsx Outdated
Comment thread apps/roam/src/utils/discoverSharedNodes.ts
Comment thread apps/roam/src/utils/discoverSharedNodes.ts
spaceId: number;
}): string => `${spaceId}:${sourceLocalId}`;

const getLatestTimestamp = (timestamps: (string | null)[]): string | null => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Concept.last_modified and Content.last_modified are PostgreSQL timestamp without time zone columns. The application writes UTC values, but PostgREST returns them without a Z or offset, so Date.parse(timestamp) interprets them in the browser’s local timezone. This definitely shifts the displayed time and can also affect latest-selection/sorting around DST transitions.

Could we normalize these values here before any comparison and have getLatestTimestamp return a canonical toISOString() value? That keeps the PR-local fix small and also makes the existing dialog formatter safe. The test fixtures should use the production shape, such as 2026-06-14T15:00:00, rather than including Z.

Out of scope, but this is something that should be looked at repo wide: ENG-2034: Standardize UTC timestamp handling across the repository

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Addressed: production-shaped timestamps get Z before parsing and getLatestTimestamp returns canonical toISOString() values.

Copy link
Copy Markdown
Collaborator

I made full optional in ENG-2016, which should make this easier.
Sorry, I thought I could review tonight but it's late.
I agree with the RID comment. What we're converging on elsewhere: Use localId, which will sometimes contain a RID. It may be worth adding a is_rid function; it would basically look for either https:// or orn: prefix at this point.

Copy link
Copy Markdown
Collaborator

And yes UTC!

Copy link
Copy Markdown
Collaborator

For me UTC is a blocker, because it means inserting invalid data in the database.

@sid597

sid597 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

ENG-1855 — addressed in this PR
├── RID correctness: preserve source local IDs that are already https:// or orn: RIDs
├── UTC correctness: normalize PostgREST timestamps before comparison and return canonical ISO values
├── searchTerm / setSearchTerm rename
└── PostHog/internalError reporting for discovery-load failures

ENG-2019 — follow-up in #1216
├── shared discovery query and candidate architecture
├── no full-text transfer during discovery; defer fetching full content until import
├── optional full content, following ENG-2016
├── separate direct-content rows from full-content summaries
├── unique ResourceAccess pagination ordering
├── named shared Platform type backed by Enums<"Platform">
├── evaluate and remove candidate properties that can safely be derived from RID
├── preserve the ENG-1855 RID and UTC behavior during extraction
└── comprehensive shared-module tests for Roam and Obsidian consumers

@sid597

sid597 commented Jul 15, 2026

Copy link
Copy Markdown
Collaborator Author

@mdroidian @maparent addressed some comments others will be addressed in next pr

@maparent maparent left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

One tiny needed change, two comments. Focused on the backend.

currentSpaceId: number;
}): Promise<SharedNodeRows> => {
const resources = (await getGroupSharedResources(client)).filter(
(resource) => resource.space_id !== currentSpaceId,

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: this should be pushed to Db request. Not vital.

client
.from("my_concepts")
.select(
"is_schema, last_modified, schema_id, source_local_id, space_id",

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I'd expect metadata and author_id here, but I guess they can both come later.

"is_schema, last_modified, schema_id, source_local_id, space_id",
)
.eq("space_id", spaceId)
.eq("is_schema", false)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Add .eq("arity", 0) to avoid relations.

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.

3 participants