@im-nobsidian/core 0.1.11 → 0.2.0

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.
package/dist/index.d.ts CHANGED
@@ -129,19 +129,49 @@ declare const ConfigSchema: z.ZodObject<{
129
129
  }>;
130
130
  advanced: z.ZodObject<{
131
131
  concurrency: z.ZodDefault<z.ZodNumber>;
132
- maxRetries: z.ZodDefault<z.ZodNumber>;
133
132
  timeoutMs: z.ZodDefault<z.ZodNumber>;
133
+ pageSize: z.ZodDefault<z.ZodNumber>;
134
134
  batchSize: z.ZodDefault<z.ZodNumber>;
135
+ rateLimitIntervalMs: z.ZodDefault<z.ZodNumber>;
136
+ maxRetries: z.ZodDefault<z.ZodNumber>;
137
+ retryBaseDelayMs: z.ZodDefault<z.ZodNumber>;
138
+ retryBackoffFactor: z.ZodDefault<z.ZodNumber>;
139
+ retryWaitMs: z.ZodDefault<z.ZodNumber>;
140
+ mediaConcurrency: z.ZodDefault<z.ZodNumber>;
141
+ mediaMaxRetries: z.ZodDefault<z.ZodNumber>;
142
+ mediaRetryBaseMs: z.ZodDefault<z.ZodNumber>;
143
+ fileConcurrency: z.ZodDefault<z.ZodNumber>;
144
+ maxFileSizeBytes: z.ZodDefault<z.ZodNumber>;
135
145
  }, "strip", z.ZodTypeAny, {
136
146
  concurrency: number;
137
- maxRetries: number;
138
147
  timeoutMs: number;
148
+ pageSize: number;
139
149
  batchSize: number;
150
+ rateLimitIntervalMs: number;
151
+ maxRetries: number;
152
+ retryBaseDelayMs: number;
153
+ retryBackoffFactor: number;
154
+ retryWaitMs: number;
155
+ mediaConcurrency: number;
156
+ mediaMaxRetries: number;
157
+ mediaRetryBaseMs: number;
158
+ fileConcurrency: number;
159
+ maxFileSizeBytes: number;
140
160
  }, {
141
161
  concurrency?: number | undefined;
142
- maxRetries?: number | undefined;
143
162
  timeoutMs?: number | undefined;
163
+ pageSize?: number | undefined;
144
164
  batchSize?: number | undefined;
165
+ rateLimitIntervalMs?: number | undefined;
166
+ maxRetries?: number | undefined;
167
+ retryBaseDelayMs?: number | undefined;
168
+ retryBackoffFactor?: number | undefined;
169
+ retryWaitMs?: number | undefined;
170
+ mediaConcurrency?: number | undefined;
171
+ mediaMaxRetries?: number | undefined;
172
+ mediaRetryBaseMs?: number | undefined;
173
+ fileConcurrency?: number | undefined;
174
+ maxFileSizeBytes?: number | undefined;
145
175
  }>;
146
176
  }, "strip", z.ZodTypeAny, {
147
177
  version: 1;
@@ -180,9 +210,19 @@ declare const ConfigSchema: z.ZodObject<{
180
210
  };
181
211
  advanced: {
182
212
  concurrency: number;
183
- maxRetries: number;
184
213
  timeoutMs: number;
214
+ pageSize: number;
185
215
  batchSize: number;
216
+ rateLimitIntervalMs: number;
217
+ maxRetries: number;
218
+ retryBaseDelayMs: number;
219
+ retryBackoffFactor: number;
220
+ retryWaitMs: number;
221
+ mediaConcurrency: number;
222
+ mediaMaxRetries: number;
223
+ mediaRetryBaseMs: number;
224
+ fileConcurrency: number;
225
+ maxFileSizeBytes: number;
186
226
  };
187
227
  }, {
188
228
  version: 1;
@@ -221,9 +261,19 @@ declare const ConfigSchema: z.ZodObject<{
221
261
  };
222
262
  advanced: {
223
263
  concurrency?: number | undefined;
224
- maxRetries?: number | undefined;
225
264
  timeoutMs?: number | undefined;
265
+ pageSize?: number | undefined;
226
266
  batchSize?: number | undefined;
267
+ rateLimitIntervalMs?: number | undefined;
268
+ maxRetries?: number | undefined;
269
+ retryBaseDelayMs?: number | undefined;
270
+ retryBackoffFactor?: number | undefined;
271
+ retryWaitMs?: number | undefined;
272
+ mediaConcurrency?: number | undefined;
273
+ mediaMaxRetries?: number | undefined;
274
+ mediaRetryBaseMs?: number | undefined;
275
+ fileConcurrency?: number | undefined;
276
+ maxFileSizeBytes?: number | undefined;
227
277
  };
228
278
  }>;
229
279
  type Config = z.infer<typeof ConfigSchema>;
@@ -279,28 +329,19 @@ interface ProgressItem {
279
329
  readonly operation: "create" | "update" | "delete";
280
330
  }
281
331
  type ProgressCallback = (current: number, total: number, item: ProgressItem) => void;
