Skip to content

ENG-1857 Import Roam-origin shared nodes through the existing Obsidian importer - #1266

Open
sid597 wants to merge 2 commits into
mainfrom
eng-1857-validate-obsidian-importer-with-roam-origin-nodes
Open

ENG-1857 Import Roam-origin shared nodes through the existing Obsidian importer#1266
sid597 wants to merge 2 commits into
mainfrom
eng-1857-validate-obsidian-importer-with-roam-origin-nodes

Conversation

@sid597

@sid597 sid597 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator
eng-1857.mp4

Problem

The producer side of the M1 contract is merged (ENG-1848 full variant, ENG-1849 schema metadata), and Roam-origin shared nodes already show up in Obsidian's import modal — but importing one was guaranteed to fail. Roam's full variant is body-only markdown (# Title + body, no YAML — see buildFullMarkdown in apps/roam/src/utils/roamToCrossAppConverters.ts), while processFileContent hard-required nodeTypeId + nodeInstanceId frontmatter inside the content: every Roam node hit "importedNode missing sourceNodeTypeId" and the created file was deleted. Roam-imported files also never got nodeInstanceId written to frontmatter, so duplicate prevention (findExistingImportedFile, getLocalNodeInstanceIds) could never match them and they would re-list as importable forever.

What changed

  • fetchNodeTypeSchemasForInstances (importNodes.ts): batched per-space lookup of each instance's node-type schema (source_local_id + name) from the source space's Concept rows. This consolidates the two inline queries computeImportPreview already ran — the importer now uses the same resolution instead of duplicating it. Both queries carry arity = 0 per the data-layer invariant (node instances and node-type schemas; relation-type schemas have roles → arity 2 and must be excluded).
  • processFileContent: now takes nodeInstanceId (the caller always has it) and nodeTypeIdFromConcept. Content frontmatter still wins when present, so Obsidian-origin imports behave exactly as before; the Concept-derived id is only reached by frontmatter-less (Roam-origin) content. For Obsidian-origin the two values are identical by construction (publish writes schema_represented_by_local_id from the same frontmatter). It also writes nodeInstanceId into the imported file's frontmatter — idempotent for Obsidian-origin, and what makes stable-identity dedup work for Roam-origin.
  • computeImportPreview: inline instance→schema queries replaced with the shared helper (also drops two as Array<...> casts).
  • Test: Roam-origin fixture for buildSharedNodes — URL-based rid (https://roamresearch.com/#/app/<graph>/<uid>), platform Roam, production-shape (Z-less) timestamps in, normalized ISO out.

Removals (dead code inside the touched signatures)

  • originalFilePath param on processFileContent — declared and passed, never read (was an eslint warning on main).
  • if (mappedNodeTypeId !== undefined)mapNodeTypeIdToLocal always returns string.
  • The "importedNode missing nodeInstanceId" error path — the caller supplies nodeInstanceId directly now; the frontmatter read it guarded is gone.
  • ParsedFrontmatter.nodeInstanceId/publishedToGroups/authorId — never read from this parse (the reads in refreshImportedFile are metadata-cache frontmatter, a different object).

Extraction parity (preview queries → shared helper)

Old (inline in computeImportPreview) New (fetchNodeTypeSchemasForInstances) Delta
instance query: space_id, is_schema=false, in(source_local_id) same + arity=0 invariant conformance
schema query: select source_local_id, name select id, source_local_id, name + arity=0 id keys the instance→schema map
if (!conceptRows) continue (silent) console.error + empty map failures logged, same control flow
iterate unique schema rows iterate per-instance map values identical outcomes — dedup preserved by existing seenNodeTypeIds / nodeTypeIdToName.has
as Array<{...}> casts ×2 none (null-guarded narrowing) honest types against nullable view columns

Identity notes

  • importedFromRid semantics are unchanged for Roam spaces: their space URL is https://roamresearch.com/#/app/<graph>, and the https branch of spaceUriAndLocalIdToRid ignores the "note" subtype — matching the rid buildSharedNodes produces for Roam platforms exactly.
  • Duplicate prevention now works end-to-end for Roam-origin: nodeInstanceId + importedFromRid frontmatter → findExistingImportedFile (re-import updates in place) and getLocalNodeInstanceIds (already-imported nodes drop out of the modal).

Markdown fidelity gaps (documented per ticket; follow-up ticket candidates)

  1. Node-type format not persisted — Roam schema literal_content is {label, template?} only; a Roam type with no name match in Obsidian gets a synthesized format (CLA - {content}), not Roam's real one.
  2. Template semantics mismatch — Roam persists template text under template; Obsidian treats nodeType.template as a template filename, so an unmatched Roam type with a template gets an unusable value.
  3. # Title H1 duplication — Roam's full embeds the title as H1 and the Obsidian filename carries it too (producer shape from ENG-1848, not changed here).
  4. Roam syntax passthrough{{[[query]]}}, {{[[table]]}}, {{[[kanban]]}}, attributes (foo::), ^^highlights^^ (≠ Obsidian ==), #[[multi-word tags]] arrive verbatim; unresolved [[links]] become alias-form dead links.
  5. Nodes without full (optional per ENG-2016) fail import with a counted error — pre-existing importer behavior, unchanged.

Out of scope (per ticket)

New import UI, materializer rewrite, relations/assets/refresh behavior. refreshAllImportedFiles still refreshes file-by-file (pre-existing loop); the new per-space lookup rides that pattern.

Testing

  • packages/database vitest 8/8 (new Roam fixture included); eslint delta 0 vs main (the 17 remaining warnings pre-date this PR, minus one this PR removes); tsc clean on touched files; plugin builds with 0 errors.
  • Runtime demo of the real flow (Roam-origin rows → import → dedup on re-open → re-import updates in place) to follow.

🤖 Generated with Claude Code


Open in Devin Review

@linear-code

linear-code Bot commented Jul 29, 2026

Copy link
Copy Markdown

ENG-1857

@vercel

vercel Bot commented Jul 29, 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 29, 2026 7:03am

Request Review

@supabase

supabase Bot commented Jul 29, 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 ↗︎.

@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.

Inline notes to orient review — decisions and parity accounting are in the PR description.

name: string;
};

export const fetchNodeTypeSchemasForInstances = async ({

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 full variant is body-only markdown (buildFullMarkdown in apps/roam/src/utils/roamToCrossAppConverters.ts emits # Title + body, no YAML), so there is no embedded frontmatter to read identity from — this resolves each instance's node type from the source space's Concept rows instead. It consolidates the two queries computeImportPreview was already running inline (parity table in the description). name is consumed by the preview caller only; the import path uses nodeTypeId. Both queries carry arity = 0 so relation instances / relation-type schemas (arity 2) can't slip in regardless of what ids a caller passes.

@@ -939,9 +1009,6 @@ const sanitizePathForImport = (path: string): string => {

type ParsedFrontmatter = {

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.

Trimmed to what this parse actually feeds: only nodeTypeId is read from content frontmatter now. The nodeInstanceId/publishedToGroups/authorId reads further down this file (refreshImportedFile) come from the metadata cache — a different object — not this parse.

Comment thread apps/obsidian/src/utils/importNodes.ts Outdated
const sourceNodeTypeId =
typeof frontmatter.nodeTypeId === "string"
? frontmatter.nodeTypeId
: nodeTypeIdFromConcept;

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.

Ordering: content frontmatter wins when present, so Obsidian-origin imports behave exactly as before; the Concept-derived id is only reached by frontmatter-less (Roam-origin) content. For Obsidian-origin the two values are identical by construction — publish writes schema_represented_by_local_id from the same frontmatter it embeds in the content.

record.nodeTypeId = mappedNodeTypeId;
}
record.nodeTypeId = mappedNodeTypeId;
record.nodeInstanceId = nodeInstanceId;

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 line is what makes duplicate prevention work for Roam-origin nodes: findExistingImportedFile (re-import updates in place) and getLocalNodeInstanceIds (already-imported nodes drop out of the modal) both match on nodeInstanceId + importedFromRid frontmatter. Obsidian-origin content already carries this value, so there it's an idempotent rewrite.

}
record.nodeTypeId = mappedNodeTypeId;
record.nodeInstanceId = nodeInstanceId;
record.importedFromRid = spaceUriAndLocalIdToRid(

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.

Now built from the caller-supplied nodeInstanceId — same value the removed frontmatter read produced. Rid shape is unchanged for Roam spaces: their space URL is https-based and the https branch of spaceUriAndLocalIdToRid ignores the "note" subtype, matching what buildSharedNodes produces for Roam platforms. Identity stays carried once as the RID (per the #1161/#1214 direction).

spaceName,
});

const nodeTypeSchemasByInstance = await fetchNodeTypeSchemasForInstances({

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.

Batched once per space, outside the per-node loop — mirrors where the preview step does the same resolution.

name: string;
}>) {
const sourceNodeTypeId = schema.source_local_id;
const nodeTypeSchemasByInstance = await fetchNodeTypeSchemasForInstances({

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.

Drop-in replacement for the two inline queries that lived here — same filters plus arity = 0, select adds id (keys the instance→schema map), and null-data now logs instead of silently continuing. Dedup behavior is preserved by the existing seenNodeTypeIds / nodeTypeIdToName.has checks: the map yields one entry per instance rather than unique schemas, but outcomes are identical.

]);
});

it("builds a Roam-origin shared node with a URL-based rid", () => {

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-origin fixture: for an https space URL the rid is <spaceUrl>/<uid> (no orn: subtype), platform Roam; timestamps go in as production-shape Z-less naive UTC and come out normalized ISO.

@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 chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 22770dc2de

ℹ️ 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 apps/obsidian/src/utils/importNodes.ts Outdated
Comment thread apps/obsidian/src/utils/importNodes.ts Outdated
@sid597

sid597 commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator Author

@codex review

@chatgpt-codex-connector

Copy link
Copy Markdown

Codex Review: Didn't find any major issues. 🎉

Reviewed commit: f44ec9944a

ℹ️ 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".

@sid597
sid597 requested review from maparent and mdroidian July 30, 2026 05:19
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