Skip to content

ENG-2153 Undecorate Roam node titles into core_title on publish - #1317

Open
sid597 wants to merge 1 commit into
mainfrom
eng-2153-undecorate-roam-node-titles-into-core_title-on-publish
Open

ENG-2153 Undecorate Roam node titles into core_title on publish#1317
sid597 wants to merge 1 commit into
mainfrom
eng-2153-undecorate-roam-node-titles-into-core_title-on-publish

Conversation

@sid597

@sid597 sid597 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator

Roam publishes the decorated page title ([[CLM]] - my claim) as the node's content value, so an importing app receives Roam's title grammar instead of the content. This PR extracts {content} from the title with the node type's format and writes it as literal_content.core_title in all three concept producers (publish, periodic sync, full-content). The decorated title stays as the direct content value.

If the type has no format, or the title does not match it, core_title equals the title. coreTitle is required on CrossAppNode: upsert_concepts replaces literal_content wholesale, so a producer that omits the key would erase core_title on the next sync. Required turns that mistake into a compile error.

No DB migration: literal_content jsonb already exists (MAP confirmed on the ticket). Tests cover formatted, unformatted, and non-matching titles, a format with {Source}, content containing " - " (non-greedy shortest match, accepted for v0), and the round trip.

Backfill of existing rows is ENG-2155. Decorating on import is ENG-2156 (Roam) and ENG-2157 (Obsidian).


Open in Devin Review

Extract {content} from the page title with the node type's format and
write it as literal_content.core_title in all three concept producers
(publish, periodic sync, full-content). coreTitle is required on
CrossAppNode so a producer omitting it fails to compile: upsert_concepts
replaces literal_content wholesale, so an omitted key would erase
core_title on the next re-upsert. Falls back to the full title when the
type has no format or the title does not match it.
@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

ENG-2153

@supabase

supabase Bot commented Aug 19, 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 Aug 19, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
discourse-graph Ready Ready Preview Aug 19, 2026 5:48pm

Request Review

@graphite-app

graphite-app Bot commented Aug 19, 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

// The title stripped of the node type's title format ("[[CLM]] - {content}"
// -> the {content} part). Equals the title when the type has no format or
// the title does not match it.
coreTitle: string;

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.

Required on purpose. upsert_concepts replaces literal_content wholesale and defaults it to {}, so a producer that omits the key erases core_title on the next re-upsert. Required makes "every producer writes it" a compile error.

author_local_id: node.authorId,
schema_represented_by_local_id: node.nodeType,
literal_content: {
core_title: node.coreTitle,

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.

No migration needed: literal_content jsonb already exists (MAP confirmed on the ticket). name and the direct content keep the decorated title; core_title is added next to it.

import getDiscourseNodes from "./getDiscourseNodes";
import extractContentFromTitle from "./extractContentFromTitle";

const getCoreTitle = (title: string, nodeTypeUid: string): string => {

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.

Callers only carry {uid, type}, so the format lookup lives in the converter. Calling getDiscourseNodes() per node is cheap: the accessor is version-cached.

createdAt: new Date(node.created || Date.now()),
modifiedAt: new Date(node.last_modified || Date.now()),
nodeType: node.node_type_id,
coreTitle: getCoreTitle(title, node.node_type_id),

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.

Nothing reads this coreTitle yet: this producer feeds upsert_content, not upsert_concepts. It is set so any CrossAppNode that reaches concept conversion carries it (the ticket lists all three producers).

nodeUid: node.source_local_id,
schemaUid: node.type,
text: node.node_title ? `${node.node_title} ${node.text}` : node.text,
coreTitle: extractContentFromTitle(node.node_title ?? node.text, {

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.

Inlined instead of reusing the publish-side helper because allNodeTypes is already in scope; the helper would refetch via getDiscourseNodes(). node_title ?? node.text matters for block-backed types: node_title is the format-matched page title, text is the block string.

@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 found 1 potential issue.

View 2 additional findings in Devin Review.

Open in Devin Review

Comment on lines +21 to +25
const getCoreTitle = (title: string, nodeTypeUid: string): string => {
const format =
getDiscourseNodes().find((node) => node.type === nodeTypeUid)?.format ?? "";
return extractContentFromTitle(title, { format });
};

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.

🟡 Node type settings are re-read for every node during sync and publish, slowing large syncs

The node type configuration is re-read from Roam for each node being converted (getDiscourseNodes() at apps/roam/src/utils/roamToCrossAppConverters.ts:23) instead of once per batch, so syncing or publishing many nodes repeats the same expensive work thousands of times.
Impact: Large or initial syncs take noticeably longer and can block the UI, and the work is entirely wasted in the full-content path where the extracted title is not used.

Per-node configuration parsing in getCoreTitle

getCoreTitle calls getDiscourseNodes() on every invocation. It is invoked once per node from fullContentNodeToCrossApp (apps/roam/src/utils/roamToCrossAppConverters.ts:84) and inside the nodeRows.map of nodeUidsWithTypeToCrossApp (apps/roam/src/utils/roamToCrossAppConverters.ts:134).

getDiscourseNodes is only cached on the new-settings-store path (apps/roam/src/components/settings/utils/accessors.ts:1153-1157); on the legacy discourseConfigRef path it re-parses every node type's config tree (specification conditions, canvas settings, template) on each call (apps/roam/src/utils/getDiscourseNodes.ts:113-160).

fullContentNodeToCrossApp runs for every node in a sync via convertRoamNodeToFullContent (apps/roam/src/utils/convertRoamNodeToFullContent.ts:20-23, called at apps/roam/src/utils/syncDgNodesToSupabase.ts:805), and that path only uses the full content — the computed coreTitle is discarded.

Compare the approach used in convertDgToSupabaseConcepts, which builds a format lookup map once (apps/roam/src/utils/syncDgNodesToSupabase.ts:671-673).

Prompt for agents
getCoreTitle in apps/roam/src/utils/roamToCrossAppConverters.ts calls getDiscourseNodes() once per node. On the legacy settings-store path getDiscourseNodes re-parses every node type's configuration tree on each call (no cache), and the function is called per node from fullContentNodeToCrossApp and from the nodeRows.map inside nodeUidsWithTypeToCrossApp, so a sync of thousands of nodes repeats the parsing thousands of times. Additionally, the full-content path (convertRoamNodeToFullContent -> crossAppNodeToDbContent(node, 'full')) never reads coreTitle, so that work is wasted there. Consider building a format-by-node-type map once per batch (as convertDgToSupabaseConcepts does) and passing it in, or memoizing the format lookup, and avoid computing coreTitle where it is not consumed.
Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

@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: e0f31a9870

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

const getCoreTitle = (title: string, nodeTypeUid: string): string => {
const format =
getDiscourseNodes().find((node) => node.type === nodeTypeUid)?.format ?? "";
return extractContentFromTitle(title, { format });

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve empty content placeholders

When a valid formatted title has an empty or whitespace-only {content} capture (for example, [[EVD]] - - @smith2020), this call stores the decorated title instead of an empty core_title. extractContentFromTitle currently evaluates the trimmed capture with || title, so an empty matched value falls through to the original title; the same behavior affects periodic sync. Preserve a successfully matched empty string rather than treating it as no match.

Useful? React with 👍 / 👎.

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