Skip to content

Commit b4f758c

Browse files
committed
[ENG-1848] Add Roam full markdown content variant for shared nodes
1 parent 5be4274 commit b4f758c

3 files changed

Lines changed: 144 additions & 1 deletion

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import type { TreeNode } from "roamjs-components/types";
2+
import type { CrossAppNode } from "@repo/database/crossAppNodeContract";
3+
import { buildFullMarkdown } from "./convertRoamNodeToFullContent";
4+
5+
/**
6+
* Reference fixture for ENG-1848 ("tests or fixtures cover representative Roam
7+
* block content becoming `full` markdown"). The Roam app has no unit-test
8+
* runner, so this exercises the real `buildFullMarkdown` transform on an
9+
* in-memory Roam page tree and types the result against the final ENG-1847
10+
* `CrossAppNode.content.full` contract. Downstream importer validation
11+
* (ENG-1857) can assert against
12+
* `roamClaimFullMarkdownFixture.full.value`, which evaluates to:
13+
*
14+
* # Sleep improves memory consolidation
15+
*
16+
* - Multiple studies show that sleep after learning strengthens memory traces.
17+
* - Supporting evidence:
18+
* - [[EVD]] - Rasch & Born 2013
19+
*/
20+
21+
const block = (text: string, children: TreeNode[] = []): TreeNode => ({
22+
text,
23+
children,
24+
order: 0,
25+
parents: [],
26+
uid: "",
27+
heading: 0,
28+
open: true,
29+
viewType: "bullet",
30+
blockViewType: "outline",
31+
editTime: new Date(0),
32+
textAlign: "left",
33+
props: { imageResize: {}, iframe: {} },
34+
});
35+
36+
const title = "Sleep improves memory consolidation";
37+
38+
const blocks: TreeNode[] = [
39+
block(
40+
"Multiple studies show that sleep after learning strengthens memory traces.",
41+
),
42+
block("Supporting evidence:", [block("[[EVD]] - Rasch & Born 2013")]),
43+
];
44+
45+
export const roamClaimFullMarkdownFixture: {
46+
title: string;
47+
blocks: TreeNode[];
48+
full: CrossAppNode["content"]["full"];
49+
} = {
50+
title,
51+
blocks,
52+
full: {
53+
format: "text/markdown",
54+
value: buildFullMarkdown({ title, blocks }),
55+
},
56+
};
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { toMarkdown } from "./pageToMarkdown";
2+
import { type RoamDiscourseNodeData } from "./getAllDiscourseNodesSince";
3+
import { type DiscourseNode } from "./getDiscourseNodes";
4+
import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid";
5+
import getPageViewType from "roamjs-components/queries/getPageViewType";
6+
import type { TreeNode, ViewType } from "roamjs-components/types";
7+
import type { LocalContentDataInput } from "@repo/database/inputTypes";
8+
9+
/**
10+
* Builds the `full` cross-app content variant for Roam discourse nodes.
11+
*
12+
* Per the shared cross-app node contract (ENG-1847,
13+
* `@repo/database/crossAppNodeContract`), every shared node must persist a
14+
* `full` variant: a self-sufficient markdown body the destination app can
15+
* materialize without querying Roam. Roam previously emitted only the `direct`
16+
* title content; this fills that gap (ENG-1848, F2/F3). The body reuses the
17+
* existing `toMarkdown` page serializer with block-refs and embeds inlined for
18+
* self-sufficiency, prefixed with the node title as an H1 — matching the
19+
* contract's Roam typed example. Known MVP0 markdown-fidelity limits live on F3.
20+
*/
21+
22+
const FULL_MARKDOWN_OPTS = {
23+
refs: true,
24+
embeds: true,
25+
simplifiedFilename: false,
26+
removeSpecialCharacters: false,
27+
maxFilenameLength: 64,
28+
linkType: "alias",
29+
allNodes: [] as DiscourseNode[],
30+
};
31+
32+
export const buildFullMarkdown = ({
33+
title,
34+
blocks,
35+
viewType = "bullet",
36+
}: {
37+
title: string;
38+
blocks: TreeNode[];
39+
viewType?: ViewType;
40+
}): string => {
41+
const body = blocks
42+
.filter((block) => !!block.text || !!block.children?.length)
43+
.map((block) =>
44+
toMarkdown({ c: block, v: viewType, i: 0, opts: FULL_MARKDOWN_OPTS }),
45+
)
46+
.join("\n")
47+
.trim();
48+
return body ? `# ${title}\n\n${body}\n` : `# ${title}\n`;
49+
};
50+
51+
export const convertRoamNodeToFullContent = ({
52+
nodes,
53+
}: {
54+
nodes: RoamDiscourseNodeData[];
55+
}): LocalContentDataInput[] =>
56+
nodes.flatMap((node) => {
57+
try {
58+
const title = node.node_title ?? node.text;
59+
const blocks = getFullTreeByParentUid(node.source_local_id).children;
60+
const viewType = getPageViewType(title) || "bullet";
61+
return [
62+
{
63+
author_local_id: node.author_local_id,
64+
source_local_id: node.source_local_id,
65+
created: new Date(node.created || Date.now()).toISOString(),
66+
last_modified: new Date(
67+
node.last_modified || Date.now(),
68+
).toISOString(),
69+
text: buildFullMarkdown({ title, blocks, viewType }),
70+
variant: "full",
71+
scale: "document",
72+
},
73+
];
74+
} catch (error) {
75+
console.error(
76+
`convertRoamNodeToFullContent: failed to build full markdown for ${node.source_local_id}:`,
77+
error,
78+
);
79+
return [];
80+
}
81+
});

apps/roam/src/utils/syncDgNodesToSupabase.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
} from "./conceptConversion";
1919
import { fetchEmbeddingsForNodes } from "./upsertNodesAsContentWithEmbeddings";
2020
import { convertRoamNodeToLocalContent } from "./upsertNodesAsContentWithEmbeddings";
21+
import { convertRoamNodeToFullContent } from "./convertRoamNodeToFullContent";
2122
import type { DGSupabaseClient } from "@repo/database/lib/client";
2223
import { intersection } from "@repo/utils/setOperations";
2324
import type { Json, Enums } from "@repo/database/dbTypes";
@@ -618,6 +619,9 @@ export const upsertNodesToSupabaseAsContentWithEmbeddings = async (
618619
const allNodeInstancesAsLocalContent = convertRoamNodeToLocalContent({
619620
nodes: roamNodes,
620621
});
622+
const fullContent = convertRoamNodeToFullContent({
623+
nodes: roamNodes,
624+
});
621625

622626
let nodesWithEmbeddings: LocalContentDataInput[];
623627
try {
@@ -658,7 +662,9 @@ export const upsertNodesToSupabaseAsContentWithEmbeddings = async (
658662
}
659663
};
660664

661-
await uploadBatches(chunk(nodesWithEmbeddings, BATCH_SIZE));
665+
await uploadBatches(
666+
chunk([...nodesWithEmbeddings, ...fullContent], BATCH_SIZE),
667+
);
662668
};
663669

664670
const getAllUsers = async (): Promise<LocalAccountDataInput[]> => {

0 commit comments

Comments
 (0)