@moxn/kb-migrate 0.4.6 → 0.4.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -15,6 +15,8 @@ export interface NotionSourceConfig extends SourceConfig {
15
15
  token: string;
16
16
  rootPageId?: string;
17
17
  maxDepth?: number;
18
+ /** Date filter for source pages */
19
+ dateFilter?: import('../date-filter.js').DateFilter;
18
20
  }
19
21
  /** Info yielded to the migration runner for database import (post-document pass). */
20
22
  export interface NotionDatabaseImportInfo {
@@ -38,7 +40,16 @@ export declare class NotionSource extends MigrationSource<NotionSourceConfig> {
38
40
  private databases;
39
41
  private databaseEntryPageIds;
40
42
  private _documentCount;
43
+ private _validated;
44
+ /** Map of Notion database ID → KB database ID, set before extraction. */
45
+ private _databaseIdMap;
41
46
  constructor(config: NotionSourceConfig);
47
+ /**
48
+ * Set the database ID map (Notion DB ID → KB DB ID).
49
+ * Must be called after validate() and before extract() so that
50
+ * child_database blocks can be converted to database_embed blocks.
51
+ */
52
+ setDatabaseIdMap(map: Map<string, string>): void;
42
53
  get sourceType(): string;
43
54
  get sourceLocation(): string;
44
55
  validate(): Promise<void>;
@@ -27,11 +27,22 @@ export class NotionSource extends MigrationSource {
27
27
  databases = [];
28
28
  databaseEntryPageIds = new Set();
29
29
  _documentCount = 0;
30
+ _validated = false;
31
+ /** Map of Notion database ID → KB database ID, set before extraction. */
32
+ _databaseIdMap = new Map();
30
33
  constructor(config) {
31
34
  super(config);
32
35
  this.client = new NotionApiClient(config.token);
33
36
  this.mediaDownloader = new NotionMediaDownloader();
34
37
  }
38
+ /**
39
+ * Set the database ID map (Notion DB ID → KB DB ID).
40
+ * Must be called after validate() and before extract() so that
41
+ * child_database blocks can be converted to database_embed blocks.
42
+ */
43
+ setDatabaseIdMap(map) {
44
+ this._databaseIdMap = map;
45
+ }
35
46
  get sourceType() {
36
47
  return 'notion';
37
48
  }
@@ -44,14 +55,31 @@ export class NotionSource extends MigrationSource {
44
55
  // Pass 1: Discovery (validate)
45
56
  // ============================================
46
57
  async validate() {
58
+ // Idempotent — safe to call multiple times (e.g. once explicitly for
59
+ // pre-creating databases, then again inside runMigration).
60
+ if (this._validated)
61
+ return;
47
62
  // 1. Test token
48
63
  console.log('Validating Notion API token...');
49
64
  await this.client.validateToken();
50
65
  console.log(' Token valid.');
51
66
  // 2. Discover all pages
52
67
  console.log('Discovering pages...');
53
- const allNotionPages = await this.client.searchPages();
68
+ let allNotionPages = await this.client.searchPages();
54
69
  console.log(` Found ${allNotionPages.length} pages in workspace.`);
70
+ // 2b. Apply date filter if configured
71
+ if (this.config.dateFilter) {
72
+ const { matchesDateFilter } = await import('../date-filter.js');
73
+ const beforeCount = allNotionPages.length;
74
+ allNotionPages = allNotionPages.filter((page) => matchesDateFilter(this.config.dateFilter, {
75
+ createdAt: page.created_time,
76
+ modifiedAt: page.last_edited_time,
77
+ }));
78
+ const skipped = beforeCount - allNotionPages.length;
79
+ if (skipped > 0) {
80
+ console.log(` Filtered ${skipped} pages by date criteria`);
81
+ }
82
+ }
55
83
  // 3. Discover all databases
56
84
  console.log('Discovering databases...');
57
85
  const allNotionDatabases = await this.client.searchDatabases();
@@ -78,6 +106,7 @@ export class NotionSource extends MigrationSource {
78
106
  console.log(` ${this.allPages.length} pages + ${this.databases.length} databases ready for import.`);
79
107
  // 7. Initialize media downloader
80
108
  await this.mediaDownloader.init();
109
+ this._validated = true;
81
110
  }
82
111
  async getDocumentCount() {
83
112
  return this._documentCount;
@@ -300,7 +329,9 @@ export class NotionSource extends MigrationSource {
300
329
  console.log(` Skipping empty page: ${node.title}`);
301
330
  return null;
302
331
  }
303
- let sections = await blocksToSections(blocks, this.client, this.pagePathMap);
332
+ let sections = await blocksToSections(blocks, this.client, this.pagePathMap, {
333
+ databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
334
+ });
304
335
  // Download Notion-hosted media files
305
336
  sections = await this.downloadSectionMedia(sections);
306
337
  // Resolve cross-references
@@ -316,6 +347,10 @@ export class NotionSource extends MigrationSource {
316
347
  sections,
317
348
  sourcePath: `notion://${node.page.id}`,
318
349
  references: references.length > 0 ? references : undefined,
350
+ metadata: {
351
+ notionPageId: normalizeId(node.page.id),
352
+ notionTitle: node.title,
353
+ },
319
354
  };
320
355
  }
321
356
  catch (error) {
@@ -338,7 +373,9 @@ export class NotionSource extends MigrationSource {
338
373
  // Get page blocks
339
374
  const blocks = await this.client.getBlockChildren(entry.id);
340
375
  if (blocks.length > 0) {
341
- const contentSections = await blocksToSections(blocks, this.client, this.pagePathMap);
376
+ const contentSections = await blocksToSections(blocks, this.client, this.pagePathMap, {
377
+ databaseIdMap: this._databaseIdMap.size > 0 ? this._databaseIdMap : undefined,
378
+ });
342
379
  sections.push(...contentSections);
343
380
  }
344
381
  // Download media
@@ -361,6 +398,10 @@ export class NotionSource extends MigrationSource {
361
398
  ],
362
399
  sourcePath: `notion://${entry.id}`,
363
400
  references: references.length > 0 ? references : undefined,
401
+ metadata: {
402
+ notionPageId: normalizeId(entry.id),
403
+ notionTitle: title,
404
+ },
364
405
  };
365
406
  }
366
407
  catch (error) {
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Base interface for export targets
3
+ *
4
+ * Each target (Notion, Google Docs, etc.) implements this interface
5
+ * to provide a consistent way to export KB documents.
6
+ */
7
+ import type { DocumentDetail, DocumentListItem } from '../types.js';
8
+ /**
9
+ * Result of exporting a single document to an external target
10
+ */
11
+ export interface ExportTargetResult {
12
+ documentId: string;
13
+ documentPath: string;
14
+ status: 'created' | 'updated' | 'skipped' | 'failed';
15
+ externalId?: string;
16
+ error?: string;
17
+ duration: number;
18
+ }
19
+ /**
20
+ * Summary log for an export-to-target operation
21
+ */
22
+ export interface ExportTargetLog {
23
+ timestamp: string;
24
+ sourceApi: string;
25
+ target: {
26
+ type: string;
27
+ location: string;
28
+ };
29
+ basePath: string;
30
+ options: {
31
+ dryRun: boolean;
32
+ conflictStrategy: 'skip' | 'update';
33
+ };
34
+ results: ExportTargetResult[];
35
+ summary: {
36
+ total: number;
37
+ created: number;
38
+ updated: number;
39
+ skipped: number;
40
+ failed: number;
41
+ duration: number;
42
+ };
43
+ }
44
+ /**
45
+ * Configuration for export targets
46
+ */
47
+ export interface ExportTargetConfig {
48
+ [key: string]: unknown;
49
+ }
50
+ /**
51
+ * Abstract base class for export targets
52
+ */
53
+ export declare abstract class ExportTarget<TConfig extends ExportTargetConfig = ExportTargetConfig> {
54
+ protected config: TConfig;
55
+ constructor(config: TConfig);
56
+ /**
57
+ * Human-readable name of the target type
58
+ */
59
+ abstract get targetType(): string;
60
+ /**
61
+ * Human-readable location identifier for logging
62
+ */
63
+ abstract get targetLocation(): string;
64
+ /**
65
+ * Validate the target configuration and connectivity
66
+ * @throws Error if configuration is invalid or target is not accessible
67
+ */
68
+ abstract validate(): Promise<void>;
69
+ /**
70
+ * Export a single document to the target
71
+ */
72
+ abstract exportDocument(doc: DocumentDetail, listItem: DocumentListItem, dryRun: boolean): Promise<ExportTargetResult>;
73
+ /**
74
+ * Clean up resources after export completes
75
+ */
76
+ cleanup(): Promise<void>;
77
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Base interface for export targets
3
+ *
4
+ * Each target (Notion, Google Docs, etc.) implements this interface
5
+ * to provide a consistent way to export KB documents.
6
+ */
7
+ /**
8
+ * Abstract base class for export targets
9
+ */
10
+ export class ExportTarget {
11
+ config;
12
+ constructor(config) {
13
+ this.config = config;
14
+ }
15
+ /**
16
+ * Clean up resources after export completes
17
+ */
18
+ async cleanup() {
19
+ // Default no-op, override if needed
20
+ }
21
+ }
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Target registry and factory
3
+ */
4
+ export { ExportTarget, type ExportTargetConfig, type ExportTargetResult, type ExportTargetLog } from './base.js';
5
+ export { NotionExportTarget, type NotionExportConfig } from './notion.js';
6
+ export { exportDatabase, type DatabaseExportResult, type DatabaseExportContext } from './notion-database-export.js';
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Target registry and factory
3
+ */
4
+ export { ExportTarget } from './base.js';
5
+ export { NotionExportTarget } from './notion.js';
6
+ export { exportDatabase } from './notion-database-export.js';
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Notion Database Export
3
+ *
4
+ * Exports KB databases to Notion databases.
5
+ * Handles:
6
+ * - Creating new Notion databases from KB database schemas
7
+ * - Updating existing Notion databases (clear + recreate entries)
8
+ * - Column mapping (select/multi_select backed by tags -> Notion select/multi_select)
9
+ * - Color mapping (hex -> Notion color names)
10
+ * - Entry creation with correct property values
11
+ */
12
+ import { Client } from '@notionhq/client';
13
+ import type { MoxnClient } from '../client.js';
14
+ export interface DatabaseExportResult {
15
+ kbDatabaseId: string;
16
+ kbDatabaseName: string;
17
+ notionDatabaseId: string;
18
+ status: 'created' | 'updated' | 'skipped' | 'failed';
19
+ entriesCreated: number;
20
+ error?: string;
21
+ }
22
+ export interface DatabaseExportContext {
23
+ notionClient: Client;
24
+ moxnClient: MoxnClient;
25
+ conflictStrategy: 'skip' | 'update';
26
+ dryRun: boolean;
27
+ }
28
+ /**
29
+ * Export a single KB database to Notion.
30
+ *
31
+ * @param ctx Export context with Notion client, Moxn client, and options
32
+ * @param kbDatabaseId KB database ID to export
33
+ * @param parentNotionPageId Notion page ID to create the database under
34
+ * @returns Export result
35
+ */
36
+ export declare function exportDatabase(ctx: DatabaseExportContext, kbDatabaseId: string, parentNotionPageId: string): Promise<DatabaseExportResult>;
@@ -0,0 +1,255 @@
1
+ /**
2
+ * Notion Database Export
3
+ *
4
+ * Exports KB databases to Notion databases.
5
+ * Handles:
6
+ * - Creating new Notion databases from KB database schemas
7
+ * - Updating existing Notion databases (clear + recreate entries)
8
+ * - Column mapping (select/multi_select backed by tags -> Notion select/multi_select)
9
+ * - Color mapping (hex -> Notion color names)
10
+ * - Entry creation with correct property values
11
+ */
12
+ const RATE_LIMIT_MS = 350;
13
+ function sleep(ms) {
14
+ return new Promise((resolve) => setTimeout(resolve, ms));
15
+ }
16
+ // Reverse mapping: hex color -> Notion color name
17
+ const HEX_TO_NOTION_COLOR = {
18
+ '#6B7280': 'gray',
19
+ '#92400E': 'brown',
20
+ '#F97316': 'orange',
21
+ '#EAB308': 'yellow',
22
+ '#22C55E': 'green',
23
+ '#3B82F6': 'blue',
24
+ '#8B5CF6': 'purple',
25
+ '#EC4899': 'pink',
26
+ '#EF4444': 'red',
27
+ };
28
+ function hexToNotionColor(hex) {
29
+ if (!hex)
30
+ return 'default';
31
+ return HEX_TO_NOTION_COLOR[hex.toUpperCase()] ?? HEX_TO_NOTION_COLOR[hex] ?? 'default';
32
+ }
33
+ /**
34
+ * Export a single KB database to Notion.
35
+ *
36
+ * @param ctx Export context with Notion client, Moxn client, and options
37
+ * @param kbDatabaseId KB database ID to export
38
+ * @param parentNotionPageId Notion page ID to create the database under
39
+ * @returns Export result
40
+ */
41
+ export async function exportDatabase(ctx, kbDatabaseId, parentNotionPageId) {
42
+ try {
43
+ // 1. Fetch resolved database from KB API
44
+ const resolved = await ctx.moxnClient.resolveDatabase(kbDatabaseId);
45
+ // 2. Check for existing Notion database mapping
46
+ let existingMapping = null;
47
+ try {
48
+ const mappingResult = await ctx.moxnClient.getNotionDatabaseMapping(kbDatabaseId);
49
+ existingMapping = mappingResult.mapping;
50
+ }
51
+ catch {
52
+ // No mapping found -- that's fine
53
+ }
54
+ // 3. Handle conflict strategy
55
+ if (existingMapping) {
56
+ if (ctx.conflictStrategy === 'skip') {
57
+ return {
58
+ kbDatabaseId,
59
+ kbDatabaseName: resolved.databaseName,
60
+ notionDatabaseId: existingMapping.notionDatabaseId,
61
+ status: 'skipped',
62
+ entriesCreated: 0,
63
+ };
64
+ }
65
+ if (ctx.dryRun) {
66
+ return {
67
+ kbDatabaseId,
68
+ kbDatabaseName: resolved.databaseName,
69
+ notionDatabaseId: existingMapping.notionDatabaseId,
70
+ status: 'updated',
71
+ entriesCreated: resolved.documents.length,
72
+ };
73
+ }
74
+ // Update existing: archive all pages, then recreate entries
75
+ const entriesCreated = await updateExistingNotionDatabase(ctx, existingMapping.notionDatabaseId, resolved);
76
+ return {
77
+ kbDatabaseId,
78
+ kbDatabaseName: resolved.databaseName,
79
+ notionDatabaseId: existingMapping.notionDatabaseId,
80
+ status: 'updated',
81
+ entriesCreated,
82
+ };
83
+ }
84
+ // 4. No existing mapping -- create new Notion database
85
+ if (ctx.dryRun) {
86
+ return {
87
+ kbDatabaseId,
88
+ kbDatabaseName: resolved.databaseName,
89
+ notionDatabaseId: 'dry-run',
90
+ status: 'created',
91
+ entriesCreated: resolved.documents.length,
92
+ };
93
+ }
94
+ const { notionDatabaseId, dataSourceId } = await createNotionDatabase(ctx, parentNotionPageId, resolved);
95
+ // 5. Create entries
96
+ const entriesCreated = await createDatabaseEntries(ctx, dataSourceId, resolved);
97
+ // 6. Upsert mapping
98
+ await ctx.moxnClient.createNotionDatabaseMapping({
99
+ kbDatabaseId,
100
+ notionDatabaseId: notionDatabaseId.replace(/-/g, ''),
101
+ notionDatabaseTitle: resolved.databaseName,
102
+ });
103
+ return {
104
+ kbDatabaseId,
105
+ kbDatabaseName: resolved.databaseName,
106
+ notionDatabaseId: notionDatabaseId.replace(/-/g, ''),
107
+ status: 'created',
108
+ entriesCreated,
109
+ };
110
+ }
111
+ catch (error) {
112
+ return {
113
+ kbDatabaseId,
114
+ kbDatabaseName: 'Unknown',
115
+ notionDatabaseId: '',
116
+ status: 'failed',
117
+ entriesCreated: 0,
118
+ error: error instanceof Error ? error.message : String(error),
119
+ };
120
+ }
121
+ }
122
+ /**
123
+ * Create a new Notion database with the correct schema.
124
+ */
125
+ async function createNotionDatabase(ctx, parentPageId, resolved) {
126
+ // Build properties from KB columns
127
+ const properties = {};
128
+ for (const col of resolved.columns) {
129
+ const options = col.options.map((opt) => ({
130
+ name: opt.tagName,
131
+ color: hexToNotionColor(opt.tagColor),
132
+ }));
133
+ if (col.type === 'select') {
134
+ properties[col.name] = {
135
+ type: 'select',
136
+ select: { options },
137
+ };
138
+ }
139
+ else if (col.type === 'multi_select') {
140
+ properties[col.name] = {
141
+ type: 'multi_select',
142
+ multi_select: { options },
143
+ };
144
+ }
145
+ }
146
+ await sleep(RATE_LIMIT_MS);
147
+ // Use client.request() for v5 API compatibility
148
+ // Properties go under initial_data_source.properties (not top-level)
149
+ const response = (await ctx.notionClient.request({
150
+ method: 'post',
151
+ path: 'databases',
152
+ body: {
153
+ parent: { type: 'page_id', page_id: parentPageId },
154
+ title: [{ type: 'text', text: { content: resolved.databaseName } }],
155
+ initial_data_source: {
156
+ type: 'external',
157
+ external: {},
158
+ properties,
159
+ },
160
+ },
161
+ }));
162
+ const notionDatabaseId = response.id;
163
+ const dataSourceId = response.data_sources?.[0]?.id ?? notionDatabaseId;
164
+ return { notionDatabaseId, dataSourceId };
165
+ }
166
+ /**
167
+ * Update an existing Notion database by archiving all pages and recreating entries.
168
+ */
169
+ async function updateExistingNotionDatabase(ctx, notionDatabaseId, resolved) {
170
+ // Archive existing pages (Notion v5: query database, archive each page)
171
+ await sleep(RATE_LIMIT_MS);
172
+ let hasMore = true;
173
+ let startCursor;
174
+ while (hasMore) {
175
+ const queryResult = await ctx.notionClient.databases.query({
176
+ database_id: notionDatabaseId,
177
+ start_cursor: startCursor,
178
+ page_size: 100,
179
+ });
180
+ for (const page of queryResult.results) {
181
+ await sleep(RATE_LIMIT_MS);
182
+ try {
183
+ await ctx.notionClient.pages.update({
184
+ page_id: page.id,
185
+ archived: true,
186
+ });
187
+ }
188
+ catch {
189
+ // Continue if individual archive fails
190
+ }
191
+ }
192
+ hasMore = queryResult.has_more;
193
+ startCursor = queryResult.next_cursor ?? undefined;
194
+ }
195
+ // Now get the data_source_id for entry creation
196
+ // Query the database to get its data_sources
197
+ await sleep(RATE_LIMIT_MS);
198
+ const dbInfo = (await ctx.notionClient.request({
199
+ method: 'get',
200
+ path: `databases/${notionDatabaseId}`,
201
+ }));
202
+ const dataSourceId = dbInfo.data_sources?.[0]?.id ?? notionDatabaseId;
203
+ // Create new entries
204
+ return createDatabaseEntries(ctx, dataSourceId, resolved);
205
+ }
206
+ /**
207
+ * Create database entries as Notion pages.
208
+ */
209
+ async function createDatabaseEntries(ctx, dataSourceId, resolved) {
210
+ let created = 0;
211
+ for (const doc of resolved.documents) {
212
+ try {
213
+ // Build property values for this entry
214
+ const properties = {
215
+ // Title property (always "Name" for databases)
216
+ Name: {
217
+ type: 'title',
218
+ title: [{ type: 'text', text: { content: doc.documentName ?? 'Untitled' } }],
219
+ },
220
+ };
221
+ // Add column values
222
+ for (const [colName, propValue] of Object.entries(doc.properties)) {
223
+ if (propValue.values.length === 0)
224
+ continue;
225
+ if (propValue.columnType === 'select') {
226
+ properties[colName] = {
227
+ type: 'select',
228
+ select: { name: propValue.values[0].tagName },
229
+ };
230
+ }
231
+ else if (propValue.columnType === 'multi_select') {
232
+ properties[colName] = {
233
+ type: 'multi_select',
234
+ multi_select: propValue.values.map((v) => ({ name: v.tagName })),
235
+ };
236
+ }
237
+ }
238
+ await sleep(RATE_LIMIT_MS);
239
+ // Create entry as page with data_source_id parent
240
+ await ctx.notionClient.request({
241
+ method: 'post',
242
+ path: 'pages',
243
+ body: {
244
+ parent: { type: 'data_source_id', data_source_id: dataSourceId },
245
+ properties,
246
+ },
247
+ });
248
+ created++;
249
+ }
250
+ catch (error) {
251
+ console.error(` Warning: Failed to create entry "${doc.documentName}": ${error instanceof Error ? error.message : error}`);
252
+ }
253
+ }
254
+ return created;
255
+ }
@@ -0,0 +1,93 @@
1
+ /**
2
+ * NotionExportTarget — exports KB documents to Notion pages.
3
+ *
4
+ * Converts KB section content (markdown) to Notion blocks using @tryfabric/martian,
5
+ * then creates or updates Notion pages via the Notion API.
6
+ *
7
+ * Supports a two-pass export algorithm:
8
+ * - Pass 1: Create all pages (without cross-references)
9
+ * - Pass 2: Append reference blocks using the KB doc ID -> Notion page ID mapping
10
+ *
11
+ * Conflict detection uses the MoxnClient's notion-mapping API endpoints
12
+ * (when available) to find existing Notion pages for a KB document.
13
+ */
14
+ import { ExportTarget, type ExportTargetConfig, type ExportTargetResult } from './base.js';
15
+ import type { DocumentDetail, DocumentListItem } from '../types.js';
16
+ export interface NotionExportConfig extends ExportTargetConfig {
17
+ /** Notion integration token */
18
+ notionToken: string;
19
+ /** Parent page ID under which to create pages */
20
+ parentPageId: string;
21
+ /** How to handle pages that already exist in Notion */
22
+ conflictStrategy: 'skip' | 'update';
23
+ /** Moxn API URL (for notion-mapping lookups) */
24
+ apiUrl: string;
25
+ /** Moxn API key */
26
+ apiKey: string;
27
+ }
28
+ /** A cross-reference extracted from section content */
29
+ export interface ExtractedKBReference {
30
+ /** The KB document ID of the source document */
31
+ sourceDocumentId: string;
32
+ /** Target KB path found in markdown links */
33
+ targetKbPath: string;
34
+ /** Display text for the reference */
35
+ displayText: string;
36
+ }
37
+ export declare class NotionExportTarget extends ExportTarget<NotionExportConfig> {
38
+ private client;
39
+ /** Cache: kbDocumentId -> notionPageId (from mapping API) */
40
+ private mappingCache;
41
+ /** Cache: notionPageId -> true (pages we've verified exist) */
42
+ private verifiedPages;
43
+ /** Map from KB path prefix -> Notion page ID (for nested page creation) */
44
+ private pathToNotionId;
45
+ /** KB path -> KB document ID mapping (built during two-pass export) */
46
+ private kbPathToDocId;
47
+ /** KB doc ID -> Notion page ID mapping (built during pass 1) */
48
+ private docIdToNotionPageId;
49
+ constructor(config: NotionExportConfig);
50
+ get targetType(): string;
51
+ get targetLocation(): string;
52
+ validate(): Promise<void>;
53
+ /**
54
+ * Register a KB document in the path -> ID index.
55
+ * Called before pass 1 so references can be resolved in pass 2.
56
+ */
57
+ registerDocument(doc: DocumentDetail): void;
58
+ /**
59
+ * After pass 1, record a successful export mapping.
60
+ * Called by the runner when a doc is created/updated successfully.
61
+ */
62
+ registerExportedPage(kbDocumentId: string, notionPageId: string): void;
63
+ /**
64
+ * Pass 2: Resolve references for a document and append link blocks to its Notion page.
65
+ *
66
+ * Finds KB path links in the document content, resolves them to Notion page IDs
67
+ * (from the current export batch or pre-existing mappings), and appends
68
+ * mention/link blocks to the Notion page.
69
+ *
70
+ * Returns the number of references resolved.
71
+ */
72
+ resolveAndAppendReferences(doc: DocumentDetail, notionPageId: string): Promise<{
73
+ resolved: number;
74
+ unresolved: number;
75
+ }>;
76
+ /**
77
+ * Resolve a KB path to a Notion page ID.
78
+ *
79
+ * Checks three sources in order:
80
+ * 1. Current export batch (docIdToNotionPageId from pass 1)
81
+ * 2. Local mapping cache
82
+ * 3. Remote notion-mapping API
83
+ */
84
+ private resolveKBPathToNotionPage;
85
+ exportDocument(doc: DocumentDetail, listItem: DocumentListItem, dryRun: boolean): Promise<ExportTargetResult>;
86
+ cleanup(): Promise<void>;
87
+ private createNotionPage;
88
+ private updateNotionPage;
89
+ private clearPageContent;
90
+ private appendRemainingBlocks;
91
+ private findExistingNotionPage;
92
+ private recordMapping;
93
+ }