@im-nobsidian/core 0.2.1 → 0.3.1

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
@@ -395,6 +395,13 @@ interface ProcessorMetadata {
395
395
  readonly properties?: Record<string, unknown>;
396
396
  readonly preserveMarkers?: PreserveMarker[];
397
397
  readonly images?: ImageReference[];
398
+ /**
399
+ * 원시 Notion markdown export 가 압축형(펜스 밖 빈 줄 0)이었는지 — orchestrator 가
400
+ * `notionEnhancedToObsidian` **이전**에 판정해 전달한다. enhanced 변환의
401
+ * `<empty-block/>`→빈 줄 치환이 끝난 뒤에는 BlockSpacer 가 스스로 판정할 수 없다(D1).
402
+ * true: 무조건 재간격 / false: 무동작 / 미지정: 내용 기반 휴리스틱 폴백.
403
+ */
404
+ readonly notionExportCompact?: boolean;
398
405
  readonly [key: string]: unknown;
399
406
  }
400
407
  interface ConversionContext {
@@ -723,16 +730,33 @@ declare class NotionClient {
723
730
  private fetchDatabaseModern;
724
731
  getDatabaseTitle(databaseId: string): Promise<string>;
725
732
  /**
726
- * 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지) 1회 호출로 판별한다.
733
+ * 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지) 판별한다.
727
734
  * 신 모델(2025-09-03)에서 링크드 DB·미공유 데이터 소스·삭제 DB 는 `data_sources` 가
728
735
  * 비어, 행 조회(`dataSources.query`)가 404 로 실패하고 그 전에 생성된 빈 폴더/`.base` 만
729
736
  * 남긴다. 발견 단계에서 미리 걸러 **빈 폴더/.base 오염 + 매 pull 의 404 노이즈**를 차단한다.
730
737
  * 제목은 함께 반환해 호출처가 추가 조회 없이 폴더명을 잡게 한다.
738
+ *
739
+ * F25: 원본이 같은 공유 범위에 있으면 linked view 컨테이너도 `data_sources` 가 **채워져**
740
+ * 온다(실측 — 워크스페이스 8건). 이때 data source 의 parent database 는 원본이므로,
741
+ * 첫 소스의 parent 가 자신이 아니면 `linkedOriginalDbId` 로 알려 컨테이너가 원본과 같은
742
+ * 행 집합을 이중 pull(폴더 릴레이 재배치 churn)하는 것을 발견 단계에서 차단한다.
731
743
  */
732
744
  getDatabaseSyncability(databaseId: string): Promise<{
733
745
  title: string;
734
746
  queryable: boolean;
747
+ linkedOriginalDbId?: string;
735
748
  }>;
749
+ /**
750
+ * linked database view 컨테이너를 원본 DB 로 해소한다.
751
+ * 컨테이너 자체는 `data_sources` 가 비어 행 조회가 불가능하지만(getDatabaseSyncability
752
+ * queryable=false), Views API 의 뷰 상세에는 원본 `data_source_id` 가 실리고 그 data source
753
+ * 의 parent 가 원본 database 다. 해소 실패(뷰 없음/권한 없음/자기참조)는 null — 호출처가
754
+ * 기존대로 접근 불가 처리한다.
755
+ */
756
+ resolveLinkedDatabase(databaseId: string): Promise<{
757
+ originalDbId: string;
758
+ viewName: string;
759
+ } | null>;
736
760
  getDatabaseSchema(databaseId: string): Promise<Record<string, {
737
761
  id: string;
738
762
  type: string;
@@ -797,7 +821,13 @@ declare class NotionClient {
797
821
  fetchAllChildrenDeep(blockId: string): Promise<BlockObjectResponse[]>;
798
822
  appendChildren(blockId: string, children: unknown[]): Promise<void>;
799
823
  deleteBlock(blockId: string): Promise<void>;
824
+ /** single_part 업로드 상한 (Notion API 규격 20MB) */
825
+ private static readonly SINGLE_PART_MAX_BYTES;
826
+ /** multi_part 파트 크기 — 규격상 마지막 파트 제외 5~20MB, 10MB 고정 사용 */
827
+ private static readonly MULTI_PART_CHUNK_BYTES;
800
828
  uploadFile(fileData: Blob, filename: string, contentType: string): Promise<string>;
829
+ /** 20MB 초과 파일의 multi_part 업로드 — part_number 는 API 규격상 문자열("1"부터) */
830
+ private uploadFileMultiPart;
801
831
  search(params: {
802
832
  query?: string;
803
833
  filter?: {
@@ -1073,7 +1103,19 @@ declare class SyncOrchestrator {
1073
1103
  private buildDiscoveredDbConfig;
1074
1104
  /** 접근 불가 DB denylist 를 상태 메타에서 로드한다(하이픈 정규화). */
1075
1105
  private loadInaccessibleDbIds;
1106
+ /** linked view 컨테이너 → 원본 DB 매핑을 상태 메타에서 로드한다(nohyph → nohyph). */
1107
+ private loadLinkedDbMap;
1076
1108
  private pullDiscoveredDatabases;
1109
+ /**
1110
+ * pull 산출 md 의 인라인 DB placeholder(`**제목** *(Notion DB)*` + 보존 마커)를
1111
+ * `![[<실제 .base 경로>|제목]]` 임베드로 재작성한다(F22) — folder note 본문에서
1112
+ * DB 가 텍스트 한 줄이 아니라 실제 Bases 뷰로 보이게 하는 마지막 조각. linked view
1113
+ * 컨테이너는 linkedMap 을 거쳐 원본 DB 의 .base 로 향한다. .base 경로는 DatabaseSyncer
1114
+ * 가 실제 기록한 경로(baseFileInfo)가 SSOT — 폴더명과 .base 파일명은 새니타이즈 규칙이
1115
+ * 달라(공백→하이픈 vs 공백 보존) localFolder 로 추측하면 깨진 임베드가 된다(E2E 실측
1116
+ * 91/158건). 최종적으로 .base 실존까지 확인해, 없으면 placeholder 를 마커째 보존한다.
1117
+ */
1118
+ private rewriteDbPlaceholderEmbeds;
1077
1119
  private resolveNotionLinks;
1078
1120
  private ensureDbSchema;
1079
1121
  private pushCreate;
@@ -1134,6 +1176,13 @@ declare class SyncOrchestrator {
1134
1176
  private pushUpdatePage;
1135
1177
  private fetchPageMarkdown;
1136
1178
  private collectInlineDbRefs;
1179
+ /**
1180
+ * 이번 라운드에 기록된 DB row md 를 읽어 본문의 인라인 DB 참조를 수집한다.
1181
+ * row 페이지는 DatabaseSyncer 자체 파이프라인으로 변환돼 fetchPageMarkdown 의
1182
+ * _inlineDbRefs 수집을 타지 않는다 — 디스크에 남은 보존 마커가 유일한 발견 신호다.
1183
+ * 부모 Notion 페이지 id 는 state DB 역조회(getByPath)로 얻는다(row upsert 직후라 항상 존재).
1184
+ */
1185
+ private collectRowInlineRefs;
1137
1186
  private resolveNotionIdWikilinks;
1138
1187
  private extractParentId;
1139
1188
  private resolveBlockToPageId;
@@ -1221,12 +1270,19 @@ declare class ImageHandler {
1221
1270
  constructor(vaultFs: VaultFS, attachmentFolder?: string, notionClient?: NotionClient | undefined, customFetch?: typeof globalThis.fetch, options?: MediaOptions, stateDb?: IStateDB | undefined);
1222
1271
  private get fetchFn();
1223
1272
  downloadImage(url: string, pageTitle: string): Promise<ImageDownloadResult>;
1224
- downloadAllImages(markdown: string, pageTitle: string): Promise<{
1273
+ private fetchImage;
1274
+ private persistImage;
1275
+ downloadAllImages(markdown: string, pageTitle: string, pageId?: string): Promise<{
1225
1276
  content: string;
1226
1277
  downloads: ImageDownloadResult[];
1227
1278
  }>;
1228
- uploadLocalImage(localPath: string): Promise<ImageUploadResult>;
1229
- uploadAndAppendImages(pageId: string, images: ImageReference[]): Promise<ImageUploadResult[]>;
1279
+ uploadLocalImage(localPath: string, noteFolder?: string): Promise<ImageUploadResult>;
1280
+ uploadAndAppendImages(pageId: string, images: ImageReference[], notePath?: string): Promise<ImageUploadResult[]>;
1281
+ /**
1282
+ * 임베드 경로를 볼트 실제 파일로 해석한다(F23). Obsidian 위키링크 임베드는 노트 옆
1283
+ * 파일을 파일명만으로 참조하는 게 관례라, 노트 폴더 → 볼트 루트 → attachments/ 순으로
1284
+ * 시도한다. 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
1285
+ */
1230
1286
  private readImageFromVault;
1231
1287
  private getContentTypeFromPath;
1232
1288
  downloadAllFiles(markdown: string, pageTitle: string): Promise<{
@@ -1237,6 +1293,16 @@ declare class ImageHandler {
1237
1293
  private downloadFile;
1238
1294
  private isNotionImageUrl;
1239
1295
  private isNotionFileUrl;
1296
+ /**
1297
+ * notion-hosted 파일 URL 하나를 로컬 첨부로 로컬라이즈한다(P3-A).
1298
+ * DB 행의 `files` 속성처럼 본문 밖에 있는 서명 URL 용 — 서명 URL 은 약 1시간 뒤
1299
+ * 만료되어 frontmatter 에 남으면 깨진 링크가 되고, push 로 되밀면 Notion 원본이
1300
+ * external(만료 URL)로 오염된다(실측). notion-hosted 가 아닌 URL(사용자가 넣은
1301
+ * 진짜 외부 링크)은 다운로드 대상이 아니므로 null 을 반환한다.
1302
+ *
1303
+ * @returns 로컬 첨부 경로(`attachments/...`), 다운로드 대상이 아니거나 실패 시 null
1304
+ */
1305
+ localizeNotionFileUrl(url: string, caption: string): Promise<string | null>;
1240
1306
  private guessExtension;
1241
1307
  private guessFileExtension;
1242
1308
  }
@@ -1260,6 +1326,13 @@ declare class FileHandler {
1260
1326
  private readonly sema;
1261
1327
  constructor(vaultFs: VaultFS, notionClient: NotionClient, stateDb: IStateDB, concurrency?: number);
1262
1328
  pushFilesForFolder(folderPageId: string, folderPath: string): Promise<FileUploadResult[]>;
1329
+ /**
1330
+ * 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 제외한다.
1331
+ * node 측 VaultFS 는 과거 `.base` 를 워커에서 빼는 식으로 우회했지만, Obsidian
1332
+ * 어댑터는 모든 파일을 반환하므로 플러그인 push 가 사이드카를 Notion 첨부로
1333
+ * 오염시켰다. 소비자인 여기서 일괄 차단해 구현체 간 동작을 통일한다.
1334
+ */
1335
+ private listUploadableFiles;
1263
1336
  pushAllFiles(): Promise<FileUploadResult[]>;
1264
1337
  downloadFileBlock(url: string, filename: string, targetFolder: string): Promise<FileDownloadResult>;
1265
1338
  downloadFileBlocks(blocks: Array<{
@@ -1295,6 +1368,12 @@ interface DatabaseSyncResult {
1295
1368
  * 엉뚱한 행을 골라 정작 바뀐 행의 relation UUID 를 영영 해소하지 못했다(M3). push 경로는 항상 빈 배열.
1296
1369
  */
1297
1370
  writtenPaths: string[];
1371
+ /**
1372
+ * F25: 이 config 가 linked view 컨테이너로 판정된 경우 data source 원본 database id.
1373
+ * 행 처리는 원본 config 에 양보하고 건너뛰었으므로, 호출처(디스커버리 캐시)는 이 config
1374
+ * 를 제거하고 linked 매핑을 기록해야 다음 pull 부터 이중 방문 자체가 사라진다.
1375
+ */
1376
+ linkedOriginalDbId?: string;
1298
1377
  }
1299
1378
  declare class DatabaseSyncer {
1300
1379
  private readonly config;
@@ -1306,10 +1385,28 @@ declare class DatabaseSyncer {
1306
1385
  private readonly propertyMapper;
1307
1386
  private readonly baseFileGenerator;
1308
1387
  private readonly sidecarGenerator;
1388
+ /**
1389
+ * 이번 프로세스가 실제로 기록한 .base 경로/DB 제목 (databaseId nohyph → info).
1390
+ * placeholder 임베드 재작성(F22)의 SSOT — 폴더명(하이픈 새니타이즈)과 .base 파일명
1391
+ * (sanitizeFileName: 공백·점 보존)은 규칙이 달라 localFolder 로 추측한 경로는 깨진
1392
+ * 임베드가 된다. generateBaseFile 은 매 pull 모든 DB 에 대해 실행되므로 pull 종료
1393
+ * 시점에는 성공한 DB 전체가 채워져 있다.
1394
+ */
1395
+ readonly baseFileInfo: Map<string, {
1396
+ basePath: string;
1397
+ title: string;
1398
+ }>;
1309
1399
  constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, pipeline: ConversionPipeline, imageHandler: ImageHandler);
1310
1400
  pullAll(): Promise<DatabaseSyncResult>;
1311
1401
  pushAll(): Promise<DatabaseSyncResult>;
1312
- pullDatabase(dbConfig: DatabaseSyncConfig): Promise<DatabaseSyncResult>;
1402
+ pullDatabase(dbConfig: DatabaseSyncConfig, opts?: {
1403
+ /**
1404
+ * F25: database id(하이픈 유무 무관) → 그 DB 행이 사는 로컬 폴더. 디스커버리 캐시가
1405
+ * 아는 DB 만 반환하고 모르면 null. linked view 컨테이너 판정 시 원본 폴더를 아는
1406
+ * 경우에만 행 소유를 양보한다 — 원본이 미발견이면 이 컨테이너가 유일한 접근 통로다.
1407
+ */
1408
+ resolveDbFolder?: (databaseId: string) => string | null;
1409
+ }): Promise<DatabaseSyncResult>;
1313
1410
  private pullDatabaseViews;
1314
1411
  private generateBaseFile;
1315
1412
  /**
@@ -1324,6 +1421,15 @@ declare class DatabaseSyncer {
1324
1421
  * 결정적 직렬화라 동일 DB 상태 → 동일 바이트 → 멱등(drift/churn 0).
1325
1422
  */
1326
1423
  private generateSidecar;
1424
+ /**
1425
+ * DB 행 `files` 속성의 notion-hosted 서명 URL 을 로컬 첨부로 로컬라이즈한다(P3-A).
1426
+ * 서명 URL 은 약 1시간 뒤 만료되어 frontmatter 에 남으면 깨진 링크가 되고, push 로
1427
+ * 되밀면 Notion 원본이 만료 URL 의 external 파일로 오염된다(실측). 다운로드 성공분은
1428
+ * `[[attachments/..]]` 위키링크로 대체한다 — Bases 카드 `image:` 는 위키링크 스칼라를
1429
+ * 렌더하므로 갤러리 커버도 유지된다. 원본 파일명은 raw 속성의 `name` 에서 취한다
1430
+ * (extractValue 는 URL 만 남긴다). 사용자가 넣은 진짜 외부 URL 은 그대로 둔다.
1431
+ */
1432
+ private localizeFileProperties;
1327
1433
  private pullDatabasePage;
1328
1434
  private pushDatabase;
1329
1435
  /**
@@ -1532,11 +1638,32 @@ declare class FrontmatterGenerator implements Processor {
1532
1638
  process(input: ProcessorInput): ProcessorOutput;
1533
1639
  }
1534
1640
 
1641
+ /**
1642
+ * 다른 복원기가 소비하지 못하고 실제로 유실된 보존 마커를 재삽입하는 최후 보루.
1643
+ *
1644
+ * 핵심 전제(F28 교훈): DB 에 저장된 startIndex 는 **push 시점 문자열**의 절대
1645
+ * 오프셋이라 pull 산출물에서는 무의미하다 — 프론트매터 앞·단어 한가운데에
1646
+ * 마커를 박아 파일을 파손시켰다. 재삽입은 반드시
1647
+ * 1) 이미 의미적으로 복원된 마커는 건너뛰고(위키링크 → `[[...]]` 존재 등),
1648
+ * 2) 텍스트 앵커(__anchor)를 1차 위치 수단으로 쓰며,
1649
+ * 3) 프론트매터 뒤·줄 경계에만, 마커 단독 줄로 삽입한다.
1650
+ *
1651
+ * 이중 모드: `__anchor` 키가 없는 마커는 구버전 DB blob(수집기가 앵커를 안 남기던
1652
+ * 시절)이다. 이들은 구계약 — "변환이 마커만 제거했다면 startIndex 오프셋 원문 삽입이
1653
+ * 원본을 정확히 재구성한다" — 을 유지하되 프론트매터 앞 삽입만 금지한다. 위해한
1654
+ * 레거시 사례(빈 params 껍데기, 이미 복원된 위키링크)는 render-null 폐기와 의미
1655
+ * 복원 검사가 이미 걸러낸다.
1656
+ */
1535
1657
  declare class PreserveMarkerInjector implements Processor {
1536
1658
  readonly name = "PreserveMarkerInjector";
1537
1659
  readonly order = 100;
1538
1660
  process(input: ProcessorInput): ProcessorOutput;
1539
- private formatMarker;
1661
+ /** 마커의 본문 표현. 복원 불가능한(빈) 마커는 null — 삽입하지 않고 폐기한다. */
1662
+ private renderMarker;
1663
+ /** 마커가 이미 원래 문법으로 복원돼 있으면 재삽입은 중복·오염이다. */
1664
+ private isSemanticallyRestored;
1665
+ /** 삽입 위치: 앵커 → startIndex(줄 경계 보정) 순. 항상 프론트매터 뒤. */
1666
+ private findInsertionPoint;
1540
1667
  }
1541
1668
 
1542
1669
  declare class LocalImageRestorer implements Processor {
@@ -1756,9 +1883,10 @@ type BasesViewType = "table" | "cards" | "list";
1756
1883
  * Notion 뷰 타입 → Obsidian Bases 뷰 타입 매핑(SSOT).
1757
1884
  *
1758
1885
  * `null` = Bases 에 대응 뷰가 없어 `.base` 로 표현 불가. 이 경우 해당 뷰는 `.base` 에서
1759
- * 누락되므로, 사이드카(`<db>.notion.json`)가 원본 뷰 설정을 무손실 보존한다
1760
- * (Notion API 는 뷰 생성/수정을 지원하지 않아 뷰는 pull-authoritative — push 로 되돌릴 수
1761
- * 없으므로 "조용한 유실 금지"는 곧 "보존 + 정직한 degrade 리포트"를 의미한다).
1886
+ * 누락되므로, 사이드카(`<db>.notion.json`)가 원본 뷰 설정을 무손실 보존한다.
1887
+ * 뷰는 현재 pull-authoritative — Notion API 가 2026-03-19 뷰 생성/수정/삭제를 열었지만
1888
+ * 본 프로젝트는 아직 뷰 write-back 을 구현하지 않았다(로드맵 항목). push 로 되돌릴 수
1889
+ * 없는 동안 "조용한 유실 금지"는 곧 "보존 + 정직한 degrade 리포트"를 의미한다.
1762
1890
  */
1763
1891
  declare const NOTION_TO_BASES_VIEW: Record<string, BasesViewType | null>;
1764
1892
  /** Notion 뷰 타입의 Bases 대응 타입을 돌려준다. 미지원/미상 타입은 `null`. */