Skip to content
Open
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
18 changes: 14 additions & 4 deletions apps/obsidian/src/utils/conceptConversion.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { DiscourseNodeInVault } from "./getDiscourseNodes";
import type { LocalConceptDataInput } from "@repo/database/inputTypes";
import type { ObsidianDiscourseNodeData } from "./syncDgNodesToSupabase";
import type { Json } from "@repo/database/dbTypes";
import { extractContentFromTitle } from "./extractContentFromTitle";

/**
* Get extra data (author, timestamps) from file metadata
Expand Down Expand Up @@ -157,15 +158,24 @@ export const discourseRelationTripleSchemaToLocalConcept = ({
/**
* Convert discourse node instance (file) to LocalConceptDataInput
*/
export const discourseNodeInstanceToLocalConcept = (
context: SupabaseContext,
nodeData: ObsidianDiscourseNodeData,
): LocalConceptDataInput => {
export const discourseNodeInstanceToLocalConcept = ({
context,
nodeData,
nodeTypesById,
}: {
context: SupabaseContext;
nodeData: ObsidianDiscourseNodeData;
nodeTypesById: Record<string, DiscourseNode>;
}): LocalConceptDataInput => {
const extraData = getNodeExtraData(nodeData.file, context.userId);
const { nodeInstanceId, nodeTypeId, importedFromRid, ...otherData } =
nodeData.frontmatter;
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.

nodeData.file.basename,
Comment thread
sid597 marked this conversation as resolved.
),
Comment thread
sid597 marked this conversation as resolved.
Comment on lines +175 to +178

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.

source_data: otherData as unknown as Json,
};
if (importedFromRid && typeof importedFromRid === "string")
Expand Down
20 changes: 17 additions & 3 deletions apps/obsidian/src/utils/extractContentFromTitle.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,24 @@
import { getDiscourseNodeFormatExpression } from "./getDiscourseNodeFormatExpression";

export const extractContentFromTitle = (format: string, title: string): string => {
export const extractContentFromTitle = (
format: string,
title: string,
): string => {
if (!format) return title;

const placeholderRegex = /{([a-zA-Z]+)}/g;
const placeholders: string[] = [];
let placeholderMatch: RegExpExecArray | null;
while ((placeholderMatch = placeholderRegex.exec(format))) {
placeholders.push(placeholderMatch[1] ?? "");
}
const regex = getDiscourseNodeFormatExpression(format);
const match = title.match(regex);
const match = regex.exec(title);
if (!match) return title;

return match?.[1]?.trim() || title;
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.

return capture === undefined ? title : capture.trim();
};
10 changes: 5 additions & 5 deletions apps/obsidian/src/utils/syncDgNodesToSupabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -452,7 +452,6 @@ export const syncAllNodesAndRelations = async (
nodesSince: changedNodeInstances,
supabaseClient,
context,
accountLocalId,
plugin,
allNodes,
fullSync: true,
Expand All @@ -470,15 +469,13 @@ const convertDgToSupabaseConcepts = async ({
nodesSince,
supabaseClient,
context,
accountLocalId,
plugin,
allNodes,
fullSync,
}: {
nodesSince: ObsidianDiscourseNodeData[];
supabaseClient: DGSupabaseClient;
context: SupabaseContext;
accountLocalId: string;
plugin: DiscourseGraphPlugin;
allNodes?: DiscourseNodeInVault[];
fullSync?: boolean;
Expand Down Expand Up @@ -594,7 +591,11 @@ const convertDgToSupabaseConcepts = async ({
.filter((n) => !!n);

const nodeInstanceToLocalConcepts = nodesSince.map((node) => {
return discourseNodeInstanceToLocalConcept(context, node);
return discourseNodeInstanceToLocalConcept({
context,
nodeData: node,
nodeTypesById,
});
});

const relationInstancesData = await loadRelations(plugin);
Expand Down Expand Up @@ -817,7 +818,6 @@ const syncChangedNodesToSupabase = async ({
nodesSince: nodesNeedingConceptUpsert,
supabaseClient,
context,
accountLocalId,
plugin,
});

Expand Down