From 7e5540dae0313cddbe41197ac95832ffd990c368 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 18 Jun 2026 12:57:54 +0530 Subject: [PATCH 1/6] [ENG-1847] Define shared cross-app node content contract --- apps/obsidian/src/utils/rid.ts | 42 +---- packages/database/package.json | 4 +- packages/database/src/crossAppNodeContract.ts | 95 ++++++++++ .../database/src/fixtures/crossAppNodes.ts | 164 ++++++++++++++++++ packages/database/src/lib/rid.ts | 38 ++++ 5 files changed, 306 insertions(+), 37 deletions(-) create mode 100644 packages/database/src/crossAppNodeContract.ts create mode 100644 packages/database/src/fixtures/crossAppNodes.ts create mode 100644 packages/database/src/lib/rid.ts diff --git a/apps/obsidian/src/utils/rid.ts b/apps/obsidian/src/utils/rid.ts index c96db9e3d..08ef6aed2 100644 --- a/apps/obsidian/src/utils/rid.ts +++ b/apps/obsidian/src/utils/rid.ts @@ -1,36 +1,6 @@ -// Functions to express a pair of spaceUri, sourceLocalId as a single string, and back. -// We're following https://github.com/BlockScience/rid-lib: -// Either a Web URL, with the last segment as the sourceLocalId; -// OR the format `orn:.:/` -// With the assumption that the sourceUri has the form : -// The subtype may be omitted. - -export const spaceUriAndLocalIdToRid = ( - spaceUri: string, - localId: string, - subtype?: string, -): string => { - if (spaceUri.startsWith("http")) return `${spaceUri}/${localId}`; - const parts = spaceUri.split(":"); - if (parts.length === 2) - return subtype - ? `orn:${parts[0]}.${subtype}:${parts[1]}/${localId}` - : `orn:${parts[0]}:${parts[1]}/${localId}`; - throw new Error("Unrecognized spaceUri"); -}; - -export const ridToSpaceUriAndLocalId = ( - rid: string, -): { spaceUri: string; sourceLocalId: string } => { - const m = rid.match(/^orn:(\w+)\.(\w+):(.*)\/([^/]+)$/); - if (m) { - return { spaceUri: `${m[1]}:${m[3]}`, sourceLocalId: m[4]! }; - } - const m2 = rid.match(/^orn:(\w+):(.*)\/([^/]+)$/); - if (m2) { - return { spaceUri: `${m2[1]}:${m2[2]}`, sourceLocalId: m2[3]! }; - } - const parts = rid.split("/"); - const sourceLocalId = parts.pop()!; - return { spaceUri: parts.join("/"), sourceLocalId }; -}; +// The RID helpers now live in the shared database package so Roam and Obsidian +// share one cross-app identity format. See @repo/database/lib/rid. +export { + spaceUriAndLocalIdToRid, + ridToSpaceUriAndLocalId, +} from "@repo/database/lib/rid"; diff --git a/packages/database/package.json b/packages/database/package.json index b2c0de431..9a814f52f 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -11,7 +11,9 @@ "default": "./src/dbDotEnv.mjs" }, "./dbTypes": "./src/dbTypes.ts", - "./inputTypes": "./src/inputTypes.ts" + "./inputTypes": "./src/inputTypes.ts", + "./crossAppNodeContract": "./src/crossAppNodeContract.ts", + "./fixtures/*": "./src/fixtures/*.ts" }, "typesVersions": { "*": { diff --git a/packages/database/src/crossAppNodeContract.ts b/packages/database/src/crossAppNodeContract.ts new file mode 100644 index 000000000..7f6582b37 --- /dev/null +++ b/packages/database/src/crossAppNodeContract.ts @@ -0,0 +1,95 @@ +import type { Enums } from "./dbTypes"; + +/** + * Shared cross-app discourse-node content contract (MVP0). + * + * This is the payload that lets Roam and Obsidian discover, import and refresh + * each other's discourse nodes. It is a typed *view* over data that already + * persists through `@repo/database/inputTypes` (`LocalConceptDataInput` / + * `LocalContentDataInput`) and the `upsert_concepts` / `upsert_content` RPCs — + * it does NOT introduce a new persistence path. Build/parse the `rid` with the + * helpers in `@repo/database/lib/rid`. The full spec — field-by-field mapping to + * the Concept/Content tables and markdown fidelity limits — lives on Linear + * issue ENG-1847. + */ + +/** Source app a shared node originates from. Mirrors the DB `Platform` enum. */ +export type Platform = Enums<"Platform">; // "Roam" | "Obsidian" + +/** Persisted content scales. Mirrors the DB `ContentVariant` enum. */ +export type ContentVariant = Enums<"ContentVariant">; + +/** + * The Content variants every shared node must persist: + * - `direct`: the import-list title. + * - `full`: a self-sufficient markdown body the destination can materialize + * without querying the source app. + */ +export const SHARED_NODE_CONTENT_VARIANTS = [ + "direct", + "full", +] as const satisfies readonly ContentVariant[]; + +/** + * MIME type of the `full` variant in MVP0. Markdown is the v0 content model; + * atJSON is the planned v1 successor (F16). Keep this as the single place that + * names the format so v1 does not have to hunt down hardcoded strings. + */ +export const FULL_CONTENT_FORMAT = "text/markdown"; + +/** Identity of the node-type schema the destination maps to / creates from. */ +export type CrossAppNodeType = { + /** + * `source_local_id` of the node-type *schema* Concept in the source space + * (the Concept with `is_schema = true`). Maps to + * `LocalConceptDataInput.schema_represented_by_local_id` on the instance. + */ + sourceLocalId: string; + /** Human-readable node-type label, e.g. "Claim". */ + label: string; +}; + +/** The required content variants of a shared node. */ +export type CrossAppNodeContent = { + /** Import-list title. Persisted as the `direct` Content variant (`text`). */ + direct: { value: string }; + /** + * Self-sufficient markdown body. Persisted as the `full` Content variant + * (`text`); `format` is the contract-level media type for that text in MVP0. + */ + full: { format: typeof FULL_CONTENT_FORMAT; value: string }; +}; + +/** + * Stable cross-app identity (F9). The triple + * (`sourceApp`, `sourceSpace.url`, `sourceLocalId`) is equivalent to `rid`; + * build/parse `rid` with `spaceUriAndLocalIdToRid` / `ridToSpaceUriAndLocalId` + * from `@repo/database/lib/rid`. Duplicate-prevention and refresh must key on + * this identity, never on the display title. + */ +export type CrossAppNodeIdentity = { + sourceApp: Platform; + /** + * Source space: `Space.url` (portable cross-app id) and `Space.name` + * (display). Do not use numeric `Space.id` as the payload identity; it is + * local to the receiving database. + */ + sourceSpace: { url: string; name: string }; + /** The node's `source_local_id` within its source space. */ + sourceLocalId: string; + /** Stable cross-app id derived from (`sourceSpace.url`, `sourceLocalId`). */ + rid: string; +}; + +/** The shared cross-app discourse-node payload (discovery + import facing). */ +export type CrossAppNode = CrossAppNodeIdentity & { + nodeType: CrossAppNodeType; + content: CrossAppNodeContent; + /** + * ISO-8601 source last-modified time. Use the source node modified timestamp, + * or the latest `Content.last_modified` across the required `direct` and + * `full` variants when deriving from persisted rows. Basis for freshness + * (F13), refresh, and duplicate-prevention. + */ + sourceModifiedAt: string; +}; diff --git a/packages/database/src/fixtures/crossAppNodes.ts b/packages/database/src/fixtures/crossAppNodes.ts new file mode 100644 index 000000000..b98ecbc13 --- /dev/null +++ b/packages/database/src/fixtures/crossAppNodes.ts @@ -0,0 +1,164 @@ +import type { + LocalConceptDataInput, + LocalContentDataInput, +} from "../inputTypes"; +import { + FULL_CONTENT_FORMAT, + type CrossAppNode, +} from "../crossAppNodeContract"; +import { spaceUriAndLocalIdToRid } from "../lib/rid"; + +/** + * Reference fixtures for the cross-app node content contract (ENG-1847). + * + * Each fixture pairs the contract-level `CrossAppNode` with the existing + * `LocalConceptDataInput` + `LocalContentDataInput[]` it persists as — showing + * downstream Roam/Obsidian tickets exactly how the contract maps onto + * `upsert_concepts` / `upsert_content` without redefining the payload. The + * fixtures use the `space_url` / `author_local_id` string keys so they stay + * portable; the live source apps pass their resolved numeric `space_id` / + * `author_id` from `SupabaseContext` instead. + */ +export type CrossAppNodeFixture = { + node: CrossAppNode; + concept: LocalConceptDataInput; + contents: LocalContentDataInput[]; +}; + +// --- Roam-origin node: a Claim shared from a Roam graph --------------------- + +const ROAM_SPACE_URL = "https://roamresearch.com/#/app/MAPLab"; +const ROAM_NODE_ID = "tgWb6JozF"; // a Roam block/page uid +const ROAM_CLAIM_SCHEMA_ID = "rCLM0schema"; // source_local_id of the Claim schema Concept +const ROAM_NODE_RID = spaceUriAndLocalIdToRid(ROAM_SPACE_URL, ROAM_NODE_ID); + +const roamFullMarkdown = `# Sleep improves memory consolidation + +Multiple studies show that sleep after learning strengthens memory traces. + +- Supported by [[EVD]] - Rasch & Born 2013 +`; + +export const roamOriginNode: CrossAppNodeFixture = { + node: { + sourceApp: "Roam", + sourceSpace: { url: ROAM_SPACE_URL, name: "MAPLab" }, + sourceLocalId: ROAM_NODE_ID, + rid: ROAM_NODE_RID, + nodeType: { sourceLocalId: ROAM_CLAIM_SCHEMA_ID, label: "Claim" }, + content: { + direct: { value: "Sleep improves memory consolidation" }, + full: { format: FULL_CONTENT_FORMAT, value: roamFullMarkdown }, + }, + sourceModifiedAt: "2026-06-12T14:00:00.000Z", + }, + concept: { + space_url: ROAM_SPACE_URL, + name: "Sleep improves memory consolidation", + source_local_id: ROAM_NODE_ID, + schema_represented_by_local_id: ROAM_CLAIM_SCHEMA_ID, + is_schema: false, + author_local_id: "roam-account-uid", + created: "2026-06-10T09:00:00.000Z", + last_modified: "2026-06-12T14:00:00.000Z", + }, + contents: [ + { + space_url: ROAM_SPACE_URL, + source_local_id: ROAM_NODE_ID, + variant: "direct", + scale: "document", + text: "Sleep improves memory consolidation", + author_local_id: "roam-account-uid", + created: "2026-06-10T09:00:00.000Z", + last_modified: "2026-06-12T14:00:00.000Z", + }, + { + space_url: ROAM_SPACE_URL, + source_local_id: ROAM_NODE_ID, + variant: "full", + scale: "document", + text: roamFullMarkdown, + author_local_id: "roam-account-uid", + created: "2026-06-10T09:00:00.000Z", + last_modified: "2026-06-12T14:00:00.000Z", + }, + ], +}; + +// --- Obsidian-origin node: an Evidence note shared from an Obsidian vault ---- + +const OBSIDIAN_VAULT_ID = "9a8b7c6d5e4f3210"; // app.appId +const OBSIDIAN_SPACE_URL = `obsidian:${OBSIDIAN_VAULT_ID}`; +const OBSIDIAN_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; // uuidv7 nodeInstanceId +const OBSIDIAN_EVD_SCHEMA_ID = "evd-7c1f9a2b"; // nodeTypeId +const OBSIDIAN_FILE_PATH = "Discourse Nodes/EVD - REM sleep and recall.md"; +const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; // file basename +const OBSIDIAN_NODE_RID = spaceUriAndLocalIdToRid( + OBSIDIAN_SPACE_URL, + OBSIDIAN_NODE_ID, + "note", +); + +// Obsidian's `full` variant is the entire file as read from the vault, which +// includes the YAML frontmatter — a known markdown-fidelity wrinkle the +// destination materialization (ENG-1858 / ENG-1872) must handle. +const obsidianFullMarkdown = `--- +nodeTypeId: ${OBSIDIAN_EVD_SCHEMA_ID} +nodeInstanceId: ${OBSIDIAN_NODE_ID} +--- + +# REM sleep correlates with recall + +Participants with more REM sleep showed better next-day recall. +`; + +export const obsidianOriginNode: CrossAppNodeFixture = { + node: { + sourceApp: "Obsidian", + sourceSpace: { url: OBSIDIAN_SPACE_URL, name: "Research Vault" }, + sourceLocalId: OBSIDIAN_NODE_ID, + rid: OBSIDIAN_NODE_RID, + nodeType: { sourceLocalId: OBSIDIAN_EVD_SCHEMA_ID, label: "Evidence" }, + content: { + direct: { value: OBSIDIAN_TITLE }, + full: { format: FULL_CONTENT_FORMAT, value: obsidianFullMarkdown }, + }, + sourceModifiedAt: "2026-06-14T10:30:00.000Z", + }, + concept: { + space_url: OBSIDIAN_SPACE_URL, + name: OBSIDIAN_FILE_PATH, // Obsidian uses the file path as the Concept name + source_local_id: OBSIDIAN_NODE_ID, + schema_represented_by_local_id: OBSIDIAN_EVD_SCHEMA_ID, + is_schema: false, + author_local_id: "obsidian-account-uid", + created: "2026-06-13T08:00:00.000Z", + last_modified: "2026-06-14T10:30:00.000Z", + literal_content: { label: OBSIDIAN_TITLE }, + }, + contents: [ + { + space_url: OBSIDIAN_SPACE_URL, + source_local_id: OBSIDIAN_NODE_ID, + variant: "direct", + scale: "document", + text: OBSIDIAN_TITLE, + author_local_id: "obsidian-account-uid", + created: "2026-06-13T08:00:00.000Z", + last_modified: "2026-06-14T10:30:00.000Z", + metadata: { filePath: OBSIDIAN_FILE_PATH }, + }, + { + space_url: OBSIDIAN_SPACE_URL, + source_local_id: OBSIDIAN_NODE_ID, + variant: "full", + scale: "document", + text: obsidianFullMarkdown, + author_local_id: "obsidian-account-uid", + created: "2026-06-13T08:00:00.000Z", + last_modified: "2026-06-14T10:30:00.000Z", + metadata: { filePath: OBSIDIAN_FILE_PATH }, + }, + ], +}; diff --git a/packages/database/src/lib/rid.ts b/packages/database/src/lib/rid.ts new file mode 100644 index 000000000..768ff6d8a --- /dev/null +++ b/packages/database/src/lib/rid.ts @@ -0,0 +1,38 @@ +// Express a pair of (spaceUri, sourceLocalId) as a single stable cross-app id +// (RID), and parse it back. Shared by Roam and Obsidian so both apps use one +// identity format for cross-app share / discovery / import / refresh. +// We follow https://github.com/BlockScience/rid-lib: +// Either a Web URL, with the last segment as the sourceLocalId; +// OR the format `orn:.:/` +// With the assumption that the sourceUri has the form : +// The subtype may be omitted. + +export const spaceUriAndLocalIdToRid = ( + spaceUri: string, + localId: string, + subtype?: string, +): string => { + if (spaceUri.startsWith("http")) return `${spaceUri}/${localId}`; + const parts = spaceUri.split(":"); + if (parts.length === 2) + return subtype + ? `orn:${parts[0]}.${subtype}:${parts[1]}/${localId}` + : `orn:${parts[0]}:${parts[1]}/${localId}`; + throw new Error("Unrecognized spaceUri"); +}; + +export const ridToSpaceUriAndLocalId = ( + rid: string, +): { spaceUri: string; sourceLocalId: string } => { + const m = rid.match(/^orn:(\w+)\.(\w+):(.*)\/([^/]+)$/); + if (m) { + return { spaceUri: `${m[1]}:${m[3]}`, sourceLocalId: m[4]! }; + } + const m2 = rid.match(/^orn:(\w+):(.*)\/([^/]+)$/); + if (m2) { + return { spaceUri: `${m2[1]}:${m2[2]}`, sourceLocalId: m2[3]! }; + } + const parts = rid.split("/"); + const sourceLocalId = parts.pop()!; + return { spaceUri: parts.join("/"), sourceLocalId }; +}; From a967efafee6224725d08c5f297e222e5fddfa4d4 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 13:37:22 +0530 Subject: [PATCH 2/6] [ENG-1847] Trim cross-app contract comments --- packages/database/src/crossAppNodeContract.ts | 71 +++++-------------- 1 file changed, 16 insertions(+), 55 deletions(-) diff --git a/packages/database/src/crossAppNodeContract.ts b/packages/database/src/crossAppNodeContract.ts index 7f6582b37..b51d11d16 100644 --- a/packages/database/src/crossAppNodeContract.ts +++ b/packages/database/src/crossAppNodeContract.ts @@ -1,95 +1,56 @@ import type { Enums } from "./dbTypes"; -/** - * Shared cross-app discourse-node content contract (MVP0). - * - * This is the payload that lets Roam and Obsidian discover, import and refresh - * each other's discourse nodes. It is a typed *view* over data that already - * persists through `@repo/database/inputTypes` (`LocalConceptDataInput` / - * `LocalContentDataInput`) and the `upsert_concepts` / `upsert_content` RPCs — - * it does NOT introduce a new persistence path. Build/parse the `rid` with the - * helpers in `@repo/database/lib/rid`. The full spec — field-by-field mapping to - * the Concept/Content tables and markdown fidelity limits — lives on Linear - * issue ENG-1847. - */ - -/** Source app a shared node originates from. Mirrors the DB `Platform` enum. */ export type Platform = Enums<"Platform">; // "Roam" | "Obsidian" -/** Persisted content scales. Mirrors the DB `ContentVariant` enum. */ export type ContentVariant = Enums<"ContentVariant">; -/** - * The Content variants every shared node must persist: - * - `direct`: the import-list title. - * - `full`: a self-sufficient markdown body the destination can materialize - * without querying the source app. - */ export const SHARED_NODE_CONTENT_VARIANTS = [ "direct", "full", ] as const satisfies readonly ContentVariant[]; -/** - * MIME type of the `full` variant in MVP0. Markdown is the v0 content model; - * atJSON is the planned v1 successor (F16). Keep this as the single place that - * names the format so v1 does not have to hunt down hardcoded strings. - */ export const FULL_CONTENT_FORMAT = "text/markdown"; -/** Identity of the node-type schema the destination maps to / creates from. */ export type CrossAppNodeType = { /** - * `source_local_id` of the node-type *schema* Concept in the source space - * (the Concept with `is_schema = true`). Maps to - * `LocalConceptDataInput.schema_represented_by_local_id` on the instance. + * Source-space schema Concept id (`Concept.source_local_id` where + * `is_schema = true`). Destination apps use this to map or create a local + * node type. */ sourceLocalId: string; - /** Human-readable node-type label, e.g. "Claim". */ label: string; }; -/** The required content variants of a shared node. */ export type CrossAppNodeContent = { - /** Import-list title. Persisted as the `direct` Content variant (`text`). */ direct: { value: string }; - /** - * Self-sufficient markdown body. Persisted as the `full` Content variant - * (`text`); `format` is the contract-level media type for that text in MVP0. - */ - full: { format: typeof FULL_CONTENT_FORMAT; value: string }; + full: { + /** + * Contract media type for `full.value`; current Content rows store the + * markdown in `text`, not in a typed media column. + */ + format: typeof FULL_CONTENT_FORMAT; + value: string; + }; }; -/** - * Stable cross-app identity (F9). The triple - * (`sourceApp`, `sourceSpace.url`, `sourceLocalId`) is equivalent to `rid`; - * build/parse `rid` with `spaceUriAndLocalIdToRid` / `ridToSpaceUriAndLocalId` - * from `@repo/database/lib/rid`. Duplicate-prevention and refresh must key on - * this identity, never on the display title. - */ export type CrossAppNodeIdentity = { sourceApp: Platform; /** - * Source space: `Space.url` (portable cross-app id) and `Space.name` - * (display). Do not use numeric `Space.id` as the payload identity; it is - * local to the receiving database. + * Use `Space.url`, not numeric `Space.id`, because `Space.id` is local to the + * receiving database. */ sourceSpace: { url: string; name: string }; - /** The node's `source_local_id` within its source space. */ sourceLocalId: string; - /** Stable cross-app id derived from (`sourceSpace.url`, `sourceLocalId`). */ + /** Stable source identity derived from `sourceSpace.url` + `sourceLocalId`. */ rid: string; }; -/** The shared cross-app discourse-node payload (discovery + import facing). */ export type CrossAppNode = CrossAppNodeIdentity & { nodeType: CrossAppNodeType; content: CrossAppNodeContent; /** - * ISO-8601 source last-modified time. Use the source node modified timestamp, - * or the latest `Content.last_modified` across the required `direct` and - * `full` variants when deriving from persisted rows. Basis for freshness - * (F13), refresh, and duplicate-prevention. + * Source modified timestamp, or latest required `Content.last_modified` when + * deriving from persisted `direct` and `full` rows. */ sourceModifiedAt: string; }; From 62755d005827d29094d97eb1b6c300e470ca7249 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 14:16:24 +0530 Subject: [PATCH 3/6] [ENG-1847] Simplify cross-app contract naming --- apps/obsidian/src/utils/importNodes.ts | 2 +- apps/obsidian/src/utils/importPreview.ts | 2 +- apps/obsidian/src/utils/importRelations.ts | 2 +- apps/obsidian/src/utils/relationsStore.ts | 5 +- apps/obsidian/src/utils/rid.ts | 6 -- apps/obsidian/src/utils/spaceFromRid.ts | 2 +- apps/obsidian/src/utils/typeUtils.ts | 2 +- packages/database/src/crossAppNodeContract.ts | 73 +++++++------------ .../database/src/fixtures/crossAppNodes.ts | 60 +++++++-------- packages/database/src/lib/rid.ts | 6 +- 10 files changed, 64 insertions(+), 96 deletions(-) delete mode 100644 apps/obsidian/src/utils/rid.ts diff --git a/apps/obsidian/src/utils/importNodes.ts b/apps/obsidian/src/utils/importNodes.ts index 1502a3417..aebf0a4e9 100644 --- a/apps/obsidian/src/utils/importNodes.ts +++ b/apps/obsidian/src/utils/importNodes.ts @@ -10,7 +10,7 @@ import { getImportedNodesInfo, getLocalNodeKeyToEndpointId, } from "~/utils/relationsStore"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; import type { PostgrestResponse } from "@supabase/supabase-js"; import type { Tables } from "@repo/database/dbTypes"; import { getSpaceNameIdFromRid } from "./spaceFromRid"; diff --git a/apps/obsidian/src/utils/importPreview.ts b/apps/obsidian/src/utils/importPreview.ts index 3d2e85335..327cda171 100644 --- a/apps/obsidian/src/utils/importPreview.ts +++ b/apps/obsidian/src/utils/importPreview.ts @@ -11,7 +11,7 @@ import { fetchRelationInstancesFromSpace, type RemoteRelationInstance, } from "./importRelations"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; export type RelationTriplet = { sourceNodeTypeName: string; diff --git a/apps/obsidian/src/utils/importRelations.ts b/apps/obsidian/src/utils/importRelations.ts index a1acd207c..a3efd01e1 100644 --- a/apps/obsidian/src/utils/importRelations.ts +++ b/apps/obsidian/src/utils/importRelations.ts @@ -3,7 +3,7 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; import { uuidv7 } from "uuidv7"; import type DiscourseGraphPlugin from "~/index"; import type { DiscourseRelationType, DiscourseRelation } from "~/types"; -import { spaceUriAndLocalIdToRid } from "./rid"; +import { spaceUriAndLocalIdToRid } from "@repo/database/lib/rid"; import { loadRelations, addRelationNoCheck, diff --git a/apps/obsidian/src/utils/relationsStore.ts b/apps/obsidian/src/utils/relationsStore.ts index b7c8f92cc..d27ead83b 100644 --- a/apps/obsidian/src/utils/relationsStore.ts +++ b/apps/obsidian/src/utils/relationsStore.ts @@ -7,7 +7,10 @@ import { getVaultId, getLocalSpaceUri } from "./supabaseContext"; import type { RelationInstance } from "~/types"; import { QueryEngine, getImportedNodesRaw } from "~/services/QueryEngine"; import { publishNewRelation } from "./publishNode"; -import { ridToSpaceUriAndLocalId, spaceUriAndLocalIdToRid } from "./rid"; +import { + ridToSpaceUriAndLocalId, + spaceUriAndLocalIdToRid, +} from "@repo/database/lib/rid"; import { getSpaceIdsBySpaceUris } from "./spaceFromRid"; const RELATIONS_FILE_NAME = "relations.json"; diff --git a/apps/obsidian/src/utils/rid.ts b/apps/obsidian/src/utils/rid.ts deleted file mode 100644 index 08ef6aed2..000000000 --- a/apps/obsidian/src/utils/rid.ts +++ /dev/null @@ -1,6 +0,0 @@ -// The RID helpers now live in the shared database package so Roam and Obsidian -// share one cross-app identity format. See @repo/database/lib/rid. -export { - spaceUriAndLocalIdToRid, - ridToSpaceUriAndLocalId, -} from "@repo/database/lib/rid"; diff --git a/apps/obsidian/src/utils/spaceFromRid.ts b/apps/obsidian/src/utils/spaceFromRid.ts index 3210fbf88..68f5ca8a8 100644 --- a/apps/obsidian/src/utils/spaceFromRid.ts +++ b/apps/obsidian/src/utils/spaceFromRid.ts @@ -1,5 +1,5 @@ import type { DGSupabaseClient } from "@repo/database/lib/client"; -import { ridToSpaceUriAndLocalId } from "./rid"; +import { ridToSpaceUriAndLocalId } from "@repo/database/lib/rid"; export const getSpaceNameIdFromRid = async ( client: DGSupabaseClient, diff --git a/apps/obsidian/src/utils/typeUtils.ts b/apps/obsidian/src/utils/typeUtils.ts index 09ebee4b7..9540ea81f 100644 --- a/apps/obsidian/src/utils/typeUtils.ts +++ b/apps/obsidian/src/utils/typeUtils.ts @@ -1,6 +1,6 @@ import type DiscourseGraphPlugin from "~/index"; import { DiscourseNode, DiscourseRelationType, ImportStatus } from "~/types"; -import { ridToSpaceUriAndLocalId } from "./rid"; +import { ridToSpaceUriAndLocalId } from "@repo/database/lib/rid"; export const getNodeTypeById = ( plugin: DiscourseGraphPlugin, diff --git a/packages/database/src/crossAppNodeContract.ts b/packages/database/src/crossAppNodeContract.ts index b51d11d16..1804a98e7 100644 --- a/packages/database/src/crossAppNodeContract.ts +++ b/packages/database/src/crossAppNodeContract.ts @@ -1,53 +1,36 @@ -import type { Enums } from "./dbTypes"; - -export type Platform = Enums<"Platform">; // "Roam" | "Obsidian" - -export type ContentVariant = Enums<"ContentVariant">; - -export const SHARED_NODE_CONTENT_VARIANTS = [ - "direct", - "full", -] as const satisfies readonly ContentVariant[]; - -export const FULL_CONTENT_FORMAT = "text/markdown"; - -export type CrossAppNodeType = { +export type CrossAppNode = { + sourceApp: "roam" | "obsidian"; /** - * Source-space schema Concept id (`Concept.source_local_id` where - * `is_schema = true`). Destination apps use this to map or create a local - * node type. + * Stable source-space id. Maps to `Space.url`, not numeric `Space.id`. */ - sourceLocalId: string; - label: string; -}; - -export type CrossAppNodeContent = { - direct: { value: string }; - full: { + sourceSpaceId: string; + sourceSpaceName: string; + /** + * Node id inside the source app/space. + * Roam: page/block UID. + * Obsidian: nodeInstanceId. + */ + sourceNodeId: string; + sourceNodeRid: string; + nodeType: { /** - * Contract media type for `full.value`; current Content rows store the - * markdown in `text`, not in a typed media column. + * Node type/schema id inside the source app/space. + * Maps to the schema Concept's `source_local_id`. */ - format: typeof FULL_CONTENT_FORMAT; - value: string; + sourceNodeTypeId: string; + label: string; + }; + content: { + direct: { value: string }; + full: { + /** + * Contract media type for `full.value`; current Content rows store the + * markdown in `text`, not in a typed media column. + */ + format: "text/markdown"; + value: string; + }; }; -}; - -export type CrossAppNodeIdentity = { - sourceApp: Platform; - /** - * Use `Space.url`, not numeric `Space.id`, because `Space.id` is local to the - * receiving database. - */ - sourceSpace: { url: string; name: string }; - sourceLocalId: string; - /** Stable source identity derived from `sourceSpace.url` + `sourceLocalId`. */ - rid: string; -}; - -export type CrossAppNode = CrossAppNodeIdentity & { - nodeType: CrossAppNodeType; - content: CrossAppNodeContent; /** * Source modified timestamp, or latest required `Content.last_modified` when * deriving from persisted `direct` and `full` rows. diff --git a/packages/database/src/fixtures/crossAppNodes.ts b/packages/database/src/fixtures/crossAppNodes.ts index b98ecbc13..49ae0d136 100644 --- a/packages/database/src/fixtures/crossAppNodes.ts +++ b/packages/database/src/fixtures/crossAppNodes.ts @@ -2,22 +2,14 @@ import type { LocalConceptDataInput, LocalContentDataInput, } from "../inputTypes"; -import { - FULL_CONTENT_FORMAT, - type CrossAppNode, -} from "../crossAppNodeContract"; +import type { CrossAppNode } from "../crossAppNodeContract"; import { spaceUriAndLocalIdToRid } from "../lib/rid"; /** - * Reference fixtures for the cross-app node content contract (ENG-1847). + * Contract fixtures with the database inputs they persist as. * - * Each fixture pairs the contract-level `CrossAppNode` with the existing - * `LocalConceptDataInput` + `LocalContentDataInput[]` it persists as — showing - * downstream Roam/Obsidian tickets exactly how the contract maps onto - * `upsert_concepts` / `upsert_content` without redefining the payload. The - * fixtures use the `space_url` / `author_local_id` string keys so they stay - * portable; the live source apps pass their resolved numeric `space_id` / - * `author_id` from `SupabaseContext` instead. + * These use portable string keys; live apps resolve numeric IDs through + * `SupabaseContext`. */ export type CrossAppNodeFixture = { node: CrossAppNode; @@ -25,11 +17,9 @@ export type CrossAppNodeFixture = { contents: LocalContentDataInput[]; }; -// --- Roam-origin node: a Claim shared from a Roam graph --------------------- - const ROAM_SPACE_URL = "https://roamresearch.com/#/app/MAPLab"; -const ROAM_NODE_ID = "tgWb6JozF"; // a Roam block/page uid -const ROAM_CLAIM_SCHEMA_ID = "rCLM0schema"; // source_local_id of the Claim schema Concept +const ROAM_NODE_ID = "tgWb6JozF"; +const ROAM_CLAIM_SCHEMA_ID = "rCLM0schema"; const ROAM_NODE_RID = spaceUriAndLocalIdToRid(ROAM_SPACE_URL, ROAM_NODE_ID); const roamFullMarkdown = `# Sleep improves memory consolidation @@ -41,14 +31,15 @@ Multiple studies show that sleep after learning strengthens memory traces. export const roamOriginNode: CrossAppNodeFixture = { node: { - sourceApp: "Roam", - sourceSpace: { url: ROAM_SPACE_URL, name: "MAPLab" }, - sourceLocalId: ROAM_NODE_ID, - rid: ROAM_NODE_RID, - nodeType: { sourceLocalId: ROAM_CLAIM_SCHEMA_ID, label: "Claim" }, + sourceApp: "roam", + sourceSpaceId: ROAM_SPACE_URL, + sourceSpaceName: "MAPLab", + sourceNodeId: ROAM_NODE_ID, + sourceNodeRid: ROAM_NODE_RID, + nodeType: { sourceNodeTypeId: ROAM_CLAIM_SCHEMA_ID, label: "Claim" }, content: { direct: { value: "Sleep improves memory consolidation" }, - full: { format: FULL_CONTENT_FORMAT, value: roamFullMarkdown }, + full: { format: "text/markdown", value: roamFullMarkdown }, }, sourceModifiedAt: "2026-06-12T14:00:00.000Z", }, @@ -86,14 +77,12 @@ export const roamOriginNode: CrossAppNodeFixture = { ], }; -// --- Obsidian-origin node: an Evidence note shared from an Obsidian vault ---- - -const OBSIDIAN_VAULT_ID = "9a8b7c6d5e4f3210"; // app.appId +const OBSIDIAN_VAULT_ID = "9a8b7c6d5e4f3210"; const OBSIDIAN_SPACE_URL = `obsidian:${OBSIDIAN_VAULT_ID}`; -const OBSIDIAN_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; // uuidv7 nodeInstanceId -const OBSIDIAN_EVD_SCHEMA_ID = "evd-7c1f9a2b"; // nodeTypeId +const OBSIDIAN_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; +const OBSIDIAN_EVD_SCHEMA_ID = "evd-7c1f9a2b"; const OBSIDIAN_FILE_PATH = "Discourse Nodes/EVD - REM sleep and recall.md"; -const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; // file basename +const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; const OBSIDIAN_NODE_RID = spaceUriAndLocalIdToRid( OBSIDIAN_SPACE_URL, OBSIDIAN_NODE_ID, @@ -115,20 +104,21 @@ Participants with more REM sleep showed better next-day recall. export const obsidianOriginNode: CrossAppNodeFixture = { node: { - sourceApp: "Obsidian", - sourceSpace: { url: OBSIDIAN_SPACE_URL, name: "Research Vault" }, - sourceLocalId: OBSIDIAN_NODE_ID, - rid: OBSIDIAN_NODE_RID, - nodeType: { sourceLocalId: OBSIDIAN_EVD_SCHEMA_ID, label: "Evidence" }, + sourceApp: "obsidian", + sourceSpaceId: OBSIDIAN_SPACE_URL, + sourceSpaceName: "Research Vault", + sourceNodeId: OBSIDIAN_NODE_ID, + sourceNodeRid: OBSIDIAN_NODE_RID, + nodeType: { sourceNodeTypeId: OBSIDIAN_EVD_SCHEMA_ID, label: "Evidence" }, content: { direct: { value: OBSIDIAN_TITLE }, - full: { format: FULL_CONTENT_FORMAT, value: obsidianFullMarkdown }, + full: { format: "text/markdown", value: obsidianFullMarkdown }, }, sourceModifiedAt: "2026-06-14T10:30:00.000Z", }, concept: { space_url: OBSIDIAN_SPACE_URL, - name: OBSIDIAN_FILE_PATH, // Obsidian uses the file path as the Concept name + name: OBSIDIAN_FILE_PATH, source_local_id: OBSIDIAN_NODE_ID, schema_represented_by_local_id: OBSIDIAN_EVD_SCHEMA_ID, is_schema: false, diff --git a/packages/database/src/lib/rid.ts b/packages/database/src/lib/rid.ts index 768ff6d8a..c96db9e3d 100644 --- a/packages/database/src/lib/rid.ts +++ b/packages/database/src/lib/rid.ts @@ -1,7 +1,5 @@ -// Express a pair of (spaceUri, sourceLocalId) as a single stable cross-app id -// (RID), and parse it back. Shared by Roam and Obsidian so both apps use one -// identity format for cross-app share / discovery / import / refresh. -// We follow https://github.com/BlockScience/rid-lib: +// Functions to express a pair of spaceUri, sourceLocalId as a single string, and back. +// We're following https://github.com/BlockScience/rid-lib: // Either a Web URL, with the last segment as the sourceLocalId; // OR the format `orn:.:/` // With the assumption that the sourceUri has the form : From ee6f436a5e3bf4bf186df4723e969b882340107d Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 14:28:56 +0530 Subject: [PATCH 4/6] [ENG-1847] Keep cross-app node examples focused --- packages/database/package.json | 3 +- .../src/crossAppNodeContract.example.ts | 69 ++++++++ .../database/src/fixtures/crossAppNodes.ts | 154 ------------------ 3 files changed, 70 insertions(+), 156 deletions(-) create mode 100644 packages/database/src/crossAppNodeContract.example.ts delete mode 100644 packages/database/src/fixtures/crossAppNodes.ts diff --git a/packages/database/package.json b/packages/database/package.json index 9a814f52f..ab7f36f84 100644 --- a/packages/database/package.json +++ b/packages/database/package.json @@ -12,8 +12,7 @@ }, "./dbTypes": "./src/dbTypes.ts", "./inputTypes": "./src/inputTypes.ts", - "./crossAppNodeContract": "./src/crossAppNodeContract.ts", - "./fixtures/*": "./src/fixtures/*.ts" + "./crossAppNodeContract": "./src/crossAppNodeContract.ts" }, "typesVersions": { "*": { diff --git a/packages/database/src/crossAppNodeContract.example.ts b/packages/database/src/crossAppNodeContract.example.ts new file mode 100644 index 000000000..e7ce04e7d --- /dev/null +++ b/packages/database/src/crossAppNodeContract.example.ts @@ -0,0 +1,69 @@ +import type { CrossAppNode } from "./crossAppNodeContract"; +import { spaceUriAndLocalIdToRid } from "./lib/rid"; + +const ROAM_SOURCE_SPACE_ID = "https://roamresearch.com/#/app/MAPLab"; +const ROAM_SOURCE_NODE_ID = "tgWb6JozF"; +const ROAM_SOURCE_NODE_TYPE_ID = "rCLM0schema"; + +const roamFullMarkdown = `# Sleep improves memory consolidation + +Multiple studies show that sleep after learning strengthens memory traces. + +- Supported by [[EVD]] - Rasch & Born 2013 +`; + +export const roamOriginNodeExample: CrossAppNode = { + sourceApp: "roam", + sourceSpaceId: ROAM_SOURCE_SPACE_ID, + sourceSpaceName: "MAPLab", + sourceNodeId: ROAM_SOURCE_NODE_ID, + sourceNodeRid: spaceUriAndLocalIdToRid( + ROAM_SOURCE_SPACE_ID, + ROAM_SOURCE_NODE_ID, + ), + nodeType: { + sourceNodeTypeId: ROAM_SOURCE_NODE_TYPE_ID, + label: "Claim", + }, + content: { + direct: { value: "Sleep improves memory consolidation" }, + full: { format: "text/markdown", value: roamFullMarkdown }, + }, + sourceModifiedAt: "2026-06-12T14:00:00.000Z", +}; + +const OBSIDIAN_SOURCE_SPACE_ID = "obsidian:9a8b7c6d5e4f3210"; +const OBSIDIAN_SOURCE_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; +const OBSIDIAN_SOURCE_NODE_TYPE_ID = "evd-7c1f9a2b"; +const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; + +const obsidianFullMarkdown = `--- +nodeTypeId: ${OBSIDIAN_SOURCE_NODE_TYPE_ID} +nodeInstanceId: ${OBSIDIAN_SOURCE_NODE_ID} +--- + +# REM sleep correlates with recall + +Participants with more REM sleep showed better next-day recall. +`; + +export const obsidianOriginNodeExample: CrossAppNode = { + sourceApp: "obsidian", + sourceSpaceId: OBSIDIAN_SOURCE_SPACE_ID, + sourceSpaceName: "Research Vault", + sourceNodeId: OBSIDIAN_SOURCE_NODE_ID, + sourceNodeRid: spaceUriAndLocalIdToRid( + OBSIDIAN_SOURCE_SPACE_ID, + OBSIDIAN_SOURCE_NODE_ID, + "note", + ), + nodeType: { + sourceNodeTypeId: OBSIDIAN_SOURCE_NODE_TYPE_ID, + label: "Evidence", + }, + content: { + direct: { value: OBSIDIAN_TITLE }, + full: { format: "text/markdown", value: obsidianFullMarkdown }, + }, + sourceModifiedAt: "2026-06-14T10:30:00.000Z", +}; diff --git a/packages/database/src/fixtures/crossAppNodes.ts b/packages/database/src/fixtures/crossAppNodes.ts deleted file mode 100644 index 49ae0d136..000000000 --- a/packages/database/src/fixtures/crossAppNodes.ts +++ /dev/null @@ -1,154 +0,0 @@ -import type { - LocalConceptDataInput, - LocalContentDataInput, -} from "../inputTypes"; -import type { CrossAppNode } from "../crossAppNodeContract"; -import { spaceUriAndLocalIdToRid } from "../lib/rid"; - -/** - * Contract fixtures with the database inputs they persist as. - * - * These use portable string keys; live apps resolve numeric IDs through - * `SupabaseContext`. - */ -export type CrossAppNodeFixture = { - node: CrossAppNode; - concept: LocalConceptDataInput; - contents: LocalContentDataInput[]; -}; - -const ROAM_SPACE_URL = "https://roamresearch.com/#/app/MAPLab"; -const ROAM_NODE_ID = "tgWb6JozF"; -const ROAM_CLAIM_SCHEMA_ID = "rCLM0schema"; -const ROAM_NODE_RID = spaceUriAndLocalIdToRid(ROAM_SPACE_URL, ROAM_NODE_ID); - -const roamFullMarkdown = `# Sleep improves memory consolidation - -Multiple studies show that sleep after learning strengthens memory traces. - -- Supported by [[EVD]] - Rasch & Born 2013 -`; - -export const roamOriginNode: CrossAppNodeFixture = { - node: { - sourceApp: "roam", - sourceSpaceId: ROAM_SPACE_URL, - sourceSpaceName: "MAPLab", - sourceNodeId: ROAM_NODE_ID, - sourceNodeRid: ROAM_NODE_RID, - nodeType: { sourceNodeTypeId: ROAM_CLAIM_SCHEMA_ID, label: "Claim" }, - content: { - direct: { value: "Sleep improves memory consolidation" }, - full: { format: "text/markdown", value: roamFullMarkdown }, - }, - sourceModifiedAt: "2026-06-12T14:00:00.000Z", - }, - concept: { - space_url: ROAM_SPACE_URL, - name: "Sleep improves memory consolidation", - source_local_id: ROAM_NODE_ID, - schema_represented_by_local_id: ROAM_CLAIM_SCHEMA_ID, - is_schema: false, - author_local_id: "roam-account-uid", - created: "2026-06-10T09:00:00.000Z", - last_modified: "2026-06-12T14:00:00.000Z", - }, - contents: [ - { - space_url: ROAM_SPACE_URL, - source_local_id: ROAM_NODE_ID, - variant: "direct", - scale: "document", - text: "Sleep improves memory consolidation", - author_local_id: "roam-account-uid", - created: "2026-06-10T09:00:00.000Z", - last_modified: "2026-06-12T14:00:00.000Z", - }, - { - space_url: ROAM_SPACE_URL, - source_local_id: ROAM_NODE_ID, - variant: "full", - scale: "document", - text: roamFullMarkdown, - author_local_id: "roam-account-uid", - created: "2026-06-10T09:00:00.000Z", - last_modified: "2026-06-12T14:00:00.000Z", - }, - ], -}; - -const OBSIDIAN_VAULT_ID = "9a8b7c6d5e4f3210"; -const OBSIDIAN_SPACE_URL = `obsidian:${OBSIDIAN_VAULT_ID}`; -const OBSIDIAN_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; -const OBSIDIAN_EVD_SCHEMA_ID = "evd-7c1f9a2b"; -const OBSIDIAN_FILE_PATH = "Discourse Nodes/EVD - REM sleep and recall.md"; -const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; -const OBSIDIAN_NODE_RID = spaceUriAndLocalIdToRid( - OBSIDIAN_SPACE_URL, - OBSIDIAN_NODE_ID, - "note", -); - -// Obsidian's `full` variant is the entire file as read from the vault, which -// includes the YAML frontmatter — a known markdown-fidelity wrinkle the -// destination materialization (ENG-1858 / ENG-1872) must handle. -const obsidianFullMarkdown = `--- -nodeTypeId: ${OBSIDIAN_EVD_SCHEMA_ID} -nodeInstanceId: ${OBSIDIAN_NODE_ID} ---- - -# REM sleep correlates with recall - -Participants with more REM sleep showed better next-day recall. -`; - -export const obsidianOriginNode: CrossAppNodeFixture = { - node: { - sourceApp: "obsidian", - sourceSpaceId: OBSIDIAN_SPACE_URL, - sourceSpaceName: "Research Vault", - sourceNodeId: OBSIDIAN_NODE_ID, - sourceNodeRid: OBSIDIAN_NODE_RID, - nodeType: { sourceNodeTypeId: OBSIDIAN_EVD_SCHEMA_ID, label: "Evidence" }, - content: { - direct: { value: OBSIDIAN_TITLE }, - full: { format: "text/markdown", value: obsidianFullMarkdown }, - }, - sourceModifiedAt: "2026-06-14T10:30:00.000Z", - }, - concept: { - space_url: OBSIDIAN_SPACE_URL, - name: OBSIDIAN_FILE_PATH, - source_local_id: OBSIDIAN_NODE_ID, - schema_represented_by_local_id: OBSIDIAN_EVD_SCHEMA_ID, - is_schema: false, - author_local_id: "obsidian-account-uid", - created: "2026-06-13T08:00:00.000Z", - last_modified: "2026-06-14T10:30:00.000Z", - literal_content: { label: OBSIDIAN_TITLE }, - }, - contents: [ - { - space_url: OBSIDIAN_SPACE_URL, - source_local_id: OBSIDIAN_NODE_ID, - variant: "direct", - scale: "document", - text: OBSIDIAN_TITLE, - author_local_id: "obsidian-account-uid", - created: "2026-06-13T08:00:00.000Z", - last_modified: "2026-06-14T10:30:00.000Z", - metadata: { filePath: OBSIDIAN_FILE_PATH }, - }, - { - space_url: OBSIDIAN_SPACE_URL, - source_local_id: OBSIDIAN_NODE_ID, - variant: "full", - scale: "document", - text: obsidianFullMarkdown, - author_local_id: "obsidian-account-uid", - created: "2026-06-13T08:00:00.000Z", - last_modified: "2026-06-14T10:30:00.000Z", - metadata: { filePath: OBSIDIAN_FILE_PATH }, - }, - ], -}; From 5954c548c3fbd1f81427860994eb49c5da3cb8a7 Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 14:31:34 +0530 Subject: [PATCH 5/6] [ENG-1847] Inline single-use node example values --- packages/database/src/crossAppNodeContract.example.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/database/src/crossAppNodeContract.example.ts b/packages/database/src/crossAppNodeContract.example.ts index e7ce04e7d..efb0f5b1f 100644 --- a/packages/database/src/crossAppNodeContract.example.ts +++ b/packages/database/src/crossAppNodeContract.example.ts @@ -3,7 +3,6 @@ import { spaceUriAndLocalIdToRid } from "./lib/rid"; const ROAM_SOURCE_SPACE_ID = "https://roamresearch.com/#/app/MAPLab"; const ROAM_SOURCE_NODE_ID = "tgWb6JozF"; -const ROAM_SOURCE_NODE_TYPE_ID = "rCLM0schema"; const roamFullMarkdown = `# Sleep improves memory consolidation @@ -22,7 +21,7 @@ export const roamOriginNodeExample: CrossAppNode = { ROAM_SOURCE_NODE_ID, ), nodeType: { - sourceNodeTypeId: ROAM_SOURCE_NODE_TYPE_ID, + sourceNodeTypeId: "rCLM0schema", label: "Claim", }, content: { @@ -35,7 +34,6 @@ export const roamOriginNodeExample: CrossAppNode = { const OBSIDIAN_SOURCE_SPACE_ID = "obsidian:9a8b7c6d5e4f3210"; const OBSIDIAN_SOURCE_NODE_ID = "0192f1a0-7b3c-7e2a-9f10-1a2b3c4d5e6f"; const OBSIDIAN_SOURCE_NODE_TYPE_ID = "evd-7c1f9a2b"; -const OBSIDIAN_TITLE = "EVD - REM sleep and recall"; const obsidianFullMarkdown = `--- nodeTypeId: ${OBSIDIAN_SOURCE_NODE_TYPE_ID} @@ -62,7 +60,7 @@ export const obsidianOriginNodeExample: CrossAppNode = { label: "Evidence", }, content: { - direct: { value: OBSIDIAN_TITLE }, + direct: { value: "EVD - REM sleep and recall" }, full: { format: "text/markdown", value: obsidianFullMarkdown }, }, sourceModifiedAt: "2026-06-14T10:30:00.000Z", From e757b671d7dc26ea816b5c15065b4dc9aa451a9d Mon Sep 17 00:00:00 2001 From: sid597 Date: Fri, 19 Jun 2026 21:55:17 +0530 Subject: [PATCH 6/6] Document RID local ID delimiter contract --- packages/database/src/lib/rid.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/database/src/lib/rid.ts b/packages/database/src/lib/rid.ts index c96db9e3d..f2a200acf 100644 --- a/packages/database/src/lib/rid.ts +++ b/packages/database/src/lib/rid.ts @@ -10,6 +10,9 @@ export const spaceUriAndLocalIdToRid = ( localId: string, subtype?: string, ): string => { + // Both RID forms use `/` as the sourceLocalId delimiter, so callers must pass + // slash-free localIds (or pre-encode them) for ridToSpaceUriAndLocalId to + // round-trip the original pair. if (spaceUri.startsWith("http")) return `${spaceUri}/${localId}`; const parts = spaceUri.split(":"); if (parts.length === 2)