282
- interface PushOptions {
283
- readonly paths?: string[];
284
- readonly excludePaths?: string[];
285
- readonly force?: boolean;
286
- readonly dryRun?: boolean;
287
- readonly onProgress?: ProgressCallback;
288
- readonly signal?: AbortSignal;
289
- }
290
- interface PullOptions {
332
+ /** push/pull/sync 가 공통으로 받는 동기화 옵션. */
333
+ interface BaseSyncOptions {
291
334
  readonly paths?: string[];
292
335
  readonly force?: boolean;
293
336
  readonly dryRun?: boolean;
294
337
  readonly onProgress?: ProgressCallback;
295
338
  readonly signal?: AbortSignal;
296
339
  }
297
- interface SyncOptions {
298
- readonly paths?: string[];
299
- readonly force?: boolean;
300
- readonly dryRun?: boolean;
301
- readonly onProgress?: ProgressCallback;
302
- readonly signal?: AbortSignal;
340
+ interface PushOptions extends BaseSyncOptions {
341
+ readonly excludePaths?: string[];
303
342
  }
343
+ type PullOptions = BaseSyncOptions;
344
+ type SyncOptions = BaseSyncOptions;
304
345
  interface PushResult {
305
346
  readonly created: number;
306
347
  readonly updated: number;
@@ -478,10 +519,14 @@ interface IStateDB {
478
519
  setNotionLastEdited(id: string, lastEdited: string): void;
479
520
  updateStatCache(id: string, mtime: string, fileSize: number): void;
480
521
  setNotionParentId(id: string, parentId: string): void;
522
+ /** 레코드의 로컬 경로를 갱신한다 (파일 rename/move 추적용). */
523
+ updatePath(id: string, newPath: string): void;
481
524
  delete(id: string): void;
482
525
  resolveWikilink(text: string): WikilinkEntry | null;
483
526
  resolvePageId(pageId: string): WikilinkEntry | null;
484
527
  upsertWikilink(entry: WikilinkEntry): void;
528
+ /** 레코드 삭제 시 해당 경로의 wikilink 항목을 제거한다 (stale 링크 방지). */
529
+ deleteWikilink(obsidianPath: string): void;
485
530
  getMeta(key: string): string | null;
486
531
  setMeta(key: string, value: string): void;
487
532
  isFileRegistered(localPath: string): boolean;
@@ -489,6 +534,12 @@ interface IStateDB {
489
534
  getFilesByPageId(notionPageId: string): FileRegistryEntry[];
490
535
  registerFile(entry: RegisterFileInput): void;
491
536
  deleteFileRegistry(localPath: string): void;
537
+ recordPendingOperation(input: RecordPendingInput): string;
538
+ getIncompletePendingOperations(): PendingOperation[];
539
+ getIncompleteOpByState(syncStateId: string, operation: PendingOperation["operation"]): PendingOperation | null;
540
+ markPendingCompleted(id: string): void;
541
+ markPendingFailed(id: string, errorMessage: string): void;
542
+ clearCompletedOperations(): void;
492
543
  storePreserveMarkers(path: string, markers: PreserveMarker[]): void;
493
544
  getPreserveMarkers(path: string): PreserveMarker[];
494
545
  transaction<T>(fn: () => T): T;
@@ -511,10 +562,12 @@ declare class StateDB implements IStateDB {
511
562
  setNotionLastEdited(id: string, lastEdited: string): void;
512
563
  updateStatCache(id: string, mtime: string, fileSize: number): void;
513
564
  setNotionParentId(id: string, parentId: string): void;
565
+ updatePath(id: string, newPath: string): void;
514
566
  delete(id: string): void;
515
567
  resolveWikilink(text: string): WikilinkEntry | null;
516
568
  resolvePageId(pageId: string): WikilinkEntry | null;
517
569
  upsertWikilink(entry: WikilinkEntry): void;
570
+ deleteWikilink(obsidianPath: string): void;
518
571
  getMeta(key: string): string | null;
519
572
  setMeta(key: string, value: string): void;
520
573
  isFileRegistered(localPath: string): boolean;
@@ -523,6 +576,13 @@ declare class StateDB implements IStateDB {
523
576
  registerFile(entry: RegisterFileInput): void;
524
577
  deleteFileRegistry(localPath: string): void;
525
578
  private mapFileRegistryRow;
579
+ recordPendingOperation(input: RecordPendingInput): string;
580
+ getIncompletePendingOperations(): PendingOperation[];
581
+ getIncompleteOpByState(syncStateId: string, operation: PendingOperation["operation"]): PendingOperation | null;
582
+ markPendingCompleted(id: string): void;
583
+ markPendingFailed(id: string, errorMessage: string): void;
584
+ clearCompletedOperations(): void;
585
+ private mapPendingRow;
526
586
  storePreserveMarkers(path: string, markers: PreserveMarker[]): void;
527
587
  getPreserveMarkers(path: string): PreserveMarker[];
528
588
  transaction<T>(fn: () => T): T;
@@ -560,20 +620,71 @@ interface RegisterFileInput {
560
620
  readonly fileHash: string;
561
621
  readonly fileSize: number;
562
622
  }
623
+ /** I12 크래시 복구 WAL 항목. push/pull 적용 전 기록되고, 적용 완료 시 completed 로 마감된다. */
624
+ interface PendingOperation {
625
+ readonly id: string;
626
+ readonly syncStateId: string;
627
+ readonly operation: "create" | "update" | "delete" | "move";
628
+ readonly direction: "push" | "pull";
629
+ readonly payload: string | null;
630
+ readonly retryCount: number;
631
+ readonly errorMessage: string | null;
632
+ readonly status: "pending" | "processing" | "completed" | "failed";
633
+ readonly createdAt: string;
634
+ readonly completedAt: string | null;
635
+ }
636
+ interface RecordPendingInput {
637
+ readonly syncStateId: string;
638
+ readonly operation: PendingOperation["operation"];
639
+ readonly direction: PendingOperation["direction"];
640
+ readonly payload?: string | null;
641
+ }
642
+
643
+ type NotionPropertyValue = Record<string, unknown>;
644
+ interface WikilinkResolver$1 {
645
+ resolve(title: string): string | null;
646
+ resolvePageId(pageId: string): string | null;
647
+ }
648
+ declare class PropertyMapper {
649
+ private schema;
650
+ private wikilinkResolver;
651
+ setWikilinkResolver(resolver: WikilinkResolver$1): void;
652
+ loadSchema(properties: Record<string, {
653
+ id: string;
654
+ type: string;
655
+ }>): void;
656
+ toNotionProperties(frontmatter: Record<string, unknown>, title: string): Record<string, NotionPropertyValue>;
657
+ fromNotionProperties(notionProps: Record<string, unknown>): Record<string, unknown>;
658
+ private convertBySchema;
659
+ private inferAndConvert;
660
+ private extractValue;
661
+ }
563
662
 
564
663
  interface NotionClientOptions {
565
664
  readonly token: string;
566
665
  readonly concurrency?: number;
567
666
  readonly timeoutMs?: number;
667
+ readonly rateLimitIntervalMs?: number;
668
+ readonly maxRetries?: number;
669
+ readonly retryBaseDelayMs?: number;
670
+ readonly retryBackoffFactor?: number;
671
+ readonly pageSize?: number;
672
+ readonly batchSize?: number;
568
673
  readonly fetch?: typeof globalThis.fetch;
569
674
  }
570
675
  declare class NotionClient {
571
676
  private readonly client;
572
677
  private readonly sema;
573
- private readonly token;
574
678
  private readonly propertyMapper;
575
- private readonly customFetch?;
679
+ private readonly minRequestInterval;
680
+ private readonly maxRetries;
681
+ private readonly retryBaseDelayMs;
682
+ private readonly retryBackoffFactor;
683
+ private readonly defaultPageSize;
684
+ private readonly batchSize;
576
685
  constructor(options: NotionClientOptions);
686
+ /** config.advanced 의 운영 튜닝값으로 클라이언트를 생성한다 (매직넘버 단일 진실원). */
687
+ static fromConfig(config: Config, fetch?: typeof globalThis.fetch): NotionClient;
577
688
  getInternalClient(): Client;
578
689
  getPage(pageId: string): Promise<PageObjectResponse>;
579
690
  createPage(params: {
@@ -599,8 +710,29 @@ declare class NotionClient {
599
710
  replaceAll?: boolean;
600
711
  }>): Promise<PageMarkdownResponse>;
601
712
  movePage(pageId: string, newParentId: string, newParentType: "page" | "database"): Promise<PageObjectResponse>;
602
- private fetchDatabaseLegacy;
713
+ /**
714
+ * 데이터베이스 1개를 신 모델(2025-09-03 data source 분리)로 조회한다.
715
+ * - title: database 객체에 그대로 존재한다.
716
+ * - properties(스키마): 신 모델에선 data source 에 있으므로 1차 data source 를 조회해 채운다.
717
+ * data source 접근 불가(링크드 DB 등)면 database 객체의 properties 로 폴백한다(제목은 보존).
718
+ *
719
+ * 폐기한 레거시 raw fetch(`GET /v1/databases/{id}`, Notion-Version 2022-06-28)는 신 모델로
720
+ * 업그레이드된 다수 DB에 400 을 반환해 자동 발견 DB 가 통째 드롭(내용 손실)되던 원인이었다.
721
+ * SDK(`databases.retrieve`/`dataSources.retrieve`)는 동일 토큰으로 정상 동작한다.
722
+ */
723
+ private fetchDatabaseModern;
603
724
  getDatabaseTitle(databaseId: string): Promise<string>;
725
+ /**
726
+ * 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지) 1회 호출로 판별한다.
727
+ * 신 모델(2025-09-03)에서 링크드 DB·미공유 데이터 소스·삭제 DB 는 `data_sources` 가
728
+ * 비어, 행 조회(`dataSources.query`)가 404 로 실패하고 그 전에 생성된 빈 폴더/`.base` 만
729
+ * 남긴다. 발견 단계에서 미리 걸러 **빈 폴더/.base 오염 + 매 pull 의 404 노이즈**를 차단한다.
730
+ * 제목은 함께 반환해 호출처가 추가 조회 없이 폴더명을 잡게 한다.
731
+ */
732
+ getDatabaseSyncability(databaseId: string): Promise<{
733
+ title: string;
734
+ queryable: boolean;
735
+ }>;
604
736
  getDatabaseSchema(databaseId: string): Promise<Record<string, {
605
737
  id: string;
606
738
  type: string;
@@ -618,6 +750,12 @@ declare class NotionClient {
618
750
  optionIds?: string[];
619
751
  }>;
620
752
  }>>;
753
+ /** 특정 data source 1개를 직접 쿼리한다(저수준). 다중 data source 순회의 빌딩블록. */
754
+ private queryDataSource;
755
+ /**
756
+ * database 의 1차 data source 1페이지를 쿼리한다(증분 detect 등 단일 소스 경로용).
757
+ * 전 행을 빠짐없이 받으려면 {@link queryAllDatabasePages} 를 쓴다(전 data source 순회).
758
+ */
621
759
  queryDatabase(databaseId: string, options?: {
622
760
  startCursor?: string;
623
761
  pageSize?: number;
@@ -626,8 +764,24 @@ declare class NotionClient {
626
764
  results: PageObjectResponse[];
627
765
  nextCursor: string | null;
628
766
  }>;
767
+ /**
768
+ * database 의 **모든 data source** 행을 빠짐없이 조회한다(2025-09-03 신 모델).
769
+ * 한 database 가 2개 이상의 data source(각자 행 집합)를 가질 수 있어, 1차 data source 만
770
+ * 페이지네이션하면 2번째+ 의 행이 통째 침묵 유실된다. 전 data source 를 순회·페이지네이션
771
+ * 하고 page_id 로 디듀프(소스 간 동일 페이지 방어)한 뒤 합친다. 다중 소스면 1회 경고해
772
+ * "소스별 탭 구분이 한 폴더로 병합"되는 점을 비침묵으로 알린다.
773
+ */
629
774
  queryAllDatabasePages(databaseId: string, filter?: unknown): Promise<PageObjectResponse[]>;
775
+ /** database 의 1차 data source id(없으면 databaseId 폴백). 단일 소스 경로 호환용. */
630
776
  getDataSourceId(databaseId: string): Promise<string>;
777
+ /**
778
+ * database 의 전 data source 메타(id + 이름)를 반환한다. data source 가 없으면(레거시/단일)
779
+ * databaseId 자체를 단일 소스로 폴백해 호출처가 항상 ≥1 개를 받게 한다.
780
+ */
781
+ getDataSourceMetas(databaseId: string): Promise<Array<{
782
+ id: string;
783
+ name: string;
784
+ }>>;
631
785
  archivePage(pageId: string): Promise<void>;
632
786
  getFileBlockUrl(blockId: string): Promise<string | null>;
633
787
  getBlock(blockId: string): Promise<BlockObjectResponse>;
@@ -661,17 +815,53 @@ declare class NotionClient {
661
815
  last_edited_time: string;
662
816
  }>>;
663
817
  getChildPages(parentId: string): Promise<PageObjectResponse[]>;
818
+ /** 페이지가 휴지통(in_trash)이거나 보관(archived) 상태인지 판정한다. */
819
+ private static isTrashedOrArchived;
664
820
  getChildDatabaseIds(parentId: string): Promise<string[]>;
665
- getChildPagesRecursive(parentId: string): Promise<PageObjectResponse[]>;
821
+ /**
822
+ * root 서브트리를 직접 BFS 순회해 하위 페이지를 모두 수집한다(중첩 깊이 무관·무손실).
823
+ * 비용은 서브트리 전체 블록 수에 비례한다.
824
+ *
825
+ * @param opts.deadlineMs `Date.now()` 기준 마감 시각. 각 페이지 처리 전 초과를 검사해
826
+ * 초과 시 {@link DiscoveryTooLargeError}를 던진다(대규모 서브트리 → search 폴백 유도).
827
+ * 미지정 시 무제한(기존 동작 유지 — 테스트 mock 경로는 영향 없음).
828
+ */
829
+ getChildPagesRecursive(parentId: string, opts?: {
830
+ deadlineMs?: number;
831
+ }): Promise<PageObjectResponse[]>;
832
+ /** 워크스페이스에서 통합에 공유된 모든 페이지를 bulk search 로 열거한다(100/요청). */
833
+ searchAllPages(): Promise<PageObjectResponse[]>;
834
+ /**
835
+ * root 하위 페이지를 **search 기반**으로 디스커버리한다(대규모 서브트리용 폴백).
836
+ *
837
+ * 전략: 워크스페이스 전체 페이지를 1회 bulk search 로 열거(비용 ∝ 워크스페이스 페이지 수,
838
+ * 예측가능·유한)한 뒤, 각 페이지의 부모 체인을 따라 root 도달 여부를 판정한다. page_id
839
+ * 부모는 메모리 맵으로 추가 호출 없이 즉시 해석하고, block_id 부모(컬럼·토글 등 레이아웃에
840
+ * 중첩된 페이지)만 getBlock 으로 소유 페이지를 해석(memo)한다. search 는 중첩 깊이와 무관하게
841
+ * 모든 페이지를 반환하므로 직접 순회와 동일하게 **무손실**이다.
842
+ *
843
+ * getChildPagesRecursive 와 동일 집합을 보장하기 위해:
844
+ * - DB 행(parent=data_source_id/database_id)은 child_page 로 도달 불가 → 제외
845
+ * - 휴지통/보관(in_trash/archived) 페이지 제외
846
+ * - root 자신은 제외(직접 순회도 root 의 *자식*부터 수집)
847
+ */
848
+ getPagesUnderRootViaSearch(rootId: string): Promise<PageObjectResponse[]>;
666
849
  listDatabaseViews(databaseId: string): Promise<ViewConfig[]>;
667
850
  getView(viewId: string): Promise<ViewConfig | null>;
668
851
  getDatabaseViewsConfig(databaseId: string): Promise<DatabaseViewsConfig>;
669
852
  extractCover(page: PageObjectResponse): PageCover | null;
670
853
  extractIcon(page: PageObjectResponse): PageIcon | null;
671
854
  extractTitle(page: PageObjectResponse): string;
855
+ /**
856
+ * 페이지 모드 pull(extractProperties)의 relation/people 속성을 raw Notion UUID 가
857
+ * 아니라 [[제목]] 위키링크로 해소하도록 resolver 를 주입한다. 주입하지 않으면 매
858
+ * pull 마다 relation 이 raw UUID 로 재생성돼, 후처리(resolveNotionLinks)에만 의존하는
859
+ * 2-write churn 이 남는다(M1). DB 모드는 orchestrator/DatabaseSyncer 의 자체 mapper 가
860
+ * 이미 resolver 를 주입받아 면역이다.
861
+ */
862
+ setWikilinkResolver(resolver: WikilinkResolver$1): void;
672
863
  extractProperties(page: PageObjectResponse): Record<string, unknown>;
673
864
  private lastRequestTime;
674
- private readonly minRequestInterval;
675
865
  private withRateLimit;
676
866
  private executeWithRetry;
677
867
  }
@@ -780,26 +970,6 @@ declare class NotionBlockBuilder {
780
970
  static table(rows: string[][], hasColumnHeader?: boolean, hasRowHeader?: boolean): NotionBlock;
781
971
  }
782
972
 
783
- type NotionPropertyValue = Record<string, unknown>;
784
- interface WikilinkResolver$1 {
785
- resolve(title: string): string | null;
786
- resolvePageId(pageId: string): string | null;
787
- }
788
- declare class PropertyMapper {
789
- private schema;
790
- private wikilinkResolver;
791
- setWikilinkResolver(resolver: WikilinkResolver$1): void;
792
- loadSchema(properties: Record<string, {
793
- id: string;
794
- type: string;
795
- }>): void;
796
- toNotionProperties(frontmatter: Record<string, unknown>, title: string): Record<string, NotionPropertyValue>;
797
- fromNotionProperties(notionProps: Record<string, unknown>): Record<string, unknown>;
798
- private convertBySchema;
799
- private inferAndConvert;
800
- private extractValue;
801
- }
802
-
803
973
  interface FileInfo {
804
974
  readonly path: string;
805
975
  readonly content: string;
@@ -838,6 +1008,27 @@ interface VaultFS {
838
1008
  getFileStat(path: string): Promise<FileStatInfo | null>;
839
1009
  }
840
1010
 
1011
+ type ResolutionChoice = "local" | "remote" | "merge" | "duplicate";
1012
+ interface ResolutionResult {
1013
+ readonly path: string;
1014
+ readonly choice: ResolutionChoice;
1015
+ readonly success: boolean;
1016
+ readonly mergeHadConflicts?: boolean;
1017
+ }
1018
+ declare class ConflictResolver {
1019
+ private readonly stateDb;
1020
+ private readonly vaultFs;
1021
+ constructor(stateDb: IStateDB, vaultFs: VaultFS);
1022
+ resolve(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
1023
+ resolveByStrategy(conflict: Conflict, strategy: ConflictStrategy): Promise<ResolutionResult>;
1024
+ resolveAll(conflicts: Conflict[], strategy: ConflictStrategy): Promise<ResolutionResult[]>;
1025
+ private resolveLocal;
1026
+ private resolveRemote;
1027
+ private resolveMerge;
1028
+ private resolveDuplicate;
1029
+ generateDiff(conflict: Conflict): string;
1030
+ }
1031
+
841
1032
  declare class SyncOrchestrator {
842
1033
  private readonly config;
843
1034
  private readonly stateDb;
@@ -850,9 +1041,13 @@ declare class SyncOrchestrator {
850
1041
  private readonly fileHandler;
851
1042
  private readonly databaseSyncer;
852
1043
  private readonly propertyMapper;
1044
+ private readonly conflictResolver;
853
1045
  private dbSchemaLoaded;
854
1046
  private _pullImageCount;
855
1047
  private _pullFileCount;
1048
+ private _inlineDbRefs;
1049
+ private _childParentIds;
1050
+ private static readonly DISCOVERY_RECURSIVE_BUDGET_MS;
856
1051
  constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, customFetch?: typeof globalThis.fetch);
857
1052
  push(options?: PushOptions): Promise<PushResult>;
858
1053
  pull(options?: PullOptions): Promise<PullResult>;
@@ -868,17 +1063,65 @@ declare class SyncOrchestrator {
868
1063
  private buildConflictsFromRecords;
869
1064
  private get isDatabaseMode();
870
1065
  private discoverChildDatabases;
1066
+ /**
1067
+ * 자동 발견된 child_database 1개를 DatabaseSyncer 설정으로 변환한다.
1068
+ * 블록 스캔/markdown 양쪽 발견 경로에서 공통으로 사용한다.
1069
+ * - localFolder: 부모 페이지의 obsidianPath 기준으로 폴더를 잡아 원본 중첩 구조를 보존
1070
+ * (예: 부모 "AI Engineer (1).md" + DB "Minirecord Project" → "AI Engineer (1)/Minirecord-Project")
1071
+ * - 제목 취득 실패 시 부모 폴더명 기반 안전 이름으로 폴백
1072
+ */
1073
+ private buildDiscoveredDbConfig;
1074
+ /** 접근 불가 DB denylist 를 상태 메타에서 로드한다(하이픈 정규화). */
1075
+ private loadInaccessibleDbIds;
871
1076
  private pullDiscoveredDatabases;
872
1077
  private resolveNotionLinks;
873
1078
  private ensureDbSchema;
874
1079
  private pushCreate;
875
1080
  private pushUpdate;
1081
+ /** 단일 충돌을 사용자가 고른 선택지(local/remote/merge/duplicate)로 해소 + Notion 전파. */
1082
+ resolveConflict(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
1083
+ /** 단일 충돌을 전략(manual/local-first/remote-first/duplicate)으로 해소 + Notion 전파. */
1084
+ resolveConflictByStrategy(conflict: Conflict, strategy: ConflictStrategy): Promise<ResolutionResult>;
1085
+ /** 여러 충돌을 동일 전략으로 일괄 해소 + Notion 전파. */
1086
+ resolveAllConflicts(conflicts: Conflict[], strategy: ConflictStrategy): Promise<ResolutionResult[]>;
1087
+ /** 충돌 미리보기용 통합 diff(원본 vs 로컬 vs 원격). 해소 없이 표시 전용. */
1088
+ generateConflictDiff(conflict: Conflict): string;
1089
+ /**
1090
+ * 해소 결과를 Notion 으로 전파해 로컬↔원격 일관성을 봉합한다.
1091
+ * - remote 선택: 로컬이 원격으로 갱신됐을 뿐이므로 push 불필요. notionLastEdited 만
1092
+ * 원격 변경의 lastEdited 로 재조정 → 다음 pull 이 같은 변경을 재충돌로 보지 않음.
1093
+ * - merge 실패(충돌 마커 잔존): 사용자가 직접 풀어야 하므로 conflict 상태 유지·push 안 함.
1094
+ * - local / merge(성공) / duplicate: 해소된 로컬 내용을 Notion 에 재push(pushUpdate 가
1095
+ * 변환·이미지·속성·해시·notionLastEdited 를 한 트랜잭션으로 재조정) → 무손실 수렴.
1096
+ */
1097
+ private propagateResolution;
876
1098
  private pushDelete;
877
1099
  private pushMove;
878
1100
  private detectRemoteChanges;
1101
+ /**
1102
+ * 증분 원격 변경 감지 — `since` 이후 수정된 페이지만 search 로 받아 created/modified 만
1103
+ * 만든다. **삭제는 의도적으로 감지하지 않는다**: search API 는 in_trash/archived 페이지를
1104
+ * 반환하지 않아(=사라진 것을 증분만으로는 구분 불가) 삭제 판정에는 전체 enumeration 이
1105
+ * 필수다. 따라서 호출부는 `deleteSync` 가 켜진 경우 이 fast-path 를 쓰지 않고
1106
+ * `detectRemoteChanges`(전체 스캔, 삭제 diff 포함)로 우회한다. (I10)
1107
+ */
879
1108
  private detectRemoteChangesIncremental;
880
1109
  private isTrackedParent;
881
1110
  private pullCreate;
1111
+ /**
1112
+ * 페이지가 폴더(자식 페이지·자식 DB 보유)인지 신뢰성 있게 판정한다.
1113
+ *
1114
+ * 결함(폴더노트 본문분리)의 근본 원인은 얕은 판정이었다 — 최상위 블록 첫 페이지에서
1115
+ * child_page 만 검사하면 callout·column·toggle 안에 중첩된 자식 페이지나 child_database 를
1116
+ * 놓쳐 폴더노트가 file 로 오분류되고, 본문이 폴더 밖 최상위로 밀려 resolveUniqueFilePath
1117
+ * 충돌(' (1).md')로 쪼개졌다.
1118
+ *
1119
+ * 1차: 발견 단계(서브트리 순회/증분)에서 전 페이지의 부모를 해소하며 만든
1120
+ * _childParentIds 집합으로 O(1) 판정(추가 API 호출 0, 처리 순서 무관) — 전체 pull 경로.
1121
+ * 폴백: 집합에 없을 때만(증분 pull 의 신규 폴더 등) fetchAllChildrenDeep 로 컨테이너를
1122
+ * 재귀 탐색해 child_page·child_database 를 직접 확인한다.
1123
+ */
1124
+ private pageHasChildContainers;
882
1125
  private pullUpdate;
883
1126
  private pullDelete;
884
1127
  private ensureFolderPage;
@@ -887,11 +1130,32 @@ declare class SyncOrchestrator {
887
1130
  private isFolderNote;
888
1131
  private resolveParentPath;
889
1132
  private pushCreatePage;
1133
+ private pageHasLiveChildren;
890
1134
  private pushUpdatePage;
891
1135
  private fetchPageMarkdown;
1136
+ private collectInlineDbRefs;
1137
+ private resolveNotionIdWikilinks;
892
1138
  private extractParentId;
893
1139
  private resolveBlockToPageId;
894
1140
  private cleanupInterruptedSync;
1141
+ /**
1142
+ * I12 — 중단된 push create 작업 재개.
1143
+ *
1144
+ * pending_operations 에 미완료(create·push) 항목이 있으면:
1145
+ * - state 에 notion_page_id 가 이미 있으면 → 생성·매핑까지는 끝났고 markCompleted 직전에
1146
+ * 중단된 것 → 완료 처리(잔여 본문/이미지는 다음 변경감지가 pushUpdate 로 마무리).
1147
+ * - notion_page_id 가 비어 있으면(Window A: 생성 적용됐으나 매핑 기록 전 중단) → 부모에서
1148
+ * 제목으로 child_page 를 검색해 고아 페이지를 입양(중복 생성 차단). 없으면 자리표시
1149
+ * 레코드를 제거(FK CASCADE 로 op 도 삭제)해 다음 push 가 새로 생성하게 한다.
1150
+ *
1151
+ * 한계(문서화): Notion 은 idempotency key 가 없어 "생성 요청 적용 직후 같은 호출 내 재시도"
1152
+ * 로 인한 중복은 완전히 차단하지 못한다. 429 는 적용 전 거절이라 안전하고, 프로세스 재시작
1153
+ * 후 재개는 본 검색-입양으로 중복을 막는다. DB 모드(부모가 database)는 child_page 검색이
1154
+ * 불가하므로 자리표시 제거 후 재생성으로 폴백한다.
1155
+ */
1156
+ private recoverInterruptedPushOps;
1157
+ /** 부모 페이지의 직속 자식 중 제목이 일치하는(보관/휴지통 제외) child_page id 를 찾는다. */
1158
+ private findChildPageByTitle;
895
1159
  private resolveUniqueFilePath;
896
1160
  }
897
1161
 
@@ -924,6 +1188,13 @@ declare class NodeVaultFS implements VaultFS {
924
1188
  private walkDir;
925
1189
  }
926
1190
 
1191
+ /** 미디어(이미지/파일) 다운로드 운영 튜닝값. 미지정 시 기존 동작과 동일한 기본값 사용. */
1192
+ interface MediaOptions {
1193
+ readonly concurrency?: number;
1194
+ readonly maxRetries?: number;
1195
+ readonly retryBaseMs?: number;
1196
+ readonly maxFileSizeBytes?: number;
1197
+ }
927
1198
  interface ImageDownloadResult {
928
1199
  readonly originalUrl: string;
929
1200
  readonly localPath: string;
@@ -933,13 +1204,21 @@ interface ImageDownloadResult {
933
1204
  interface ImageUploadResult {
934
1205
  readonly localPath: string;
935
1206
  readonly fileUploadId: string;
1207
+ /** 업로드한 이미지의 전체 sha256 — file_registry 비교 키(FileHandler 와 동일 산식). */
1208
+ readonly fileHash: string;
1209
+ readonly fileSize: number;
936
1210
  }
937
1211
  declare class ImageHandler {
938
1212
  private readonly vaultFs;
939
1213
  private readonly attachmentFolder;
940
1214
  private readonly notionClient?;
1215
+ private readonly stateDb?;
941
1216
  private readonly customFetch?;
942
- constructor(vaultFs: VaultFS, attachmentFolder?: string, notionClient?: NotionClient | undefined, customFetch?: typeof globalThis.fetch);
1217
+ private readonly concurrency;
1218
+ private readonly maxRetries;
1219
+ private readonly retryBaseMs;
1220
+ private readonly maxFileSizeBytes;
1221
+ constructor(vaultFs: VaultFS, attachmentFolder?: string, notionClient?: NotionClient | undefined, customFetch?: typeof globalThis.fetch, options?: MediaOptions, stateDb?: IStateDB | undefined);
943
1222
  private get fetchFn();
944
1223
  downloadImage(url: string, pageTitle: string): Promise<ImageDownloadResult>;
945
1224
  downloadAllImages(markdown: string, pageTitle: string): Promise<{
@@ -979,7 +1258,7 @@ declare class FileHandler {
979
1258
  private readonly notionClient;
980
1259
  private readonly stateDb;
981
1260
  private readonly sema;
982
- constructor(vaultFs: VaultFS, notionClient: NotionClient, stateDb: IStateDB);
1261
+ constructor(vaultFs: VaultFS, notionClient: NotionClient, stateDb: IStateDB, concurrency?: number);
983
1262
  pushFilesForFolder(folderPageId: string, folderPath: string): Promise<FileUploadResult[]>;
984
1263
  pushAllFiles(): Promise<FileUploadResult[]>;
985
1264
  downloadFileBlock(url: string, filename: string, targetFolder: string): Promise<FileDownloadResult>;
@@ -1006,7 +1285,16 @@ declare class ConversionPipeline {
1006
1285
  interface DatabaseSyncResult {
1007
1286
  created: number;
1008
1287
  updated: number;
1288
+ /** Pull 시 로컬·리모트 동시 수정으로 발생한 충돌 (push 는 항상 빈 배열). */
1289
+ conflicts: Conflict[];
1009
1290
  failed: FailedOperation[];
1291
+ /**
1292
+ * 이번 pull 에서 실제로 디스크에 기록한 db-row 파일 경로(SSOT).
1293
+ * 오케스트레이터의 링크 해소(resolveNotionLinks) 대상은 반드시 이 목록으로 정한다 —
1294
+ * 과거의 `getByStatus("synced").slice(-N)` 휴리스틱은 ORDER BY 가 없어 증분 pull 에서
1295
+ * 엉뚱한 행을 골라 정작 바뀐 행의 relation UUID 를 영영 해소하지 못했다(M3). push 경로는 항상 빈 배열.
1296
+ */
1297
+ writtenPaths: string[];
1010
1298
  }
1011
1299
  declare class DatabaseSyncer {
1012
1300
  private readonly config;
@@ -1017,14 +1305,33 @@ declare class DatabaseSyncer {
1017
1305
  private readonly imageHandler;
1018
1306
  private readonly propertyMapper;
1019
1307
  private readonly baseFileGenerator;
1308
+ private readonly sidecarGenerator;
1020
1309
  constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, pipeline: ConversionPipeline, imageHandler: ImageHandler);
1021
1310
  pullAll(): Promise<DatabaseSyncResult>;
1022
1311
  pushAll(): Promise<DatabaseSyncResult>;
1023
1312
  pullDatabase(dbConfig: DatabaseSyncConfig): Promise<DatabaseSyncResult>;
1024
1313
  private pullDatabaseViews;
1025
1314
  private generateBaseFile;
1315
+ /**
1316
+ * DB 제목이 바뀌면 `.base`/`.notion.json` 파일명(safeName)이 바뀌어 옛 이름의 산출물이
1317
+ * 같은 폴더에 고아로 잔존한다(drift·부활 원인). DB 폴더 **직속**의 산출물 중 현재 이름이
1318
+ * 아닌 것을 삭제해 멱등을 보장한다. 하위 폴더(중첩 DB)의 산출물은 보호한다.
1319
+ */
1320
+ private cleanupStaleDbArtifacts;
1321
+ /**
1322
+ * `.base` 가 표현하지 못하는 Notion DB 메타데이터(미지원 뷰·필터식·커버크기 등)를
1323
+ * 사이드카 `<db>.notion.json` 으로 무손실 보존하고, degrade 된 항목을 정직하게 로그한다.
1324
+ * 결정적 직렬화라 동일 DB 상태 → 동일 바이트 → 멱등(drift/churn 0).
1325
+ */
1326
+ private generateSidecar;
1026
1327
  private pullDatabasePage;
1027
1328
  private pushDatabase;
1329
+ /**
1330
+ * rename 감지를 위한 고아 레코드 인덱스(내용 해시 → 레코드)를 만든다.
1331
+ * 고아 = 해당 DB 소속 db-row 레코드 중 로컬 파일이 더 이상 존재하지 않는(livePaths 에 없는) 것.
1332
+ * 같은 해시가 여러 고아에 걸리면 첫 항목을 유지한다(희박한 케이스).
1333
+ */
1334
+ private buildOrphanHashIndex;
1028
1335
  }
1029
1336
 
1030
1337
  declare class BlockConverter {
@@ -1032,6 +1339,19 @@ declare class BlockConverter {
1032
1339
  private client;
1033
1340
  initNotionToMd(client: Client): void;
1034
1341
  private registerCustomTransformers;
1342
+ /**
1343
+ * `numbered_list_item` 을 `1.` 마크다운으로 내보내 **번호 목록 타입을 보존**한다(I2 블록 라운드트립).
1344
+ *
1345
+ * notion-to-md 기본 렌더는 `md.bullet(text, block.numbered_list_item.number)` 를 쓰는데
1346
+ * Notion API 는 list item 에 `.number` 를 채워주지 않아 `undefined` → 불릿(`- `)으로 **강등**된다.
1347
+ * 그러면 블록 폴백 경로(notionBlocksToMarkdown)로 pull 한 뒤 다시 push 하면 numbered_list_item 이
1348
+ * bulleted_list_item 으로 **타입 유실**된다(기본 markdown-api 경로는 영향 없음).
1349
+ *
1350
+ * 항상 `1.` 로 내보내면 마크다운이 시각적으로 자동 재번호하므로 표시는 동일하고, martian 이 다시
1351
+ * numbered_list_item 으로 파싱해 타입이 보존된다. 커스텀 트랜스포머 등록은 notion-to-md 의 기본
1352
+ * 자식 처리를 끄므로(라이브러리 설계), 자식은 toggle/callout 과 동일하게 직접 fetch·들여쓰기한다.
1353
+ */
1354
+ private registerNumberedListTransformer;
1035
1355
  private registerToggleTransformer;
1036
1356
  private registerToggleHeadingTransformers;
1037
1357
  private registerColumnTransformer;
@@ -1055,6 +1375,14 @@ declare class BlockConverter {
1055
1375
  private toggleCounter;
1056
1376
  private columnCounter;
1057
1377
  private preProcessMarkdown;
1378
+ /**
1379
+ * compact underline/color 보존마커를 평문으로 강등 — block(martian) 폴백 경로 전용.
1380
+ *
1381
+ * 무손실 underline/color 복원은 기본 경로인 Markdown API({@link obsidianToNotionEnhanced})가
1382
+ * 전담한다. martian 은 이 서식을 표현할 수 없으므로, Markdown API 가 실패해 이 폴백을 탈 때는
1383
+ * 마커 텍스트가 본문에 리터럴로 새지 않도록 텍스트만 남기고 마커를 제거한다(서식 degrade).
1384
+ */
1385
+ private stripInlineAnnotationMarkers;
1058
1386
  private replaceDividers;
1059
1387
  private extractToggleBlocks;
1060
1388
  private extractColumnBlocks;
@@ -1062,9 +1390,41 @@ declare class BlockConverter {
1062
1390
  private convertTogglePlaceholder;
1063
1391
  private convertColumnPlaceholder;
1064
1392
  private convertDividerPlaceholder;
1393
+ /**
1394
+ * 단일 라인 보존 마커(목차·breadcrumb)를 원래 블록으로 복원한다.
1395
+ * pull 시 마크다운 표현이 없는 블록을 `%%im-nobsidian:toc%%` /
1396
+ * `%%im-nobsidian:breadcrumb%%` 마커 문단으로 남겨두므로, push 시 이 문단을
1397
+ * 감지해 table_of_contents / breadcrumb 블록으로 되돌린다. 그러지 않으면
1398
+ * 마커 텍스트가 일반 문단으로 Notion 에 기록되는 잠재 손실이 된다.
1399
+ */
1400
+ private convertSingleLineMarker;
1065
1401
  private convertImageToVideoOrEmbed;
1402
+ /**
1403
+ * `@tryfabric/martian` 의 blockquote 표현을 **평탄화**한다(I2/I3 무손실 push).
1404
+ *
1405
+ * martian 의 parseBlockquote 는 `notion.blockquote([], children)` 로 본문을 quote 의
1406
+ * **자식 paragraph** 로 내려보내고 quote.rich_text 를 비운다(라이브러리 설계). 그 결과
1407
+ * `> 인용문` 을 push 하면 Notion 에 "빈 인용 줄 + 들여쓴 자식 문단"이 만들어지고, 다시
1408
+ * pull 하면 `> `(빈 줄) 아래에 자식 문단이 매달려 **원문과 구조가 갈라진다**(라운드트립 손실).
1409
+ * 또한 quote.rich_text[0] 이 항상 비어 callout 이모지 감지({@link convertQuoteToCallout})도
1410
+ * 무력화됐다.
1411
+ *
1412
+ * 선두의 연속된 paragraph 자식을 quote.rich_text 로 끌어올리고(문단 사이는 줄바꿈 rich_text
1413
+ * 로 연결 — Notion 네이티브 멀티라인 인용 표현), paragraph 가 아닌 자식(중첩 리스트 등)은
1414
+ * children 으로 남겨 구조를 보존한다. 이미 rich_text 에 본문이 있거나 끌어올릴 paragraph 가
1415
+ * 없으면 원본을 그대로 반환한다(안전 no-op).
1416
+ */
1417
+ private flattenMartianBlockquote;
1066
1418
  private convertQuoteToCallout;
1067
1419
  notionBlocksToMarkdown(pageId: string): Promise<string>;
1420
+ /**
1421
+ * **in-memory** 블록 배열을 마크다운으로 변환한다(페이지 fetch 없음). notion-to-md 의
1422
+ * blocksToMarkdown 을 직접 호출하므로, `has_children:false` 인 평면 블록은 Notion 클라이언트를
1423
+ * 전혀 건드리지 않는다 → 고정 픽스처로 **오프라인 결정론** 블록 라운드트립(I2)을 잠글 수 있는
1424
+ * 정식 테스트 시임. children 이 있는 블록(table 행·callout 자식 등)은 클라이언트를 호출하므로
1425
+ * 호출 측이 평면 블록만 넣어야 오프라인이 보장된다.
1426
+ */
1427
+ notionBlockArrayToMarkdown(blocks: Array<Record<string, unknown>>): Promise<string>;
1068
1428
  }
1069
1429
 
1070
1430
  declare class FrontmatterExtractor implements Processor {
@@ -1124,8 +1484,26 @@ declare class PropertiesTableInjector implements Processor {
1124
1484
  process(input: ProcessorInput): ProcessorOutput;
1125
1485
  }
1126
1486
 
1127
- declare class HtmlAnnotationStripper implements Processor {
1128
- readonly name = "HtmlAnnotationStripper";
1487
+ /**
1488
+ * 인라인 annotation(underline·color) 무손실 보존기 — push 전처리(order 5).
1489
+ *
1490
+ * 과거에는 underline/color 표기를 **제거**(strip)했으나, 그러면 Notion 으로 push 할 때
1491
+ * 서식이 영구 소실됐다(I3 손실점). Notion 의 Enhanced Markdown 은 underline/color 를
1492
+ * `<span underline="true">` / `<span color="X">` 로 **무손실 표현**하므로, 여기서는 제거 대신
1493
+ * 프로젝트 정본인 **compact 보존마커**(`%%im-nobsidian:underline%%…%%/underline%%`,
1494
+ * `%%im-nobsidian:color:C%%…%%/color%%`)로 **승격**만 한다. 실제 Notion span 복원은
1495
+ * push 직전 단일 SSOT 인 {@link obsidianToNotionEnhanced} 가 담당한다.
1496
+ *
1497
+ * 입력 표기는 pull 경로별로 다르다.
1498
+ * - Markdown API 경로: 이미 compact 마커 → 무변경 통과(여기 정규식에 걸리지 않음).
1499
+ * - notion-to-md(block) 경로: `<u>` + 공백형 색상 마커 → compact 마커로 정규화.
1500
+ * - 사용자가 직접 작성한 `<u>` / 레거시 `<span class="notion-*">` → compact 마커로 승격.
1501
+ *
1502
+ * compact 마커는 공백이 없어 PreserveMarkerCollector(공백형만 수집)·기타 전처리기를
1503
+ * 무손상 통과한다(기존 underline/color 왕복이 이를 실증).
1504
+ */
1505
+ declare class InlineAnnotationPreserver implements Processor {
1506
+ readonly name = "InlineAnnotationPreserver";
1129
1507
  readonly order = 5;
1130
1508
  process(input: ProcessorInput): ProcessorOutput;
1131
1509
  }
@@ -1184,29 +1562,26 @@ interface MergeConflictRegion {
1184
1562
  readonly localLines: string[];
1185
1563
  readonly remoteLines: string[];
1186
1564
  }
1565
+ /**
1566
+ * 3-way 병합 (diff3). base 를 공통 조상으로 보고 local/remote 의 변경을 합친다.
1567
+ *
1568
+ * 표준 diff3 알고리즘:
1569
+ * 1. base↔local, base↔remote 각각의 LCS 로 "변경 조각(hunk)"을 추출한다.
1570
+ * (hunk = base 의 어떤 구간 [baseStart, baseEnd) 을 새 내용으로 치환·삭제·삽입)
1571
+ * 2. 모든 hunk 를 base 위치순으로 정렬하고, **base 구간이 겹치는** hunk 들을
1572
+ * 하나의 region 으로 묶는다. 겹치지 않는 hunk 사이의 base 줄은 양쪽 공통이므로 그대로 출력.
1573
+ * 3. 각 region 에서 local/remote 가 만들어낸 내용을 비교해 판정한다.
1574
+ * - local 결과 == base → local 미변경 → remote 채택
1575
+ * - remote 결과 == base → remote 미변경 → local 채택
1576
+ * - local 결과 == remote 결과 → 동일 변경 → 한쪽 채택
1577
+ * - 그 외 → 충돌 (양쪽이 같은 구간을 다르게 변경)
1578
+ *
1579
+ * 핵심은 hunk 의 base 구간이 **실제로 겹칠 때만** 충돌로 본다는 점이다. 서로 다른(인접
1580
+ * 포함) 줄을 고친 삽입·삭제·수정은 충돌 없이 합쳐진다. (이전 단순 라인-치환 구현은
1581
+ * 삽입/삭제를 오정렬해 거짓 충돌을 만들거나 무한 루프에 빠질 수 있었다.)
1582
+ */
1187
1583
  declare function threeWayMerge(base: string, local: string, remote: string): MergeResult;
1188
1584
 
1189
- type ResolutionChoice = "local" | "remote" | "merge" | "duplicate";
1190
- interface ResolutionResult {
1191
- readonly path: string;
1192
- readonly choice: ResolutionChoice;
1193
- readonly success: boolean;
1194
- readonly mergeHadConflicts?: boolean;
1195
- }
1196
- declare class ConflictResolver {
1197
- private readonly stateDb;
1198
- private readonly vaultFs;
1199
- constructor(stateDb: IStateDB, vaultFs: VaultFS);
1200
- resolve(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
1201
- resolveByStrategy(conflict: Conflict, strategy: ConflictStrategy): Promise<ResolutionResult>;
1202
- resolveAll(conflicts: Conflict[], strategy: ConflictStrategy): Promise<ResolutionResult[]>;
1203
- private resolveLocal;
1204
- private resolveRemote;
1205
- private resolveMerge;
1206
- private resolveDuplicate;
1207
- generateDiff(conflict: Conflict): string;
1208
- }
1209
-
1210
1585
  interface NotionPageNode {
1211
1586
  readonly id: string;
1212
1587
  readonly title: string;
@@ -1260,12 +1635,12 @@ declare class FileWatcher {
1260
1635
  private watcher;
1261
1636
  constructor(rootPath: string, callback: WatchCallback);
1262
1637
  start(): void;
1638
+ private toVaultPath;
1263
1639
  stop(): Promise<void>;
1264
1640
  }
1265
1641
 
1266
1642
  interface WatchSyncOptions {
1267
1643
  readonly debounceMs?: number;
1268
- readonly syncDirection?: "push" | "pull" | "both";
1269
1644
  readonly onSyncStart?: () => void;
1270
1645
  readonly onSyncComplete?: (result: SyncResult) => void;
1271
1646
  readonly onSyncError?: (error: Error) => void;
@@ -1376,6 +1751,18 @@ interface BaseFileOptions {
1376
1751
  readonly viewsConfig: DatabaseViewsConfig;
1377
1752
  readonly folderPath: string;
1378
1753
  }
1754
+ type BasesViewType = "table" | "cards" | "list";
1755
+ /**
1756
+ * Notion 뷰 타입 → Obsidian Bases 뷰 타입 매핑(SSOT).
1757
+ *
1758
+ * `null` = Bases 에 대응 뷰가 없어 `.base` 로 표현 불가. 이 경우 해당 뷰는 `.base` 에서
1759
+ * 누락되므로, 사이드카(`<db>.notion.json`)가 원본 뷰 설정을 무손실 보존한다
1760
+ * (Notion API 는 뷰 생성/수정을 지원하지 않아 뷰는 pull-authoritative — push 로 되돌릴 수
1761
+ * 없으므로 "조용한 유실 금지"는 곧 "보존 + 정직한 degrade 리포트"를 의미한다).
1762
+ */
1763
+ declare const NOTION_TO_BASES_VIEW: Record<string, BasesViewType | null>;
1764
+ /** Notion 뷰 타입의 Bases 대응 타입을 돌려준다. 미지원/미상 타입은 `null`. */
1765
+ declare function basesViewTypeOf(notionType: string): BasesViewType | null;
1379
1766
  declare class BaseFileGenerator {
1380
1767
  generate(options: BaseFileOptions): string;
1381
1768
  private writeFilters;
@@ -1383,13 +1770,104 @@ declare class BaseFileGenerator {
1383
1770
  private writeFormulas;
1384
1771
  private writePropertyDisplayNames;
1385
1772
  private writeViews;
1773
+ /**
1774
+ * 뷰 이름을 Bases 요구사항대로 유일화한다(in-place). 동명 충돌 시 ' 2',' 3'… 를 붙이되
1775
+ * 이미 존재하는 이름과 다시 충돌하지 않을 때까지 증가시킨다.
1776
+ */
1777
+ private dedupeViewNames;
1386
1778
  private convertView;
1387
1779
  private toNoteRef;
1780
+ /**
1781
+ * 속성 참조(order/sort/groupBy/image)를 YAML로 안전하게 출력한다.
1782
+ * `file.name`·`formula.coverImage`·`note.cover` 같은 접두사 식별자와 공백 포함 일반 속성명은
1783
+ * 평문 스칼라로 안전하므로 그대로 두고, 콜론·해시·따옴표 등 YAML 메타문자가 있을 때만 인용한다.
1784
+ */
1785
+ private propRef;
1388
1786
  private resolvePropertyName;
1787
+ private decodeId;
1389
1788
  private yamlKey;
1390
1789
  private yamlValue;
1391
1790
  }
1392
1791
 
1792
+ /**
1793
+ * DB 사이드카(`<db>.notion.json`) — `.base` 로 표현할 수 없는 Notion DB 메타데이터를
1794
+ * 무손실 보존하는 단일 파일.
1795
+ *
1796
+ * ## 왜 필요한가 (I4·I7 "조용한 유실 금지")
1797
+ *
1798
+ * Obsidian Bases(`.base`)는 Notion DB 뷰의 일부만 표현한다:
1799
+ * · 미지원 뷰(calendar/timeline/form/chart/map/dashboard)는 `.base` 에서 통째 누락.
1800
+ * · 표현되는 뷰도 Notion **필터식·커버 크기/비율·컬럼 width/wrap·빈그룹 숨김** 등은
1801
+ * Bases 문법에 대응이 없어 손실된다.
1802
+ *
1803
+ * Notion API 는 뷰 생성/수정을 지원하지 않으므로(뷰는 pull-authoritative) 이 메타데이터는
1804
+ * push 로 Notion 에 되돌릴 수 없다. 따라서 "무손실"의 의미는 **로컬에 완전·정직하게
1805
+ * 보존하고, 무엇이 degrade 됐는지 명시 리포트**하는 것이다. 사이드카는 이를 `.base` 와
1806
+ * 같은 폴더에 co-locate 해 사람이 발견 가능하고 도구가 재사용 가능하게 만든다.
1807
+ *
1808
+ * ## 멱등성
1809
+ *
1810
+ * 매 pull 마다 재생성되므로 **결정적**이어야 한다(타임스탬프·실행시각 금지, 키 정렬).
1811
+ * 동일 DB 상태 → 동일 바이트 → drift 0 / churn 0.
1812
+ */
1813
+ interface NotionSidecar {
1814
+ readonly generator: "im-nobsidian";
1815
+ readonly schemaVersion: 1;
1816
+ readonly databaseId: string;
1817
+ readonly databaseName: string;
1818
+ /** 속성 스키마 — 이름 정렬. `.base` displayName 만으로는 잃는 타입/옵션을 보존. */
1819
+ readonly properties: SidecarProperty[];
1820
+ /** 모든 뷰(표현/미표현 공통) — Notion 순서 유지. */
1821
+ readonly views: SidecarView[];
1822
+ /** 정직한 손실 리포트 — 무엇이 `.base` 에서 누락/degrade 됐는가. */
1823
+ readonly degraded: DegradeNote[];
1824
+ }
1825
+ interface SidecarProperty {
1826
+ readonly name: string;
1827
+ readonly id: string;
1828
+ readonly type: string;
1829
+ /** select/multi_select/status 옵션명(있으면). */
1830
+ readonly options?: string[];
1831
+ }
1832
+ interface SidecarView {
1833
+ readonly id: string;
1834
+ readonly name: string;
1835
+ readonly notionType: string;
1836
+ /** Bases 대응 타입. `null` = `.base` 미표현(사이드카가 유일 보존처). */
1837
+ readonly basesType: BasesViewType | null;
1838
+ /** 원본 뷰 설정 중 의미 있는 항목(키 정렬). 빈 값은 생략. */
1839
+ readonly config: Record<string, unknown>;
1840
+ }
1841
+ interface DegradeNote {
1842
+ readonly view: string;
1843
+ readonly kind: "view-unrepresentable" | "setting-dropped";
1844
+ readonly detail: string;
1845
+ }
1846
+ declare class SidecarGenerator {
1847
+ /**
1848
+ * 사이드카 객체를 빌드한다(순수 — I/O 없음). 직렬화는 {@link serialize} 가 담당.
1849
+ */
1850
+ build(options: {
1851
+ databaseId: string;
1852
+ databaseName: string;
1853
+ schema: Record<string, BasePropertySchema>;
1854
+ viewsConfig: DatabaseViewsConfig;
1855
+ }): NotionSidecar;
1856
+ /**
1857
+ * 사이드카를 결정적 JSON 문자열로 직렬화한다(키 재귀 정렬 + 말미 개행).
1858
+ * 동일 입력 → 동일 바이트 → 멱등(drift/churn 0).
1859
+ */
1860
+ serialize(sidecar: NotionSidecar): string;
1861
+ private buildProperties;
1862
+ private buildView;
1863
+ /**
1864
+ * 뷰 설정 중 의미 있는 항목을 추출한다. `.base` 가 표현 못하는 항목(필터·커버크기 등)은
1865
+ * config 에 보존하면서 동시에 degrade 리포트에 1줄씩 기록한다(표현되는 뷰에 한해 —
1866
+ * 미표현 뷰는 이미 view-unrepresentable 로 통째 보고됨).
1867
+ */
1868
+ private extractConfig;
1869
+ }
1870
+
1393
1871
  declare function sortEntries(entries: DBEntry[], sorts: NonNullable<ViewConfig["sorts"]>): DBEntry[];
1394
1872
  declare function groupEntries(entries: DBEntry[], groupByProperty: string, options?: {
1395
1873
  sort?: "ascending" | "descending";
@@ -1414,12 +1892,95 @@ declare function getNotionColor(color: NotionColor | string): string;
1414
1892
  declare function getNotionBgColor(color: NotionColor | string): string;
1415
1893
  declare function generateColorCSS(): string;
1416
1894
 
1895
+ /**
1896
+ * 본문 링크 충실도(fidelity) 분류기 — 회귀 상시 잠금용 측정 인프라.
1897
+ *
1898
+ * pull 결과물(.md 본문)에 남은 notion 링크/멘션을 네 부류로 분류한다:
1899
+ *
1900
+ * 1. preservedMarkers — `[text](url)%%im-nobsidian:...%%` 형태의 의도된 무손실
1901
+ * 보존 마커(copy_indicator 등). push 라운드트립 복원용으로 *일부러* 남긴 것이라
1902
+ * 결함이 아니다.
1903
+ * 2. externalLinks — 동기화 코퍼스에 없는 외부 notion.so 링크. 위키링크로 환원할
1904
+ * 대상이 없으므로 맨 링크로 두는 게 정당하다.
1905
+ * 3. selfReferences — 자기 자신 페이지를 가리키는 링크/멘션(블록 앵커 등). 정당.
1906
+ * 4. defects — 동기화된 *다른* 페이지로의 미해소 링크/멘션. 이것만이 진짜
1907
+ * 충실도 결함이다(M2/M3/M4/M6 가 봉합한 대상).
1908
+ *
1909
+ * 변환 파이프라인이 완전하면 `defects.length === 0` 이어야 한다. 이 분류기를 픽스처로
1910
+ * 잠가(unit test) 두면 변환기 회귀가 즉시 드러나고, 실제 vault 에 대고 돌리면(아래
1911
+ * scripts/audit-vault.mjs) 코퍼스 전체의 충실도를 정량 측정할 수 있다.
1912
+ *
1913
+ * 마커/ID 판별은 SSOT 를 재사용한다 — 마커 브랜드는 {@link MARKER_BRAND},
1914
+ * 정규 id 는 {@link compactNotionId}. 별도 헬퍼를 만들지 않는다.
1915
+ */
1916
+ /** 미해소로 남은 링크/멘션의 형태. */
1917
+ type FidelityDefectForm = "md-link" | "mention-page";
1918
+ /** 진짜 충실도 결함 — 동기화된 다른 페이지로의 미해소 링크/멘션. */
1919
+ interface FidelityDefect {
1920
+ /** 미해소 형태(맨 마크다운 링크 / 미변환 mention-page 태그). */
1921
+ form: FidelityDefectForm;
1922
+ /** 대상 페이지의 정규(compact) id. */
1923
+ targetPageId: string;
1924
+ /** 대상 페이지 제목(맵에 있으면). */
1925
+ targetTitle: string | null;
1926
+ /** 결함 원문 조각(진단용). */
1927
+ raw: string;
1928
+ }
1929
+ /** {@link classifyBodyFidelity} 분류 결과. */
1930
+ interface FidelityClassification {
1931
+ /** `[text](url)%%im-nobsidian:...%%` — 의도된 무손실 보존 마커(결함 아님). */
1932
+ preservedMarkers: number;
1933
+ /** 코퍼스에 없는 외부 notion.so 링크(정당). */
1934
+ externalLinks: number;
1935
+ /** 자기 자신 페이지를 가리키는 링크/멘션(정당). */
1936
+ selfReferences: number;
1937
+ /** 동기화된 다른 페이지로의 미해소 링크/멘션 — 진짜 결함. */
1938
+ defects: FidelityDefect[];
1939
+ }
1940
+ /** 분류에 필요한 코퍼스 컨텍스트. */
1941
+ interface FidelityContext {
1942
+ /** 동기화된 전체 페이지의 정규(compact) id 집합. */
1943
+ knownPageIds: ReadonlySet<string>;
1944
+ /** 정규 id → 제목. 선택(결함 진단용). */
1945
+ titleById?: ReadonlyMap<string, string>;
1946
+ /** 이 본문이 속한 페이지의 정규 id. 자기참조 판별용. 선택. */
1947
+ ownPageId?: string;
1948
+ }
1949
+ /**
1950
+ * 본문 문자열의 notion 링크/멘션 충실도를 분류한다. 순수 함수 — I/O 없음.
1951
+ *
1952
+ * @param body 분석할 마크다운 본문(프론트매터 포함 가능; 보존 마커·위키링크는 무시됨).
1953
+ * @param ctx 코퍼스 컨텍스트(알려진 페이지 id 집합 등).
1954
+ */
1955
+ declare function classifyBodyFidelity(body: string, ctx: FidelityContext): FidelityClassification;
1956
+ /** 여러 본문의 분류 결과를 합산한 코퍼스 단위 요약. */
1957
+ interface FidelitySummary {
1958
+ files: number;
1959
+ preservedMarkers: number;
1960
+ externalLinks: number;
1961
+ selfReferences: number;
1962
+ defects: number;
1963
+ /** 결함이 1건 이상인 파일 경로 → 결함 목록. */
1964
+ defectsByFile: Record<string, FidelityDefect[]>;
1965
+ }
1966
+ /** 파일별 분류 결과(경로 + 분류)를 코퍼스 요약으로 합산한다. */
1967
+ declare function summarizeFidelity(perFile: ReadonlyArray<{
1968
+ path: string;
1969
+ classification: FidelityClassification;
1970
+ }>): FidelitySummary;
1971
+
1417
1972
  declare function computeHash(content: string): string;
1418
1973
  declare function computeBufferHash(buffer: Buffer): string;
1419
1974
 
1420
1975
  declare function generateId(): string;
1421
1976
  declare function normalizeNotionId(id: string): string;
1422
1977
  declare function notionIdsEqual(a: string, b: string): boolean;
1978
+ /**
1979
+ * 멘션 위키링크용 정규 id — 하이픈 제거 + 소문자. 오케스트레이터의 멘션 해소 정규식
1980
+ * (`[[notion:<32hex>]]`)이 인식하는 유일한 형태다. 블록 폴백 경로(formatMention)와
1981
+ * markdown-api 경로(convertPageMentions)가 동일 형태로 수렴하도록 한다(rank20/I3).
1982
+ */
1983
+ declare function compactNotionId(id: string): string;
1423
1984
 
1424
1985
  declare function sanitizeFileName(name: string): string;
1425
1986
 
@@ -1437,4 +1998,47 @@ interface PipelineOptions {
1437
1998
  }
1438
1999
  declare function createDefaultPipeline(options?: PipelineOptions): ConversionPipeline;
1439
2000
 
1440
- export { BaseFileGenerator, type BaseFileOptions, type BasePropertySchema, BlockConverter, type CalendarEntry, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, type Config, ConfigManager, ConfigSchema, type Conflict, ConflictResolver, type ConflictStrategy, type ConversionContext, type ConversionPath, ConversionPipeline, type ConversionResult, type CoverConfig, type DBEntry, DEFAULT_CONFIG, type DatabaseSyncConfig, type DatabaseSyncResult, DatabaseSyncer, type DatabaseViewsConfig, EmbedResolver, EntryEditor, type FailedOperation, type FileDownloadResult, FileHandler, type FileInfo, type FileRegistryEntry, type FileStatInfo, type FileSystemEntry, type FileType, type FileUploadResult, FileWatcher, type FilterCondition, FrontmatterExtractor, FrontmatterGenerator, type GroupByConfig, type GroupedEntries, HtmlAnnotationStripper, type IStateDB, type ImageDownloadResult, ImageHandler, type ImageReference, InlineDBParser, type LocalChange, LocalImageRestorer, type Logger, type MappingResult, type MappingType, MathNormalizer, MentionToWikilink, type MergeConflictRegion, type MergeResult, NodeVaultFS, type NonMdFileInfo, type NotionBlock, NotionBlockBuilder, NotionClient, type NotionClientOptions, type NotionColor, type NotionPageNode, type NotionRichText, type RichTextItem as NotionRichTextItem, type OperationType, type PageCover, type PageCreationPlan, type PageIcon, type PathFilterConfig, type PreserveMarker, PreserveMarkerCollector, PreserveMarkerInjector, type Processor, type ProcessorInput, type ProcessorMetadata, type ProcessorOutput, type ProgressCallback, type ProgressItem, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, type PropertyOption, type PropertySchema, type PropertyValue, type PullOptions, type PullResult, type PushOptions, type PushResult, type RegisterFileInput, type RemoteChange, type ResolutionChoice, type ResolutionResult, type RichTextSegment, StateDB, type StatusResult, type SyncDirection, type SyncOptions, SyncOrchestrator, type SyncRecord, type SyncResult, type SyncStatus, TreeMapper, UnsupportedBlockStripper, type UpsertSyncRecord, type VaultFS, type ViewConfig, ViewDataProvider, type ViewPropertyConfig, type ViewRenderData, type ViewType, type WatchSyncOptions, WatchSyncService, type WikilinkEntry, WikilinkResolver, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, threeWayMerge };
2001
+ /**
2002
+ * 내부 메타데이터 경로 단일 진실원(SSOT).
2003
+ *
2004
+ * `.im-nobsidian` 디렉터리·하위 파일명·ignore 파일·gitignore 항목을 한곳에서 정의한다.
2005
+ * 디렉터리/파일명을 바꾸거나 리브랜딩할 때 이 파일만 수정하면 전 패키지에 반영된다.
2006
+ */
2007
+ /** 내부 메타데이터 디렉터리명 (vault 루트 기준). */
2008
+ declare const INTERNAL_DIR = ".im-nobsidian";
2009
+ /** 동기화 설정 파일명. */
2010
+ declare const CONFIG_FILE = "config.json";
2011
+ /** SQLite 상태 DB 파일명. */
2012
+ declare const STATE_DB_FILE = "sync.db";
2013
+ /** DB 뷰 메타데이터 파일명. */
2014
+ declare const DB_VIEWS_FILE = "db-views.json";
2015
+ /** 동기화 제외 패턴 파일명 (디렉터리 외부, vault 루트 직속). */
2016
+ declare const IGNORE_FILE = ".im-nobsidian-ignore";
2017
+ /** `.gitignore`에 추가하는 항목. */
2018
+ declare const GITIGNORE_ENTRY = ".im-nobsidian/";
2019
+ /** vault 상대 POSIX 경로 — DB 뷰 메타데이터. */
2020
+ declare const DB_VIEWS_PATH = ".im-nobsidian/db-views.json";
2021
+ /** vault 상대 POSIX 경로 — 상태 DB. */
2022
+ declare const STATE_DB_PATH = ".im-nobsidian/sync.db";
2023
+ /** 파일 감시에서 제외할 내부 디렉터리 glob. */
2024
+ declare const INTERNAL_DIR_GLOB = "**/.im-nobsidian/**";
2025
+ /**
2026
+ * 주어진 vault 상대 경로(POSIX)가 내부 메타데이터 디렉터리에 속하는지 판별한다.
2027
+ */
2028
+ declare function isInternalPath(path: string): boolean;
2029
+
2030
+ /**
2031
+ * 보존 마커(preserve marker) 단일 진실원(SSOT).
2032
+ *
2033
+ * 변환 불가 요소를 라운드트립 보존하기 위해 본문에 삽입하는 마커가 공유하는
2034
+ * 브랜드 토큰을 한곳에서 정의한다. 리브랜딩 시 {@link MARKER_BRAND}만 바꾸면
2035
+ * 모든 빌더·정규식에 반영된다.
2036
+ *
2037
+ * 주의: 마커 포맷(공백 유무)은 기존 vault·Notion에 이미 기록된 데이터와의 호환을
2038
+ * 위해 변경하지 않는다. 두 포맷이 공존한다 — 공백형(`%% brand:... %%`)과
2039
+ * 압축형(`%%brand:...%%`).
2040
+ */
2041
+ /** 모든 보존 마커가 공유하는 브랜드 토큰. */
2042
+ declare const MARKER_BRAND = "im-nobsidian";
2043
+
2044
+ export { BaseFileGenerator, type BaseFileOptions, type BasePropertySchema, type BaseSyncOptions, type BasesViewType, BlockConverter, CONFIG_FILE, type CalendarEntry, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, type Config, ConfigManager, ConfigSchema, type Conflict, ConflictResolver, type ConflictStrategy, type ConversionContext, type ConversionPath, ConversionPipeline, type ConversionResult, type CoverConfig, type DBEntry, DB_VIEWS_FILE, DB_VIEWS_PATH, DEFAULT_CONFIG, type DatabaseSyncConfig, type DatabaseSyncResult, DatabaseSyncer, type DatabaseViewsConfig, type DegradeNote, EmbedResolver, EntryEditor, type FailedOperation, type FidelityClassification, type FidelityContext, type FidelityDefect, type FidelityDefectForm, type FidelitySummary, type FileDownloadResult, FileHandler, type FileInfo, type FileRegistryEntry, type FileStatInfo, type FileSystemEntry, type FileType, type FileUploadResult, FileWatcher, type FilterCondition, FrontmatterExtractor, FrontmatterGenerator, GITIGNORE_ENTRY, type GroupByConfig, type GroupedEntries, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, type IStateDB, type ImageDownloadResult, ImageHandler, type ImageReference, InlineAnnotationPreserver, InlineDBParser, type LocalChange, LocalImageRestorer, type Logger, MARKER_BRAND, type MappingResult, type MappingType, MathNormalizer, MentionToWikilink, type MergeConflictRegion, type MergeResult, NOTION_TO_BASES_VIEW, NodeVaultFS, type NonMdFileInfo, type NotionBlock, NotionBlockBuilder, NotionClient, type NotionClientOptions, type NotionColor, type NotionPageNode, type NotionRichText, type RichTextItem as NotionRichTextItem, type NotionSidecar, type OperationType, type PageCover, type PageCreationPlan, type PageIcon, type PathFilterConfig, type PendingOperation, type PreserveMarker, PreserveMarkerCollector, PreserveMarkerInjector, type Processor, type ProcessorInput, type ProcessorMetadata, type ProcessorOutput, type ProgressCallback, type ProgressItem, PropertiesTableInjector, PropertiesTableRestorer, PropertyMapper, type PropertyOption, type PropertySchema, type PropertyValue, type PullOptions, type PullResult, type PushOptions, type PushResult, type RecordPendingInput, type RegisterFileInput, type RemoteChange, type ResolutionChoice, type ResolutionResult, type RichTextSegment, STATE_DB_FILE, STATE_DB_PATH, SidecarGenerator, type SidecarProperty, type SidecarView, StateDB, type StatusResult, type SyncDirection, type SyncOptions, SyncOrchestrator, type SyncRecord, type SyncResult, type SyncStatus, TreeMapper, UnsupportedBlockStripper, type UpsertSyncRecord, type VaultFS, type ViewConfig, ViewDataProvider, type ViewPropertyConfig, type ViewRenderData, type ViewType, type WatchSyncOptions, WatchSyncService, type WikilinkEntry, WikilinkResolver, basesViewTypeOf, classifyBodyFidelity, compactNotionId, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, isInternalPath, normalizeNotionId, notionIdsEqual, sanitizeFileName, setLogger, sortEntries, summarizeFidelity, threeWayMerge };