Skip to content

ENG-2154 Undecorate Obsidian node titles into core_title on publish - #1318

Open
sid597 wants to merge 5 commits into
mainfrom
eng-2154-undecorate-obsidian-node-titles-into-core_title-on-publish
Open

ENG-2154 Undecorate Obsidian node titles into core_title on publish#1318
sid597 wants to merge 5 commits into
mainfrom
eng-2154-undecorate-obsidian-node-titles-into-core_title-on-publish

Conversation

@sid597

@sid597 sid597 commented Aug 19, 2026

Copy link
Copy Markdown
Collaborator
eng-2154.mp4

This is what we are following:
image
This PR is the top path of the diagram: undecorate on publish, Obsidian.

Obsidian publishes the decorated file name, so importers receive Obsidian's title grammar. This writes the undecorated content as literal_content.core_title on publish (contract from ENG-2153).

  • discourseNodeInstanceToLocalConcept extracts {content} from the file basename with the node type's format and writes it as core_title. The existing keys stay.
  • A type without a format, or a non-matching basename, gets core_title equal to the basename. Importers fall back to the direct content text when the key is absent, so old rows stay valid. Backfill is ENG-2155.
  • The key is written on every upsert because upsert_concepts replaces literal_content wholesale. Publish and periodic sync both flow through this one converter.

No tests: apps/obsidian has no test infrastructure and we decided not to add it in this PR; ENG-2176 moves the extractor where a test runner exists.

From review:

  • extractContentFromTitle now returns the {content} capture by placeholder position and preserves a matched-but-empty capture as "". The format's placeholders map 1:1 to the expression's capture groups, so the capture index is the placeholder index. This replaces the ticket's accepted first-capture limitation: ENG-2155's backfill treats any non-null core_title as already set, so a wrong value written before a later fix would never be repaired — cheaper to fix before rows are written. Same extraction rule as the Roam side (ENG-2153 Undecorate Roam node titles into core_title on publish #1317), with one asymmetry tracked in ENG-2176: Roam's placeholder scan ({[\w\d-]+}) is wider than its builder's grammar, Obsidian's matches its builder exactly.
  • The change is publish-path-only: the two pre-existing callers (QueryEngine) store the result in BulkImportCandidate.extractedContent, which nothing reads.
  • discourseNodeInstanceToLocalConcept takes named parameters, matching the sibling converters in the file and the AGENTS.md rule.
  • The unused accountLocalId parameter of convertDgToSupabaseConcepts is removed — pre-existing on main, removed here because this PR touches the signature and eslint --max-warnings 0 flags the file.
  • Formats containing regex metacharacters the expression builder does not escape still mis-extract (fallback to the decorated basename); deferred to ENG-2176 since the same gap exists in Roam's builder, so it is one cross-app fix. ENG-2176 also covers unifying the three copies of this transform (Roam, here, and DiscourseContextView's inline copy — which can render a different Content string than what gets published) into a shared, testable package.

Notes for review:

  • A format without {content} publishes the first placeholder's capture (format validation only requires some {var}) — same rule as Roam.
  • core_title recomputes when a file's title changes and on full sync. A format edit alone does not rewrite existing rows: it also does not rename existing files, so stored values stay consistent with the basenames they came from.
  • label stays alongside core_title: label is the decorated basename, core_title the undecorated content — an importer cannot undecorate a foreign app's title grammar, so both cross the boundary.

@linear-code

linear-code Bot commented Aug 19, 2026

Copy link
Copy Markdown

ENG-2154

@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 24, 2026 11:28am

Request Review

@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 2 potential issues.

View 1 additional finding in Devin Review.

Open in Devin Review

Comment thread apps/obsidian/src/utils/conceptConversion.ts Outdated
Comment thread apps/obsidian/src/utils/conceptConversion.ts

@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: 88e00370ee

ℹ️ 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/conceptConversion.ts
export const discourseNodeInstanceToLocalConcept = (
context: SupabaseContext,
nodeData: ObsidianDiscourseNodeData,
nodeTypesById: Record<string, DiscourseNode>,

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.

The converter only had the type id, and the format lives on the type. The single caller already builds this map for discourseRelationTripleSchemaToLocalConcept, so it comes in the same way.

Comment on lines +171 to +174
core_title: extractContentFromTitle(
nodeTypesById[nodeData.nodeTypeId]?.format ?? "",
nodeData.file.basename,
),

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.

extractContentFromTitle already returns the title when the format is empty or does not match, which is the fallback ENG-2153 specifies. The ?? "" covers a file whose nodeTypeId no longer matches a configured type.

The write is unconditional on purpose: upsert_concepts replaces literal_content wholesale, so a conditional write would let a later sync erase the key.

const contentIndex = placeholders.findIndex(
(name) => name.toLowerCase() === "content",
);
const capture = contentIndex >= 0 ? match[contentIndex + 1] : match[1];

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.

getDiscourseNodeFormatExpression turns every {[a-zA-Z]+} placeholder into one (.*?) group, in order, and the scan above uses the same pattern, so the capture index is the placeholder index + 1. A matched-but-empty capture stays "" on purpose: the old || title fallback wrote the decorated basename as core_title, a non-null value ENG-2155's backfill probe would treat as already migrated. Formats with metacharacters the expression builder does not escape still fall back to the basename — ENG-2176.

const literal_content: Record<string, Json> = {
label: nodeData.file.basename,
core_title: extractContentFromTitle(
nodeTypesById[nodeData.nodeTypeId]?.format ?? "",

@sid597 sid597 Aug 24, 2026

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.

nodeData.nodeTypeId is the typed copy of frontmatter.nodeTypeId (same value; getDiscourseNodes copies it out). The destructured nodeTypeId on line 171 exists to strip the key from otherData before it becomes source_data, and line 187 keeps using it with the pre-existing as string — switching that to nodeData.nodeTypeId would drop an assertion, left as is to avoid churn on a PR under review.

@sid597
sid597 requested review from maparent and mdroidian August 24, 2026 11:37
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