Skip to content

Commit add4831

Browse files
committed
[ENG-1848] Add Roam full markdown content variant for shared nodes
1 parent 7e5540d commit add4831

3 files changed

Lines changed: 146 additions & 1 deletion

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import type { TreeNode } from "roamjs-components/types";
2+
import {
3+
FULL_CONTENT_FORMAT,
4+
type CrossAppNodeContent,
5+
} from "@repo/database/crossAppNodeContract";
6+
import { buildFullMarkdown } from "./convertRoamNodeToFullContent";
7+
8+
/**
9+
* Reference fixture for ENG-1848 ("tests or fixtures cover representative Roam
10+
* block content becoming `full` markdown"). The Roam app has no unit-test
11+
* runner, so this exercises the real `buildFullMarkdown` transform on an
12+
* in-memory Roam page tree and types the result against the ENG-1847 contract's
13+
* `full` variant. Downstream importer validation (ENG-1857) can assert against
14+
* `roamClaimFullMarkdownFixture.full.value`, which evaluates to:
15+
*
16+
* # Sleep improves memory consolidation
17+
*
18+
* - Multiple studies show that sleep after learning strengthens memory traces.
19+
* - Supporting evidence:
20+
* - [[EVD]] - Rasch & Born 2013
21+
*/
22+
23+
const block = (text: string, children: TreeNode[] = []): TreeNode => ({
24+
text,
25+
children,
26+
order: 0,
27+
parents: [],
28+
uid: "",
29+
heading: 0,
30+
open: true,
31+
viewType: "bullet",
32+
blockViewType: "outline",
33+
editTime: new Date(0),
34+
textAlign: "left",
35+
props: { imageResize: {}, iframe: {} },
36+
});
37+
38+
const title = "Sleep improves memory consolidation";
39+
40+
const blocks: TreeNode[] = [
41+
block(
42+
"Multiple studies show that sleep after learning strengthens memory traces.",
43+
),
44+
block("Supporting evidence:", [block("[[EVD]] - Rasch & Born 2013")]),
45+
];
46+
47+
export const roamClaimFullMarkdownFixture: {
48+
title: string;
49+
blocks: TreeNode[];
50+
full: CrossAppNodeContent["full"];
51+
} = {
52+
title,
53+
blocks,
54+
full: {
55+
format: FULL_CONTENT_FORMAT,
56+
value: buildFullMarkdown({ title, blocks }),
57+
},
58+
};
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 fixture. 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)