Cookbook · Importing
A ~30-line adapter shape: parse the source, map each entity, persist via UPGClient, verify at the end.
Recipe
// adapter-markdown.ts
import { readdir, readFile } from 'node:fs/promises'
import matter from 'gray-matter'
import { UPGClient } from '@unified-product-graph/sdk'
export async function importMarkdownVault(dir: string, upg: UPGClient) {
const files = (await readdir(dir)).filter(f => f.endsWith('.md'))
let imported = 0
for (const file of files) {
const raw = await readFile(`${dir}/${file}`, 'utf-8')
const { data: front, content } = matter(raw)
const type = (front.type as string) ?? 'note'
const title = (front.title as string) ?? file.replace('.md', '')
await upg.nodes.create({
type,
title,
description: content.slice(0, 500),
tags: (front.tags as string[]) ?? [],
})
imported++
}
const report = await upg.verify()
const { score } = await upg.health()
console.log(`imported ${imported} · health ${score}/10`)
if (report.errors.length) console.warn('with warnings:', report.errors)
}What it does
Every adapter follows the same shape: enumerate source items, map each to a typed node + edges, write through UPGClient, verify at the end. gray-matter is one parser among many; swap it for your source format. Type mapping is where adapter logic lives; everything else is plumbing.
Variations
With explicit edges to a parent root
const { node: root } = await upg.nodes.create({ type: 'workspace', title: 'Vault' })
// then for each file:
await upg.nodes.create({ type, title, parent_id: root.id })Idempotent: skip if already imported
const hits = await upg.search(title, { limit: 1 })
if (hits[0]?.score && hits[0].score > 0.95) continueSee also