@im-nobsidian/core 0.3.0 → 0.3.2
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 +715 -44
- package/dist/index.js +3337 -829
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -142,6 +142,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
142
142
|
mediaRetryBaseMs: z.ZodDefault<z.ZodNumber>;
|
|
143
143
|
fileConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
144
144
|
maxFileSizeBytes: z.ZodDefault<z.ZodNumber>;
|
|
145
|
+
mediaDownloadTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
146
|
+
itemTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
145
147
|
}, "strip", z.ZodTypeAny, {
|
|
146
148
|
concurrency: number;
|
|
147
149
|
timeoutMs: number;
|
|
@@ -157,6 +159,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
157
159
|
mediaRetryBaseMs: number;
|
|
158
160
|
fileConcurrency: number;
|
|
159
161
|
maxFileSizeBytes: number;
|
|
162
|
+
mediaDownloadTimeoutMs: number;
|
|
163
|
+
itemTimeoutMs: number;
|
|
160
164
|
}, {
|
|
161
165
|
concurrency?: number | undefined;
|
|
162
166
|
timeoutMs?: number | undefined;
|
|
@@ -172,6 +176,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
172
176
|
mediaRetryBaseMs?: number | undefined;
|
|
173
177
|
fileConcurrency?: number | undefined;
|
|
174
178
|
maxFileSizeBytes?: number | undefined;
|
|
179
|
+
mediaDownloadTimeoutMs?: number | undefined;
|
|
180
|
+
itemTimeoutMs?: number | undefined;
|
|
175
181
|
}>;
|
|
176
182
|
}, "strip", z.ZodTypeAny, {
|
|
177
183
|
version: 1;
|
|
@@ -223,6 +229,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
223
229
|
mediaRetryBaseMs: number;
|
|
224
230
|
fileConcurrency: number;
|
|
225
231
|
maxFileSizeBytes: number;
|
|
232
|
+
mediaDownloadTimeoutMs: number;
|
|
233
|
+
itemTimeoutMs: number;
|
|
226
234
|
};
|
|
227
235
|
}, {
|
|
228
236
|
version: 1;
|
|
@@ -274,6 +282,8 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
274
282
|
mediaRetryBaseMs?: number | undefined;
|
|
275
283
|
fileConcurrency?: number | undefined;
|
|
276
284
|
maxFileSizeBytes?: number | undefined;
|
|
285
|
+
mediaDownloadTimeoutMs?: number | undefined;
|
|
286
|
+
itemTimeoutMs?: number | undefined;
|
|
277
287
|
};
|
|
278
288
|
}>;
|
|
279
289
|
type Config = z.infer<typeof ConfigSchema>;
|
|
@@ -353,6 +363,12 @@ interface PullResult {
|
|
|
353
363
|
readonly created: number;
|
|
354
364
|
readonly updated: number;
|
|
355
365
|
readonly deleted: number;
|
|
366
|
+
/**
|
|
367
|
+
* 로컬에서 사라졌다가 리모트 원본으로 되살린 파일 수.
|
|
368
|
+
* updated 와 분리해 보고한다 — "수정 N건" 에 섞이면 사용자는 자기 볼트에서
|
|
369
|
+
* 파일이 없어졌다가 복구됐다는 사실 자체를 알 수 없다.
|
|
370
|
+
*/
|
|
371
|
+
readonly restored: number;
|
|
356
372
|
readonly conflicts: Conflict[];
|
|
357
373
|
readonly writtenPaths: string[];
|
|
358
374
|
readonly failed: FailedOperation[];
|
|
@@ -485,6 +501,37 @@ interface DatabaseViewsConfig {
|
|
|
485
501
|
readonly lastSynced: string;
|
|
486
502
|
readonly views: ViewConfig[];
|
|
487
503
|
}
|
|
504
|
+
/** select/multi_select/status 의 선택지 하나. */
|
|
505
|
+
interface BasePropertyOption {
|
|
506
|
+
readonly name: string;
|
|
507
|
+
readonly color?: string;
|
|
508
|
+
/**
|
|
509
|
+
* Notion 내부 옵션 id. 사람이 보는 산출물에는 절대 나가지 않지만,
|
|
510
|
+
* status 그룹(`BaseStatusGroup.optionIds`)을 옵션명으로 되돌리는 유일한 열쇠다 —
|
|
511
|
+
* 뷰 필터가 옵션명 대신 그룹명으로 오기 때문에(예: `["To-do"]`) 이게 없으면
|
|
512
|
+
* 필터를 옮길 수 없다.
|
|
513
|
+
*/
|
|
514
|
+
readonly id?: string;
|
|
515
|
+
}
|
|
516
|
+
/** status 속성의 그룹(To-do/In progress/Complete). 소속 옵션을 id 로 가리킨다. */
|
|
517
|
+
interface BaseStatusGroup {
|
|
518
|
+
readonly name: string;
|
|
519
|
+
readonly color?: string;
|
|
520
|
+
readonly optionIds?: string[];
|
|
521
|
+
}
|
|
522
|
+
/**
|
|
523
|
+
* Notion DB 속성 스키마 — `.base` 생성·사이드카·필터 번역이 함께 쓰는 SSOT.
|
|
524
|
+
*
|
|
525
|
+
* 같은 모양을 세 군데(클라이언트 반환형·생성기 입력형)에 따로 적어 두면 한쪽만
|
|
526
|
+
* 넓혔을 때 조용히 어긋난다. 실제로 옵션 `id` 가 클라이언트에서만 빠져 있어
|
|
527
|
+
* status 그룹 필터를 옮길 수 없었다.
|
|
528
|
+
*/
|
|
529
|
+
interface BasePropertySchema {
|
|
530
|
+
readonly id: string;
|
|
531
|
+
readonly type: string;
|
|
532
|
+
readonly options?: BasePropertyOption[];
|
|
533
|
+
readonly groups?: BaseStatusGroup[];
|
|
534
|
+
}
|
|
488
535
|
interface PageCover {
|
|
489
536
|
readonly type: "file" | "external";
|
|
490
537
|
readonly url: string;
|
|
@@ -730,33 +777,46 @@ declare class NotionClient {
|
|
|
730
777
|
private fetchDatabaseModern;
|
|
731
778
|
getDatabaseTitle(databaseId: string): Promise<string>;
|
|
732
779
|
/**
|
|
733
|
-
* 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지)
|
|
780
|
+
* 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지) 판별한다.
|
|
734
781
|
* 신 모델(2025-09-03)에서 링크드 DB·미공유 데이터 소스·삭제 DB 는 `data_sources` 가
|
|
735
782
|
* 비어, 행 조회(`dataSources.query`)가 404 로 실패하고 그 전에 생성된 빈 폴더/`.base` 만
|
|
736
783
|
* 남긴다. 발견 단계에서 미리 걸러 **빈 폴더/.base 오염 + 매 pull 의 404 노이즈**를 차단한다.
|
|
737
784
|
* 제목은 함께 반환해 호출처가 추가 조회 없이 폴더명을 잡게 한다.
|
|
785
|
+
*
|
|
786
|
+
* F25: 원본이 같은 공유 범위에 있으면 linked view 컨테이너도 `data_sources` 가 **채워져**
|
|
787
|
+
* 온다(실측 — 워크스페이스 8건). 이때 data source 의 parent database 는 원본이므로,
|
|
788
|
+
* 첫 소스의 parent 가 자신이 아니면 `linkedOriginalDbId` 로 알려 컨테이너가 원본과 같은
|
|
789
|
+
* 행 집합을 이중 pull(폴더 릴레이 재배치 churn)하는 것을 발견 단계에서 차단한다.
|
|
738
790
|
*/
|
|
739
791
|
getDatabaseSyncability(databaseId: string): Promise<{
|
|
740
792
|
title: string;
|
|
741
793
|
queryable: boolean;
|
|
794
|
+
linkedOriginalDbId?: string;
|
|
742
795
|
}>;
|
|
796
|
+
/**
|
|
797
|
+
* linked database view 컨테이너를 원본 DB 로 해소한다.
|
|
798
|
+
* 컨테이너 자체는 `data_sources` 가 비어 행 조회가 불가능하지만(getDatabaseSyncability
|
|
799
|
+
* queryable=false), Views API 의 뷰 상세에는 원본 `data_source_id` 가 실리고 그 data source
|
|
800
|
+
* 의 parent 가 원본 database 다. 해소 실패(뷰 없음/권한 없음/자기참조)는 null — 호출처가
|
|
801
|
+
* 기존대로 접근 불가 처리한다.
|
|
802
|
+
*/
|
|
803
|
+
resolveLinkedDatabase(databaseId: string): Promise<{
|
|
804
|
+
originalDbId: string;
|
|
805
|
+
viewName: string;
|
|
806
|
+
} | null>;
|
|
743
807
|
getDatabaseSchema(databaseId: string): Promise<Record<string, {
|
|
744
808
|
id: string;
|
|
745
809
|
type: string;
|
|
746
810
|
}>>;
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
color?: string;
|
|
757
|
-
optionIds?: string[];
|
|
758
|
-
}>;
|
|
759
|
-
}>>;
|
|
811
|
+
/**
|
|
812
|
+
* DB 속성 스키마 전체(선택지·status 그룹 포함).
|
|
813
|
+
*
|
|
814
|
+
* 옵션 `id` 까지 그대로 실어 보낸다 — 산출물에는 안 나가지만, status 뷰 필터가
|
|
815
|
+
* 옵션명이 아니라 **그룹명**(`To-do` 등)으로 오기 때문에 `groups[].optionIds` 를
|
|
816
|
+
* 옵션명으로 되돌리려면 이 id 가 있어야 한다. 예전엔 여기서 id 를 떨궈,
|
|
817
|
+
* 그룹으로 필터링된 뷰를 Bases 로 옮길 방법이 아예 없었다.
|
|
818
|
+
*/
|
|
819
|
+
getDatabaseSchemaFull(databaseId: string): Promise<Record<string, BasePropertySchema>>;
|
|
760
820
|
/** 특정 data source 1개를 직접 쿼리한다(저수준). 다중 data source 순회의 빌딩블록. */
|
|
761
821
|
private queryDataSource;
|
|
762
822
|
/**
|
|
@@ -802,9 +862,25 @@ declare class NotionClient {
|
|
|
802
862
|
fetchAllChildren(blockId: string): Promise<BlockObjectResponse[]>;
|
|
803
863
|
private static readonly CONTAINER_BLOCK_TYPES;
|
|
804
864
|
fetchAllChildrenDeep(blockId: string): Promise<BlockObjectResponse[]>;
|
|
805
|
-
|
|
865
|
+
/**
|
|
866
|
+
* 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로 뒤**에 삽입한다
|
|
867
|
+
* (기본은 맨 끝). 자리표시자를 실제 미디어 블록으로 제자리 교체할 때 필요하다.
|
|
868
|
+
*
|
|
869
|
+
* 실측(2025-09-03): position 삽입 응답의 `results` 는 방금 넣은 블록뿐 아니라
|
|
870
|
+
* 삽입 지점 이후의 형제까지 함께 돌려준다. 앞에서부터 batch 길이만큼이 새 블록이므로
|
|
871
|
+
* 배치를 이어 붙일 때는 그 마지막 id 를 다음 기준점으로 삼는다.
|
|
872
|
+
*/
|
|
873
|
+
appendChildren(blockId: string, children: unknown[], options?: {
|
|
874
|
+
readonly after?: string;
|
|
875
|
+
}): Promise<string[]>;
|
|
806
876
|
deleteBlock(blockId: string): Promise<void>;
|
|
877
|
+
/** single_part 업로드 상한 (Notion API 규격 20MB) */
|
|
878
|
+
private static readonly SINGLE_PART_MAX_BYTES;
|
|
879
|
+
/** multi_part 파트 크기 — 규격상 마지막 파트 제외 5~20MB, 10MB 고정 사용 */
|
|
880
|
+
private static readonly MULTI_PART_CHUNK_BYTES;
|
|
807
881
|
uploadFile(fileData: Blob, filename: string, contentType: string): Promise<string>;
|
|
882
|
+
/** 20MB 초과 파일의 multi_part 업로드 — part_number 는 API 규격상 문자열("1"부터) */
|
|
883
|
+
private uploadFileMultiPart;
|
|
808
884
|
search(params: {
|
|
809
885
|
query?: string;
|
|
810
886
|
filter?: {
|
|
@@ -831,6 +907,8 @@ declare class NotionClient {
|
|
|
831
907
|
*
|
|
832
908
|
* @param opts.deadlineMs `Date.now()` 기준 마감 시각. 각 페이지 처리 전 초과를 검사해
|
|
833
909
|
* 초과 시 {@link DiscoveryTooLargeError}를 던진다(대규모 서브트리 → search 폴백 유도).
|
|
910
|
+
* **그때까지 찾은 페이지는 에러의 `partial` 에 실어 보낸다** — 버리면 호출측이 두
|
|
911
|
+
* 경로를 경합시키게 되고, 어느 쪽이 이기는지를 벽시계가 정하게 된다(R12-A).
|
|
834
912
|
* 미지정 시 무제한(기존 동작 유지 — 테스트 mock 경로는 영향 없음).
|
|
835
913
|
*/
|
|
836
914
|
getChildPagesRecursive(parentId: string, opts?: {
|
|
@@ -869,8 +947,28 @@ declare class NotionClient {
|
|
|
869
947
|
setWikilinkResolver(resolver: WikilinkResolver$1): void;
|
|
870
948
|
extractProperties(page: PageObjectResponse): Record<string, unknown>;
|
|
871
949
|
private lastRequestTime;
|
|
950
|
+
/**
|
|
951
|
+
* 429 이후 **클라이언트 전체**가 쉬어야 하는 시각(epoch ms).
|
|
952
|
+
*
|
|
953
|
+
* rate limit 은 요청 하나의 문제가 아니라 워크스페이스 전체에 걸린 신호다. 그러니
|
|
954
|
+
* 429 를 맞은 요청만 기다리게 두면 나머지 슬롯이 곧바로 다시 두드려 429 를 재생산한다.
|
|
955
|
+
* 슬롯을 붙잡아 남을 막는 대신, 다 같이 보는 이 게이트로 쉬게 한다.
|
|
956
|
+
*/
|
|
957
|
+
private cooldownUntil;
|
|
958
|
+
/**
|
|
959
|
+
* 요청 1건을 rate limit 슬롯 안에서 실행하고, 재시도 가능한 실패는 백오프 후 다시 시도한다.
|
|
960
|
+
*
|
|
961
|
+
* 백오프 대기는 **슬롯을 놓은 뒤** 한다. 예전 구현은 `sema.acquire()` 안에서 재시도
|
|
962
|
+
* 전체(최대 60초 x 5회)를 돌려, 불운한 요청 하나가 동시성 한 칸을 최대 5분 점유했다.
|
|
963
|
+
* 동시성 기본값이 3이므로 그런 요청 3건이면 클라이언트 전체가 멈춘다. 게다가 재시도는
|
|
964
|
+
* 로그를 한 줄도 남기지 않아, 겉보기에는 죽은 프로세스와 구분되지 않았다 — 실제로
|
|
965
|
+
* 268페이지 pull 이 진행 로그가 멈춘 채 수 분씩 정지하는 것으로 나타났다(R9).
|
|
966
|
+
* 그래서 (1) 대기 중에는 슬롯을 반납하고, (2) 429 는 {@link cooldownUntil} 전역
|
|
967
|
+
* 게이트로 함께 쉬고, (3) 모든 재시도를 경고로 남긴다.
|
|
968
|
+
*/
|
|
872
969
|
private withRateLimit;
|
|
873
|
-
|
|
970
|
+
/** 슬롯을 쥔 뒤의 발사 대기 — 전역 쿨다운(429)과 최소 요청 간격을 함께 지킨다. */
|
|
971
|
+
private waitForTurn;
|
|
874
972
|
}
|
|
875
973
|
|
|
876
974
|
interface RichTextSegment {
|
|
@@ -977,6 +1075,145 @@ declare class NotionBlockBuilder {
|
|
|
977
1075
|
static table(rows: string[][], hasColumnHeader?: boolean, hasRowHeader?: boolean): NotionBlock;
|
|
978
1076
|
}
|
|
979
1077
|
|
|
1078
|
+
/**
|
|
1079
|
+
* DB 완결성 게이트 — "원격에 있는 행이 볼트에도 전부 있는가"를 대조한다.
|
|
1080
|
+
*
|
|
1081
|
+
* ## 왜 필요한가 (R11)
|
|
1082
|
+
*
|
|
1083
|
+
* 기존 게이트는 전부 **멱등성**(같은 입력에 같은 출력)을 본다 — 해시 불일치 0, repull
|
|
1084
|
+
* 바이트 동일, churn 0. 그런데 멱등성은 *체계적 미발견*을 구조적으로 잡지 못한다:
|
|
1085
|
+
* 디스커버리가 매번 **같은 행을 똑같이 놓치면** 두 번째 pull 도 첫 번째와 동일한
|
|
1086
|
+
* 결과를 내므로 churn 은 0 이고 해시도 전부 일치한다. 실제로 2026-07-17 pull 은 DB 행
|
|
1087
|
+
* 296개를 침묵 유실한 채 `audit-vault 결함 0 · 해시 불일치 0 · byte-identical repull`
|
|
1088
|
+
* 을 전부 통과했다. "안 바뀐다"와 "빠짐없다"는 다른 성질이고, 후자를 보는 게이트가
|
|
1089
|
+
* 없었던 것이다.
|
|
1090
|
+
*
|
|
1091
|
+
* 이 모듈은 후자만 본다. 원격 행 id 집합과 볼트가 추적 중인 db-row id 집합을 직접
|
|
1092
|
+
* 대조해, 어느 쪽에만 있는지를 센다. 카운트가 아니라 **집합**을 비교하는 이유는
|
|
1093
|
+
* 카운트만 보면 "1건 미발견 + 1건 잔재"가 상쇄돼 통과하기 때문이다.
|
|
1094
|
+
*
|
|
1095
|
+
* ## 범위와 한계
|
|
1096
|
+
*
|
|
1097
|
+
* 볼트가 **아는** database 에 대해 "그 DB 의 모든 행이 있는가"를 보장한다. database
|
|
1098
|
+
* 자체를 통째로 못 찾은 경우는 볼트에 흔적이 없어 여기서 알 수 없으므로, 호출부가
|
|
1099
|
+
* 설정/디스커버리가 아는 database id 를 {@link CompletenessOptions.databaseIds} 로
|
|
1100
|
+
* 넘겨 그 구멍까지 덮는다(행 0개로 대조되어 미발견이 그대로 드러난다).
|
|
1101
|
+
*
|
|
1102
|
+
* 원격 열거는 {@link CompletenessRemoteSource.queryAllDatabasePages} — 전 data source
|
|
1103
|
+
* 를 순회하는 SSOT — 하나만 쓴다. 여기서 별도 쿼리를 짜면 검증기가 피검증 대상과 다른
|
|
1104
|
+
* 열거 규칙을 갖게 돼, 게이트가 통과해도 실제 pull 은 놓치는 상황이 생긴다.
|
|
1105
|
+
*/
|
|
1106
|
+
/** 원격 행 열거 — {@link NotionClient} 가 그대로 만족한다(전 data source 순회 SSOT). */
|
|
1107
|
+
interface CompletenessRemoteSource {
|
|
1108
|
+
queryAllDatabasePages(databaseId: string, filter?: unknown): Promise<Array<{
|
|
1109
|
+
id: string;
|
|
1110
|
+
}>>;
|
|
1111
|
+
}
|
|
1112
|
+
/** 볼트가 추적 중인 레코드 열거 — {@link IStateDB} 가 그대로 만족한다. */
|
|
1113
|
+
interface CompletenessLocalSource {
|
|
1114
|
+
getAll(): ReadonlyArray<{
|
|
1115
|
+
readonly notionPageId: string | null;
|
|
1116
|
+
readonly notionParentId: string | null;
|
|
1117
|
+
readonly fileType: string;
|
|
1118
|
+
}>;
|
|
1119
|
+
}
|
|
1120
|
+
/** database 1개의 대조 결과. */
|
|
1121
|
+
interface DatabaseCompleteness {
|
|
1122
|
+
/** 대조 대상 database id(입력 표기 그대로). */
|
|
1123
|
+
readonly databaseId: string;
|
|
1124
|
+
/** 원격 행 수(전 data source 합·page id 디듀프 후). */
|
|
1125
|
+
readonly remoteRows: number;
|
|
1126
|
+
/** 볼트가 이 DB 소속으로 추적 중인 db-row 수. */
|
|
1127
|
+
readonly localRows: number;
|
|
1128
|
+
/** 원격에만 있는 행 id — **침묵 미발견**. 비어야 정상. */
|
|
1129
|
+
readonly missingIds: readonly string[];
|
|
1130
|
+
/** 볼트에만 있는 행 id — 원격 삭제 잔재/부모 오배치. 비어야 정상. */
|
|
1131
|
+
readonly extraIds: readonly string[];
|
|
1132
|
+
}
|
|
1133
|
+
/** 원격 열거가 실패한 database — 결과를 "이상 없음"으로 접지 않기 위해 따로 싣는다. */
|
|
1134
|
+
interface CompletenessFailure {
|
|
1135
|
+
readonly databaseId: string;
|
|
1136
|
+
readonly error: string;
|
|
1137
|
+
}
|
|
1138
|
+
/** {@link verifyDatabaseCompleteness} 결과. */
|
|
1139
|
+
interface CompletenessReport {
|
|
1140
|
+
/** database 별 대조 결과(입력 순서). */
|
|
1141
|
+
readonly databases: readonly DatabaseCompleteness[];
|
|
1142
|
+
/** 원격 열거 실패 목록. 하나라도 있으면 `complete` 는 false. */
|
|
1143
|
+
readonly failures: readonly CompletenessFailure[];
|
|
1144
|
+
/** 대조에 성공한 database 들의 원격 행 총합. */
|
|
1145
|
+
readonly remoteTotal: number;
|
|
1146
|
+
/** 같은 database 들의 볼트 db-row 총합. */
|
|
1147
|
+
readonly localTotal: number;
|
|
1148
|
+
/** 미발견·잔재·조회 실패가 모두 0. */
|
|
1149
|
+
readonly complete: boolean;
|
|
1150
|
+
}
|
|
1151
|
+
interface CompletenessOptions {
|
|
1152
|
+
/**
|
|
1153
|
+
* 볼트에 행이 하나도 없어도 반드시 대조할 database id(설정 `notion.databases[]` ·
|
|
1154
|
+
* 디스커버리 캐시 등). 통째로 미발견된 DB 를 드러내는 유일한 경로다.
|
|
1155
|
+
*/
|
|
1156
|
+
readonly databaseIds?: readonly string[];
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* 원격 행 집합과 볼트 db-row 집합을 database 별로 대조한다.
|
|
1160
|
+
*
|
|
1161
|
+
* 대조 대상 = 볼트가 db-row 를 추적 중인 모든 database ∪
|
|
1162
|
+
* {@link CompletenessOptions.databaseIds}. 한 database 의 열거가 실패해도 나머지는
|
|
1163
|
+
* 계속 대조하고, 실패는 {@link CompletenessReport.failures} 로 비침묵 보고한다.
|
|
1164
|
+
*/
|
|
1165
|
+
declare function verifyDatabaseCompleteness(remote: CompletenessRemoteSource, local: CompletenessLocalSource, options?: CompletenessOptions): Promise<CompletenessReport>;
|
|
1166
|
+
/**
|
|
1167
|
+
* 원격 페이지 열거 — {@link NotionClient} 가 그대로 만족한다.
|
|
1168
|
+
*
|
|
1169
|
+
* DB 쪽과 달리 여기서는 **일부러 pull 과 다른 열거를 쓴다**. DB 행은 열거 방법이 하나뿐이라
|
|
1170
|
+
* 검증기가 다른 규칙을 쓰면 게이트가 통과해도 pull 은 놓치는 상황이 생기지만, 페이지는
|
|
1171
|
+
* 열거 경로가 둘(직접 순회 · search)이고 **둘이 같은 집합을 내지 않는다**는 게 애초의
|
|
1172
|
+
* 결함이다(R12-A). pull 이 어느 경로로 돌았든 독립된 두 번째 열거와 대조해야 의미가 있다.
|
|
1173
|
+
*/
|
|
1174
|
+
interface PageCompletenessRemoteSource {
|
|
1175
|
+
getPagesUnderRootViaSearch(rootId: string): Promise<Array<{
|
|
1176
|
+
id: string;
|
|
1177
|
+
}>>;
|
|
1178
|
+
}
|
|
1179
|
+
/** {@link verifyPageCompleteness} 결과. */
|
|
1180
|
+
interface PageCompletenessReport {
|
|
1181
|
+
/** 원격(search 열거) root 하위 페이지 수. */
|
|
1182
|
+
readonly remotePages: number;
|
|
1183
|
+
/** 볼트가 추적 중인 페이지 수(db-row 제외). */
|
|
1184
|
+
readonly vaultPages: number;
|
|
1185
|
+
/** 원격에만 있는 페이지 id — **침묵 미발견**. 비어야 정상. */
|
|
1186
|
+
readonly missingIds: readonly string[];
|
|
1187
|
+
/**
|
|
1188
|
+
* 볼트에만 있는 페이지 id — **정보성**(판정에 쓰지 않는다).
|
|
1189
|
+
*
|
|
1190
|
+
* DB 행과 달리 페이지는 볼트에만 있는 게 정상일 수 있다: 아직 push 하지 않은 로컬 노트,
|
|
1191
|
+
* root 페이지 자신(search 열거는 root 를 제외한다), search 색인이 아직 못 따라온 신규
|
|
1192
|
+
* 페이지. 이걸 실패로 접으면 게이트가 거짓 적색을 내며 신뢰를 잃는다.
|
|
1193
|
+
*/
|
|
1194
|
+
readonly localOnlyIds: readonly string[];
|
|
1195
|
+
/** 열거 실패 사유. 있으면 `complete` 는 false(실패를 통과로 접지 않는다). */
|
|
1196
|
+
readonly error?: string;
|
|
1197
|
+
/** 미발견 0 이고 열거도 성공. */
|
|
1198
|
+
readonly complete: boolean;
|
|
1199
|
+
}
|
|
1200
|
+
/** {@link SyncOrchestrator.verifyCompleteness} 결과 — DB 와 페이지 대조를 한 판정으로 묶는다. */
|
|
1201
|
+
interface VaultCompletenessReport {
|
|
1202
|
+
readonly databases: CompletenessReport;
|
|
1203
|
+
/** 페이지 모드에서만 채워진다. DB 모드에는 root 서브트리가 없어 `null`. */
|
|
1204
|
+
readonly pages: PageCompletenessReport | null;
|
|
1205
|
+
/** 두 대조가 모두 통과. */
|
|
1206
|
+
readonly complete: boolean;
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* 원격 root 하위 페이지 집합과 볼트가 추적 중인 페이지 집합을 대조한다.
|
|
1210
|
+
*
|
|
1211
|
+
* 볼트 쪽은 `fileType !== "db-row"` 로 고른다 — 페이지형 타입을 열거하면
|
|
1212
|
+
* (`file`·`folder-note`·`folder-only`) 새 타입이 생겼을 때 한쪽만 낡아 조용히 게이트를
|
|
1213
|
+
* 빠져나간다. "행이 아닌 것은 전부 페이지"가 이 대조가 실제로 뜻하는 바다.
|
|
1214
|
+
*/
|
|
1215
|
+
declare function verifyPageCompleteness(remote: PageCompletenessRemoteSource, local: CompletenessLocalSource, rootPageId: string): Promise<PageCompletenessReport>;
|
|
1216
|
+
|
|
980
1217
|
interface FileInfo {
|
|
981
1218
|
readonly path: string;
|
|
982
1219
|
readonly content: string;
|
|
@@ -1054,6 +1291,7 @@ declare class SyncOrchestrator {
|
|
|
1054
1291
|
private _pullFileCount;
|
|
1055
1292
|
private _inlineDbRefs;
|
|
1056
1293
|
private _childParentIds;
|
|
1294
|
+
private readonly claimedPaths;
|
|
1057
1295
|
private static readonly DISCOVERY_RECURSIVE_BUDGET_MS;
|
|
1058
1296
|
constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, customFetch?: typeof globalThis.fetch);
|
|
1059
1297
|
push(options?: PushOptions): Promise<PushResult>;
|
|
@@ -1061,12 +1299,33 @@ declare class SyncOrchestrator {
|
|
|
1061
1299
|
sync(options?: SyncOptions): Promise<SyncResult>;
|
|
1062
1300
|
status(): Promise<StatusResult>;
|
|
1063
1301
|
statusLocal(): Promise<StatusResult>;
|
|
1302
|
+
/**
|
|
1303
|
+
* 볼트 완결성 검증 — 원격에 있는 것이 볼트에 빠짐없이 있는지 대조한다(R11-B · R12-C).
|
|
1304
|
+
*
|
|
1305
|
+
* 기존 게이트(해시 일치·repull 바이트 동일·churn 0)는 전부 **멱등성**을 본다. 매번
|
|
1306
|
+
* 같은 것을 놓치는 체계적 미발견은 그 게이트를 전부 통과한다 — 실제로 296개 DB 행이
|
|
1307
|
+
* 그렇게 침묵 유실됐다. 여기서만 "빠짐없다"를 본다.
|
|
1308
|
+
*
|
|
1309
|
+
* 대조 대상 database id 는 이 클래스가 조립한다: 설정에 적힌 것 + 디스커버리 캐시가
|
|
1310
|
+
* 아는 것 + DB 모드의 루트 DB. `discovered_dbs` 메타 키를 아는 곳을 이 파일 하나로
|
|
1311
|
+
* 유지해, 호출부(CLI·E2E)가 캐시 표현을 각자 다시 해석하지 않게 한다.
|
|
1312
|
+
*
|
|
1313
|
+
* 페이지(R12-C)는 pull 이 쓴 경로가 아니라 **search 열거**로 대조한다. 페이지는 열거
|
|
1314
|
+
* 경로가 둘이고 둘이 같은 집합을 내지 않는 게 애초의 결함이므로(R12-A), 독립된 두 번째
|
|
1315
|
+
* 열거와 맞대 봐야 의미가 있다.
|
|
1316
|
+
*/
|
|
1317
|
+
verifyCompleteness(): Promise<VaultCompletenessReport>;
|
|
1064
1318
|
fetch(): Promise<{
|
|
1065
1319
|
newPages: number;
|
|
1066
1320
|
deletedPages: number;
|
|
1067
1321
|
modifiedPages: number;
|
|
1068
1322
|
duration: number;
|
|
1069
1323
|
}>;
|
|
1324
|
+
/**
|
|
1325
|
+
* @param options.fullRender 원격 본문을 pull 과 동일한 파이프라인으로 렌더할지.
|
|
1326
|
+
* 기본(false)은 화면 미리보기용 경량 렌더 — 첨부를 내려받지 않으므로 `status` 처럼
|
|
1327
|
+
* 읽기만 하는 경로가 쓴다. 볼트에 덮어쓸 본문이 필요한 해소 경로는 반드시 켠다.
|
|
1328
|
+
*/
|
|
1070
1329
|
private buildConflictsFromRecords;
|
|
1071
1330
|
private get isDatabaseMode();
|
|
1072
1331
|
private discoverChildDatabases;
|
|
@@ -1080,11 +1339,59 @@ declare class SyncOrchestrator {
|
|
|
1080
1339
|
private buildDiscoveredDbConfig;
|
|
1081
1340
|
/** 접근 불가 DB denylist 를 상태 메타에서 로드한다(하이픈 정규화). */
|
|
1082
1341
|
private loadInaccessibleDbIds;
|
|
1342
|
+
/** linked view 컨테이너 → 원본 DB 매핑을 상태 메타에서 로드한다(nohyph → nohyph). */
|
|
1343
|
+
private loadLinkedDbMap;
|
|
1083
1344
|
private pullDiscoveredDatabases;
|
|
1345
|
+
/**
|
|
1346
|
+
* pull 산출 md 의 인라인 DB placeholder(`**제목** *(Notion DB)*` + 보존 마커)를
|
|
1347
|
+
* `![[<실제 .base 경로>|제목]]` 임베드로 재작성한다(F22) — folder note 본문에서
|
|
1348
|
+
* DB 가 텍스트 한 줄이 아니라 실제 Bases 뷰로 보이게 하는 마지막 조각. linked view
|
|
1349
|
+
* 컨테이너는 linkedMap 을 거쳐 원본 DB 의 .base 로 향한다. .base 경로는 DatabaseSyncer
|
|
1350
|
+
* 가 실제 기록한 경로(baseFileInfo)가 SSOT — 폴더명과 .base 파일명은 새니타이즈 규칙이
|
|
1351
|
+
* 달라(공백→하이픈 vs 공백 보존) localFolder 로 추측하면 깨진 임베드가 된다(E2E 실측
|
|
1352
|
+
* 91/158건). 최종적으로 .base 실존까지 확인해, 없으면 placeholder 를 마커째 보존한다.
|
|
1353
|
+
*/
|
|
1354
|
+
private rewriteDbPlaceholderEmbeds;
|
|
1084
1355
|
private resolveNotionLinks;
|
|
1085
1356
|
private ensureDbSchema;
|
|
1086
1357
|
private pushCreate;
|
|
1358
|
+
/**
|
|
1359
|
+
* 노트에 박힌 로컬 미디어를 Notion 에 올린다(R1).
|
|
1360
|
+
*
|
|
1361
|
+
* 먼저 본문 자리표시자를 실제 image/file 블록으로 **제자리** 교체한다. 그러고도 남은
|
|
1362
|
+
* 이미지만 페이지 끝에 덧붙이는 예전 경로로 흘린다 — 자리표시자가 없는 경우는 본문
|
|
1363
|
+
* push 가 통째로 스킵됐을 때(pageHasLiveChildren) 정도이고, 그때도 이미지 자체는
|
|
1364
|
+
* 올라가야 하므로 폴백을 남긴다.
|
|
1365
|
+
*/
|
|
1366
|
+
private syncEmbeddedMedia;
|
|
1087
1367
|
private pushUpdate;
|
|
1368
|
+
/**
|
|
1369
|
+
* 해소 대상 충돌 목록 — 원격 본문을 pull 과 **같은 파이프라인**으로 렌더해 담는다.
|
|
1370
|
+
*
|
|
1371
|
+
* 해소는 실제로 볼트 파일을 덮어쓰므로 `status()` 의 경량 미리보기 렌더를 쓰면 안 된다
|
|
1372
|
+
* (프론트매터·첨부가 빠진 반쪽 본문이 덮인다). 반대로 전체 pull 을 먼저 돌려 목록을
|
|
1373
|
+
* 얻는 것도 안 된다 — 해소하겠다고 볼트를 먼저 원격으로 덮어쓰는 셈이라 순서가 거꾸로다.
|
|
1374
|
+
* 그래서 충돌 레코드만 좁혀 그 페이지들만 읽어 온다.
|
|
1375
|
+
*/
|
|
1376
|
+
listConflicts(): Promise<Conflict[]>;
|
|
1377
|
+
/**
|
|
1378
|
+
* 추적 파일의 **현재 원격 본문**을 pull 과 같은 변환으로 렌더한다 — 표시 전용.
|
|
1379
|
+
*
|
|
1380
|
+
* 첨부는 내려받지 않는다: 비교를 보려다 볼트에 파일이 생기면 안 된다. 그 대가로 아직
|
|
1381
|
+
* 내려받지 않은 미디어는 원격 URL 로 남아 비교 화면에만 차이로 보인다.
|
|
1382
|
+
*
|
|
1383
|
+
* @returns 추적되지 않았거나 원격 페이지가 없으면 null.
|
|
1384
|
+
*/
|
|
1385
|
+
renderRemoteSnapshot(path: string): Promise<string | null>;
|
|
1386
|
+
/**
|
|
1387
|
+
* 해소할 게 남지 않은 충돌 레코드를 `synced` 로 되돌리고, 되돌린 경로를 반환한다.
|
|
1388
|
+
*
|
|
1389
|
+
* 충돌로 표시된 파일은 push 대상에서 통째로 빠진다(양쪽 덮어쓰기 방지). 그래서 사용자가
|
|
1390
|
+
* 손으로 양쪽을 맞춰 둬 이미 같은 내용이 됐는데도 레코드만 남으면, 그 파일의 이후 편집이
|
|
1391
|
+
* **영원히 Notion 에 올라가지 않는다** — 아무 경고 없이 정체된다. 내용이 이미 동일한
|
|
1392
|
+
* 건만 골라 상태를 되돌린다(진짜 충돌은 손대지 않는다).
|
|
1393
|
+
*/
|
|
1394
|
+
clearStaleConflicts(conflicts: readonly Conflict[]): string[];
|
|
1088
1395
|
/** 단일 충돌을 사용자가 고른 선택지(local/remote/merge/duplicate)로 해소 + Notion 전파. */
|
|
1089
1396
|
resolveConflict(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
|
|
1090
1397
|
/** 단일 충돌을 전략(manual/local-first/remote-first/duplicate)으로 해소 + Notion 전파. */
|
|
@@ -1114,6 +1421,22 @@ declare class SyncOrchestrator {
|
|
|
1114
1421
|
*/
|
|
1115
1422
|
private detectRemoteChangesIncremental;
|
|
1116
1423
|
private isTrackedParent;
|
|
1424
|
+
/**
|
|
1425
|
+
* 추적 중(sync_state)인데 디스크에서 사라진 파일을 찾는다.
|
|
1426
|
+
*
|
|
1427
|
+
* 원격 변경 감지(detectRemoteChanges*)는 "Notion 에서 바뀐 것"만 본다. 로컬에서 파일이
|
|
1428
|
+
* 지워진 경우는 어느 경로로도 잡히지 않아, 재pull 해도 `--force` 로도 되살아나지 않고
|
|
1429
|
+
* 영구히 발산했다. push 는 deleteSync=false 면 원격을 지우지 않으므로 사용자에겐 복구
|
|
1430
|
+
* 수단이 볼트 전체 초기화밖에 남지 않는다 — 이 스캔이 그 마지막 구멍을 막는다.
|
|
1431
|
+
*
|
|
1432
|
+
* 레코드마다 stat 을 던지면 1000건 규모에서 수 초가 든다. 볼트 워크 1회로 실재 목록을
|
|
1433
|
+
* 만든 뒤 차집합을 취한다(md). md 가 아닌 추적 레코드는 수가 적어 개별 확인으로 남긴다.
|
|
1434
|
+
*
|
|
1435
|
+
* 로컬 이름변경·이동은 "사라짐" 과 구별해야 한다 — 되살리면 원본과 새 이름의 사본이
|
|
1436
|
+
* 둘 다 남는다. push 의 detectMoves 와 같은 기준(내용 해시 일치)을 쓰되, 볼트 전체를
|
|
1437
|
+
* 해시하지 않도록 크기가 같은 미추적 파일만 후보로 좁혀 확인한다.
|
|
1438
|
+
*/
|
|
1439
|
+
private detectMissingLocalFiles;
|
|
1117
1440
|
private pullCreate;
|
|
1118
1441
|
/**
|
|
1119
1442
|
* 페이지가 폴더(자식 페이지·자식 DB 보유)인지 신뢰성 있게 판정한다.
|
|
@@ -1129,6 +1452,18 @@ declare class SyncOrchestrator {
|
|
|
1129
1452
|
* 재귀 탐색해 child_page·child_database 를 직접 확인한다.
|
|
1130
1453
|
*/
|
|
1131
1454
|
private pageHasChildContainers;
|
|
1455
|
+
/**
|
|
1456
|
+
* 원격 페이지를 **pull 과 동일한 파이프라인**으로 볼트에 쓸 수 있는 본문까지 렌더한다.
|
|
1457
|
+
* (본문 변환 → 이미지/첨부 내려받기 → 속성 주입 → preserve marker → 압축형 간격 판정)
|
|
1458
|
+
*
|
|
1459
|
+
* pull 경로와 충돌 해소 경로가 이 한 곳을 공유해야 한다. 예전엔 충돌 목록만
|
|
1460
|
+
* {@link fetchPageMarkdown} 원문을 그대로 담아, `nobsi resolve` 에서 "원격 유지"를 고르면
|
|
1461
|
+
* 프론트매터도 첨부도 없는 반쪽 본문이 볼트에 덮여 썼다 — 해소가 곧 손실이었다.
|
|
1462
|
+
*
|
|
1463
|
+
* @returns 렌더된 본문과 함께, 호출자가 위키링크 레지스트리 등에 쓰는 제목·속성.
|
|
1464
|
+
* 여기서 이미 읽은 값을 되돌려 줘야 호출자가 같은 페이지를 다시 파싱하지 않는다.
|
|
1465
|
+
*/
|
|
1466
|
+
private renderRemotePage;
|
|
1132
1467
|
private pullUpdate;
|
|
1133
1468
|
private pullDelete;
|
|
1134
1469
|
private ensureFolderPage;
|
|
@@ -1141,6 +1476,13 @@ declare class SyncOrchestrator {
|
|
|
1141
1476
|
private pushUpdatePage;
|
|
1142
1477
|
private fetchPageMarkdown;
|
|
1143
1478
|
private collectInlineDbRefs;
|
|
1479
|
+
/**
|
|
1480
|
+
* 이번 라운드에 기록된 DB row md 를 읽어 본문의 인라인 DB 참조를 수집한다.
|
|
1481
|
+
* row 페이지는 DatabaseSyncer 자체 파이프라인으로 변환돼 fetchPageMarkdown 의
|
|
1482
|
+
* _inlineDbRefs 수집을 타지 않는다 — 디스크에 남은 보존 마커가 유일한 발견 신호다.
|
|
1483
|
+
* 부모 Notion 페이지 id 는 state DB 역조회(getByPath)로 얻는다(row upsert 직후라 항상 존재).
|
|
1484
|
+
*/
|
|
1485
|
+
private collectRowInlineRefs;
|
|
1144
1486
|
private resolveNotionIdWikilinks;
|
|
1145
1487
|
private extractParentId;
|
|
1146
1488
|
private resolveBlockToPageId;
|
|
@@ -1163,6 +1505,25 @@ declare class SyncOrchestrator {
|
|
|
1163
1505
|
private recoverInterruptedPushOps;
|
|
1164
1506
|
/** 부모 페이지의 직속 자식 중 제목이 일치하는(보관/휴지통 제외) child_page id 를 찾는다. */
|
|
1165
1507
|
private findChildPageByTitle;
|
|
1508
|
+
/**
|
|
1509
|
+
* 신규 pull 페이지가 쓸 파일 경로를 충돌 없이 결정하고 **그 자리에서 선점**한다.
|
|
1510
|
+
*
|
|
1511
|
+
* DB 행과 **같은 규칙**({@link pagePathCandidates})을 쓴다. 예전에는 `(1)`, `(2)` …
|
|
1512
|
+
* 순번을 99 까지 훑고 고갈되면 **원본 경로를 그대로 돌려줬는데**, 그러면 남의 노트를
|
|
1513
|
+
* 조용히 덮어써 내용이 사라진다. 순번은 그때의 볼트 상태로 정해져 실행마다 페이지끼리
|
|
1514
|
+
* 접미사가 뒤바뀔 수도 있었다(pull 마다 파일이 갈아엎히는 churn).
|
|
1515
|
+
*
|
|
1516
|
+
* 점유 판정은 세 가지를 모두 본다:
|
|
1517
|
+
* · 이번 실행의 선점 장부 — 아래 참조.
|
|
1518
|
+
* · 추적 레코드 — 다른 페이지가 소유한 경로면 피하고, 자기 소유면 그대로 재사용한다.
|
|
1519
|
+
* · 볼트 파일 — 추적되지 않는 사용자 노트가 놓여 있으면 피한다.
|
|
1520
|
+
*
|
|
1521
|
+
* 선점 장부가 필요한 이유는 pull 이 워커 풀로 **동시 실행**되기 때문이다. 조회와 기록
|
|
1522
|
+
* 사이에 await 가 끼면 동명 페이지 여럿이 나란히 "비어 있음"을 보고 같은 경로를 고른다.
|
|
1523
|
+
* 그러면 마지막에 쓴 페이지만 남고 나머지 본문이 사라진다(실측 재현: 동명 3페이지 →
|
|
1524
|
+
* 파일 1개·레코드 1건). 그래서 마지막 확인과 등록을 **await 없는 동기 구간**에 묶는다 —
|
|
1525
|
+
* 그 사이에는 다른 작업이 끼어들 수 없으므로 두 페이지가 같은 경로를 얻는 일이 없다.
|
|
1526
|
+
*/
|
|
1166
1527
|
private resolveUniqueFilePath;
|
|
1167
1528
|
}
|
|
1168
1529
|
|
|
@@ -1201,6 +1562,8 @@ interface MediaOptions {
|
|
|
1201
1562
|
readonly maxRetries?: number;
|
|
1202
1563
|
readonly retryBaseMs?: number;
|
|
1203
1564
|
readonly maxFileSizeBytes?: number;
|
|
1565
|
+
/** 다운로드 1회 시도의 시간 상한(ms). 기본 300초. 0 이하면 상한 없음(권장하지 않음). */
|
|
1566
|
+
readonly downloadTimeoutMs?: number;
|
|
1204
1567
|
}
|
|
1205
1568
|
interface ImageDownloadResult {
|
|
1206
1569
|
readonly originalUrl: string;
|
|
@@ -1215,6 +1578,12 @@ interface ImageUploadResult {
|
|
|
1215
1578
|
readonly fileHash: string;
|
|
1216
1579
|
readonly fileSize: number;
|
|
1217
1580
|
}
|
|
1581
|
+
/** 제자리 교체 결과 — 어떤 임베드 대상이 처리됐는지 호출자가 알아야 중복 업로드를 막는다. */
|
|
1582
|
+
interface MaterializeResult {
|
|
1583
|
+
readonly uploaded: ImageUploadResult[];
|
|
1584
|
+
/** 실제 블록으로 바뀐 임베드 대상(원본 문자열 + 별칭 제거형 둘 다 담는다). */
|
|
1585
|
+
readonly handledTargets: Set<string>;
|
|
1586
|
+
}
|
|
1218
1587
|
declare class ImageHandler {
|
|
1219
1588
|
private readonly vaultFs;
|
|
1220
1589
|
private readonly attachmentFolder;
|
|
@@ -1225,25 +1594,77 @@ declare class ImageHandler {
|
|
|
1225
1594
|
private readonly maxRetries;
|
|
1226
1595
|
private readonly retryBaseMs;
|
|
1227
1596
|
private readonly maxFileSizeBytes;
|
|
1597
|
+
private readonly downloadTimeoutMs;
|
|
1228
1598
|
constructor(vaultFs: VaultFS, attachmentFolder?: string, notionClient?: NotionClient | undefined, customFetch?: typeof globalThis.fetch, options?: MediaOptions, stateDb?: IStateDB | undefined);
|
|
1229
1599
|
private get fetchFn();
|
|
1600
|
+
/** 상한 로직은 첨부 경로와 공유한다 — 한쪽만 빠뜨리지 않도록(utils/download-fetch). */
|
|
1601
|
+
private fetchForDownload;
|
|
1230
1602
|
downloadImage(url: string, pageTitle: string): Promise<ImageDownloadResult>;
|
|
1231
1603
|
private fetchImage;
|
|
1604
|
+
/**
|
|
1605
|
+
* 캡션이 push 가 적어 둔 원본 임베드 대상인지 판정한다(R1). 맞으면 그 문자열을
|
|
1606
|
+
* 그대로 돌려주고, 호출자는 사본을 내려받는 대신 `![[원본]]` 으로 되살린다.
|
|
1607
|
+
*
|
|
1608
|
+
* 사용자가 Notion 에서 손으로 쓴 캡션을 경로로 오인하면 깨진 위키링크가 되므로,
|
|
1609
|
+
* ① 파일명이 실제 업로드된 파일명과 같고 ② 볼트에 그 파일이 실재할 때만 인정한다.
|
|
1610
|
+
* 둘 중 하나라도 어긋나면 null 을 돌려 기존 다운로드 경로로 떨어진다.
|
|
1611
|
+
*
|
|
1612
|
+
* ②의 실재 확인은 push 와 **같은 후보 사다리**(노트 폴더 → 루트 → attachments/)를
|
|
1613
|
+
* 쓴다 — 위키링크는 파일명만 적는 게 관례라 볼트 루트만 보면 노트 옆 파일을 놓친다.
|
|
1614
|
+
*/
|
|
1615
|
+
private resolveOriginalTarget;
|
|
1232
1616
|
private persistImage;
|
|
1233
|
-
downloadAllImages(markdown: string, pageTitle: string, pageId?: string): Promise<{
|
|
1617
|
+
downloadAllImages(markdown: string, pageTitle: string, pageId?: string, notePath?: string): Promise<{
|
|
1234
1618
|
content: string;
|
|
1235
1619
|
downloads: ImageDownloadResult[];
|
|
1236
1620
|
}>;
|
|
1237
1621
|
uploadLocalImage(localPath: string, noteFolder?: string): Promise<ImageUploadResult>;
|
|
1238
1622
|
uploadAndAppendImages(pageId: string, images: ImageReference[], notePath?: string): Promise<ImageUploadResult[]>;
|
|
1239
1623
|
/**
|
|
1240
|
-
*
|
|
1241
|
-
*
|
|
1242
|
-
*
|
|
1624
|
+
* push 가 심어 둔 자리표시자 quote 를 **제자리에서** 실제 image/file 블록으로 바꾼다(R1).
|
|
1625
|
+
*
|
|
1626
|
+
* 그 전에는 임베드 하나가 Notion 에서 두 조각으로 갈라졌다 — 본문 자리에는
|
|
1627
|
+
* `> 📎 foo.png` + `%% im-nobsidian:local-image:… %%` 리터럴 마커가, 실제 이미지는
|
|
1628
|
+
* 페이지 맨 끝에 따로. 실측: 볼트 175개 파일에서 마커 978건이 Notion 에 그대로
|
|
1629
|
+
* 텍스트로 새고 있었고 그중 964건(98.6%)이 이 미디어 쌍이었다. 사용자가 Notion 에서
|
|
1630
|
+
* 보는 화면이 깨질 뿐 아니라 이미지가 본문 흐름에서 이탈한다.
|
|
1631
|
+
*
|
|
1632
|
+
* 캡션에는 원본 임베드 대상을 그대로 적는다. pull 이 경로(와 `|별칭`)를 되찾는
|
|
1633
|
+
* 진실원이 되고, 사용자에게도 출처가 보인다.
|
|
1634
|
+
*
|
|
1635
|
+
* 볼트에서 파일을 못 찾으면 자리표시자를 그대로 둔다 — 마커가 남아야 pull 이
|
|
1636
|
+
* 임베드를 복원할 수 있으므로, 반쯤 지우는 것보다 안전하다.
|
|
1637
|
+
*/
|
|
1638
|
+
materializeLocalMedia(pageId: string, pushedMarkdown: string, notePath?: string): Promise<MaterializeResult>;
|
|
1639
|
+
/**
|
|
1640
|
+
* 페이지 블록 트리를 훑어 자리표시자를 부모와 함께 모은다. 콜아웃·토글·목록 안에
|
|
1641
|
+
* 들어간 임베드도 제자리 교체하려면 부모 id 가 필요하다(append 의 after_block 은
|
|
1642
|
+
* 같은 부모의 자식만 기준으로 삼는다).
|
|
1643
|
+
*/
|
|
1644
|
+
private collectPlaceholders;
|
|
1645
|
+
/**
|
|
1646
|
+
* 업로드분을 file_registry 에 남긴다. 노트 임베드는 여기서 페이지에 붙으므로,
|
|
1647
|
+
* 이후 FileHandler.pushAllFiles 가 같은 첨부를 폴더 페이지에 standalone 으로
|
|
1648
|
+
* 재업로드하는 중복을 공유 레지스트리로 차단한다.
|
|
1649
|
+
*/
|
|
1650
|
+
private registerUploads;
|
|
1651
|
+
/**
|
|
1652
|
+
* 임베드 경로가 가리킬 수 있는 볼트 실제 파일 후보를 우선순위대로 만든다(F23).
|
|
1653
|
+
* Obsidian 위키링크 임베드는 노트 옆 파일을 파일명만으로 참조하는 게 관례라
|
|
1654
|
+
* 노트 폴더 → 볼트 루트 → attachments/ 순.
|
|
1655
|
+
*
|
|
1656
|
+
* push(업로드)와 pull(원본 복원)이 **같은 사다리**를 봐야 왕복이 닫힌다. 예전에는
|
|
1657
|
+
* pull 쪽이 볼트 루트만 확인해서, `__e2e_probe__/노트.md` 안의 `![[t-img.png]]` 는
|
|
1658
|
+
* 올라갈 때는 노트 폴더에서 잘 찾아 올리고도 내려올 때는 원본을 못 찾아 사본을
|
|
1659
|
+
* 새로 받았다(실측) — 왕복할 때마다 첨부가 한 벌씩 늘어난다.
|
|
1660
|
+
*/
|
|
1661
|
+
private vaultCandidates;
|
|
1662
|
+
/**
|
|
1663
|
+
* 임베드 경로를 볼트 실제 파일로 해석해 읽는다(F23).
|
|
1664
|
+
* 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
|
|
1243
1665
|
*/
|
|
1244
1666
|
private readImageFromVault;
|
|
1245
|
-
|
|
1246
|
-
downloadAllFiles(markdown: string, pageTitle: string): Promise<{
|
|
1667
|
+
downloadAllFiles(markdown: string, pageTitle: string, notePath?: string): Promise<{
|
|
1247
1668
|
content: string;
|
|
1248
1669
|
downloads: ImageDownloadResult[];
|
|
1249
1670
|
}>;
|
|
@@ -1251,11 +1672,39 @@ declare class ImageHandler {
|
|
|
1251
1672
|
private downloadFile;
|
|
1252
1673
|
private isNotionImageUrl;
|
|
1253
1674
|
private isNotionFileUrl;
|
|
1675
|
+
/**
|
|
1676
|
+
* notion-hosted 파일 URL 하나를 로컬 첨부로 로컬라이즈한다(P3-A).
|
|
1677
|
+
* DB 행의 `files` 속성처럼 본문 밖에 있는 서명 URL 용 — 서명 URL 은 약 1시간 뒤
|
|
1678
|
+
* 만료되어 frontmatter 에 남으면 깨진 링크가 되고, push 로 되밀면 Notion 원본이
|
|
1679
|
+
* external(만료 URL)로 오염된다(실측). notion-hosted 가 아닌 URL(사용자가 넣은
|
|
1680
|
+
* 진짜 외부 링크)은 다운로드 대상이 아니므로 null 을 반환한다.
|
|
1681
|
+
*
|
|
1682
|
+
* @returns 로컬 첨부 경로(`attachments/...`), 다운로드 대상이 아니거나 실패 시 null
|
|
1683
|
+
*/
|
|
1684
|
+
localizeNotionFileUrl(url: string, caption: string): Promise<string | null>;
|
|
1254
1685
|
private guessExtension;
|
|
1255
1686
|
private guessFileExtension;
|
|
1256
1687
|
}
|
|
1257
1688
|
|
|
1689
|
+
/**
|
|
1690
|
+
* 확장자 → MIME / Notion 블록타입 매핑의 단일 출처.
|
|
1691
|
+
*
|
|
1692
|
+
* 예전에는 ImageHandler 가 이미지 확장자만 담은 축소판 테이블을 따로 들고 있었다.
|
|
1693
|
+
* 그 결과 노트에 박힌 `![[문서.txt]]` 같은 임베드를 올릴 때 `application/octet-stream`
|
|
1694
|
+
* 으로 요청해 Notion File Upload API 가 400 으로 거절했다(실측). 업로드 경로가 둘인데
|
|
1695
|
+
* 테이블이 둘이면 한쪽만 고쳐지므로 여기로 합친다.
|
|
1696
|
+
*/
|
|
1258
1697
|
type NotionBlockType = "image" | "pdf" | "video" | "audio" | "file";
|
|
1698
|
+
|
|
1699
|
+
/** 첨부 다운로드 운영 튜닝값. 미지정 시 기존 동작과 동일한 기본값 사용. */
|
|
1700
|
+
interface FileHandlerOptions {
|
|
1701
|
+
/** 다운로드에 쓸 fetch 구현(테스트 주입용). 기본 전역 fetch. */
|
|
1702
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
1703
|
+
/** 다운로드 1회 시도의 시간 상한(ms). 기본 300초. 0 이하면 상한 없음(권장하지 않음). */
|
|
1704
|
+
readonly downloadTimeoutMs?: number;
|
|
1705
|
+
/** 첨부 1건 업로드 전체의 시간 상한(ms). 기본 30분. 0 이하면 상한 없음. */
|
|
1706
|
+
readonly itemTimeoutMs?: number;
|
|
1707
|
+
}
|
|
1259
1708
|
interface FileUploadResult {
|
|
1260
1709
|
readonly localPath: string;
|
|
1261
1710
|
readonly fileUploadId: string;
|
|
@@ -1272,8 +1721,34 @@ declare class FileHandler {
|
|
|
1272
1721
|
private readonly notionClient;
|
|
1273
1722
|
private readonly stateDb;
|
|
1274
1723
|
private readonly sema;
|
|
1275
|
-
|
|
1724
|
+
private readonly customFetch?;
|
|
1725
|
+
private readonly downloadTimeoutMs;
|
|
1726
|
+
private readonly itemTimeoutMs;
|
|
1727
|
+
constructor(vaultFs: VaultFS, notionClient: NotionClient, stateDb: IStateDB, concurrency?: number, options?: FileHandlerOptions);
|
|
1728
|
+
private get fetchFn();
|
|
1276
1729
|
pushFilesForFolder(folderPageId: string, folderPath: string): Promise<FileUploadResult[]>;
|
|
1730
|
+
/**
|
|
1731
|
+
* 첨부 1건을 폴더 페이지에 올린다 — 두 push 경로(`pushFilesForFolder` · `pushAllFiles`)의
|
|
1732
|
+
* 공통 단위. 변경 없음 판정·동시성 슬롯·시간 상한·실패 격리를 모두 여기 한 곳에 둔다.
|
|
1733
|
+
*
|
|
1734
|
+
* 한 곳에 모은 이유는 R9a 에서 실제로 데인 적이 있어서다. 같은 다운로드 로직이
|
|
1735
|
+
* image/file 핸들러에 두 벌 존재한 탓에 한쪽에만 상한이 걸린 채로 남았고, 상한 없는
|
|
1736
|
+
* 쪽이 세마포어를 쥔 채 영원히 매달렸다. 여기도 같은 코드가 두 루프에 나뉘어 있었다.
|
|
1737
|
+
*
|
|
1738
|
+
* 상한(R9f): 업로드는 호출 1건마다 상한이 있어도(SDK 30초) 멀티파트는 파트 수만큼
|
|
1739
|
+
* 호출이 늘어나는 **합성 경로**라 전체로는 여전히 무한대다. 상한을 넘긴 첨부는 경고만
|
|
1740
|
+
* 남기고 나머지 첨부는 계속 올린다 — 첨부 하나가 push 전체를 멎게 하지 않는다.
|
|
1741
|
+
*
|
|
1742
|
+
* @returns 업로드 결과. 변경 없음(스킵)이거나 실패했으면 null.
|
|
1743
|
+
*/
|
|
1744
|
+
private pushSingleFile;
|
|
1745
|
+
/**
|
|
1746
|
+
* 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 제외한다.
|
|
1747
|
+
* node 측 VaultFS 는 과거 `.base` 를 워커에서 빼는 식으로 우회했지만, Obsidian
|
|
1748
|
+
* 어댑터는 모든 파일을 반환하므로 플러그인 push 가 사이드카를 Notion 첨부로
|
|
1749
|
+
* 오염시켰다. 소비자인 여기서 일괄 차단해 구현체 간 동작을 통일한다.
|
|
1750
|
+
*/
|
|
1751
|
+
private listUploadableFiles;
|
|
1277
1752
|
pushAllFiles(): Promise<FileUploadResult[]>;
|
|
1278
1753
|
downloadFileBlock(url: string, filename: string, targetFolder: string): Promise<FileDownloadResult>;
|
|
1279
1754
|
downloadFileBlocks(blocks: Array<{
|
|
@@ -1309,6 +1784,18 @@ interface DatabaseSyncResult {
|
|
|
1309
1784
|
* 엉뚱한 행을 골라 정작 바뀐 행의 relation UUID 를 영영 해소하지 못했다(M3). push 경로는 항상 빈 배열.
|
|
1310
1785
|
*/
|
|
1311
1786
|
writtenPaths: string[];
|
|
1787
|
+
/**
|
|
1788
|
+
* R13: 볼트에서 사라졌기에 되살린 행 수. `updated` 와 섞지 않는다 — 리모트는 그대로이고
|
|
1789
|
+
* 바뀐 것은 로컬의 부재이므로, 합쳐 세면 dry-run 과 멱등성 게이트(churn)가 "원격이
|
|
1790
|
+
* 바뀌었다"고 거짓말한다. 페이지 경로가 이미 같은 이유로 `restored` 를 분리해 보고한다.
|
|
1791
|
+
*/
|
|
1792
|
+
restored: number;
|
|
1793
|
+
/**
|
|
1794
|
+
* F25: 이 config 가 linked view 컨테이너로 판정된 경우 data source 원본 database id.
|
|
1795
|
+
* 행 처리는 원본 config 에 양보하고 건너뛰었으므로, 호출처(디스커버리 캐시)는 이 config
|
|
1796
|
+
* 를 제거하고 linked 매핑을 기록해야 다음 pull 부터 이중 방문 자체가 사라진다.
|
|
1797
|
+
*/
|
|
1798
|
+
linkedOriginalDbId?: string;
|
|
1312
1799
|
}
|
|
1313
1800
|
declare class DatabaseSyncer {
|
|
1314
1801
|
private readonly config;
|
|
@@ -1320,10 +1807,28 @@ declare class DatabaseSyncer {
|
|
|
1320
1807
|
private readonly propertyMapper;
|
|
1321
1808
|
private readonly baseFileGenerator;
|
|
1322
1809
|
private readonly sidecarGenerator;
|
|
1810
|
+
/**
|
|
1811
|
+
* 이번 프로세스가 실제로 기록한 .base 경로/DB 제목 (databaseId nohyph → info).
|
|
1812
|
+
* placeholder 임베드 재작성(F22)의 SSOT — 폴더명(하이픈 새니타이즈)과 .base 파일명
|
|
1813
|
+
* (sanitizeFileName: 공백·점 보존)은 규칙이 달라 localFolder 로 추측한 경로는 깨진
|
|
1814
|
+
* 임베드가 된다. generateBaseFile 은 매 pull 모든 DB 에 대해 실행되므로 pull 종료
|
|
1815
|
+
* 시점에는 성공한 DB 전체가 채워져 있다.
|
|
1816
|
+
*/
|
|
1817
|
+
readonly baseFileInfo: Map<string, {
|
|
1818
|
+
basePath: string;
|
|
1819
|
+
title: string;
|
|
1820
|
+
}>;
|
|
1323
1821
|
constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, pipeline: ConversionPipeline, imageHandler: ImageHandler);
|
|
1324
1822
|
pullAll(): Promise<DatabaseSyncResult>;
|
|
1325
1823
|
pushAll(): Promise<DatabaseSyncResult>;
|
|
1326
|
-
pullDatabase(dbConfig: DatabaseSyncConfig
|
|
1824
|
+
pullDatabase(dbConfig: DatabaseSyncConfig, opts?: {
|
|
1825
|
+
/**
|
|
1826
|
+
* F25: database id(하이픈 유무 무관) → 그 DB 행이 사는 로컬 폴더. 디스커버리 캐시가
|
|
1827
|
+
* 아는 DB 만 반환하고 모르면 null. linked view 컨테이너 판정 시 원본 폴더를 아는
|
|
1828
|
+
* 경우에만 행 소유를 양보한다 — 원본이 미발견이면 이 컨테이너가 유일한 접근 통로다.
|
|
1829
|
+
*/
|
|
1830
|
+
resolveDbFolder?: (databaseId: string) => string | null;
|
|
1831
|
+
}): Promise<DatabaseSyncResult>;
|
|
1327
1832
|
private pullDatabaseViews;
|
|
1328
1833
|
private generateBaseFile;
|
|
1329
1834
|
/**
|
|
@@ -1338,8 +1843,29 @@ declare class DatabaseSyncer {
|
|
|
1338
1843
|
* 결정적 직렬화라 동일 DB 상태 → 동일 바이트 → 멱등(drift/churn 0).
|
|
1339
1844
|
*/
|
|
1340
1845
|
private generateSidecar;
|
|
1846
|
+
/**
|
|
1847
|
+
* DB 행 `files` 속성의 notion-hosted 서명 URL 을 로컬 첨부로 로컬라이즈한다(P3-A).
|
|
1848
|
+
* 서명 URL 은 약 1시간 뒤 만료되어 frontmatter 에 남으면 깨진 링크가 되고, push 로
|
|
1849
|
+
* 되밀면 Notion 원본이 만료 URL 의 external 파일로 오염된다(실측). 다운로드 성공분은
|
|
1850
|
+
* `[[attachments/..]]` 위키링크로 대체한다 — Bases 카드 `image:` 는 위키링크 스칼라를
|
|
1851
|
+
* 렌더하므로 갤러리 커버도 유지된다. 원본 파일명은 raw 속성의 `name` 에서 취한다
|
|
1852
|
+
* (extractValue 는 URL 만 남긴다). 사용자가 넣은 진짜 외부 URL 은 그대로 둔다.
|
|
1853
|
+
*/
|
|
1854
|
+
private localizeFileProperties;
|
|
1341
1855
|
private pullDatabasePage;
|
|
1342
1856
|
private pushDatabase;
|
|
1857
|
+
/**
|
|
1858
|
+
* db-row 파일 1건을 Notion 으로 밀어 올린다 — 생성·갱신·rename 재매핑·스킵을 모두 담당.
|
|
1859
|
+
*
|
|
1860
|
+
* 호출부의 루프에서 떼어낸 이유는 상한(`withDeadline`)을 걸 단위가 필요해서다(R9e).
|
|
1861
|
+
* 그래서 결과는 카운터를 직접 건드리지 않고 {@link PushRowOutcome} 으로 돌려준다 —
|
|
1862
|
+
* 상한에 걸린 행이 카운터를 절반만 올린 채 끊기는 일이 구조적으로 불가능해진다.
|
|
1863
|
+
*
|
|
1864
|
+
* 본문 push 실패만 `failed` 를 **반환**하고 나머지 예외는 그대로 던진다. 둘의 차이는
|
|
1865
|
+
* 상태 처리에 있다: 전자는 레코드를 `error` 로 내려 해시 전진을 막아야 하므로 여기서
|
|
1866
|
+
* 처리하고, 후자는 호출부의 catch 가 일괄 기록한다(기존 동작 그대로).
|
|
1867
|
+
*/
|
|
1868
|
+
private pushDatabaseRow;
|
|
1343
1869
|
/**
|
|
1344
1870
|
* rename 감지를 위한 고아 레코드 인덱스(내용 해시 → 레코드)를 만든다.
|
|
1345
1871
|
* 고아 = 해당 DB 소속 db-row 레코드 중 로컬 파일이 더 이상 존재하지 않는(livePaths 에 없는) 것.
|
|
@@ -1766,19 +2292,6 @@ declare class EntryEditor {
|
|
|
1766
2292
|
updateDate(entryPath: string, dateProperty: string, newDate: string): Promise<void>;
|
|
1767
2293
|
}
|
|
1768
2294
|
|
|
1769
|
-
interface BasePropertySchema {
|
|
1770
|
-
readonly id: string;
|
|
1771
|
-
readonly type: string;
|
|
1772
|
-
readonly options?: Array<{
|
|
1773
|
-
name: string;
|
|
1774
|
-
color?: string;
|
|
1775
|
-
}>;
|
|
1776
|
-
readonly groups?: Array<{
|
|
1777
|
-
name: string;
|
|
1778
|
-
color?: string;
|
|
1779
|
-
optionIds?: string[];
|
|
1780
|
-
}>;
|
|
1781
|
-
}
|
|
1782
2295
|
interface BaseFileOptions {
|
|
1783
2296
|
readonly databaseId: string;
|
|
1784
2297
|
readonly databaseName: string;
|
|
@@ -1787,16 +2300,31 @@ interface BaseFileOptions {
|
|
|
1787
2300
|
readonly folderPath: string;
|
|
1788
2301
|
}
|
|
1789
2302
|
type BasesViewType = "table" | "cards" | "list";
|
|
2303
|
+
/** Notion 뷰 하나를 Bases 로 옮길 때의 판정. */
|
|
2304
|
+
interface BasesViewMapping {
|
|
2305
|
+
/** Bases 뷰 타입. `null` = 대응이 없어 `.base` 에 실을 수 없음. */
|
|
2306
|
+
readonly type: BasesViewType | null;
|
|
2307
|
+
/**
|
|
2308
|
+
* 값이 있으면 **정확한 대응이 아니라 격하**라는 뜻(사람용 사유).
|
|
2309
|
+
* 뷰 자체는 살아남지만 원래 뷰의 표현 일부(달력 격자·타임라인 축)는 재현되지 않는다.
|
|
2310
|
+
*/
|
|
2311
|
+
readonly degradeReason?: string;
|
|
2312
|
+
}
|
|
1790
2313
|
/**
|
|
1791
|
-
* Notion 뷰 타입 → Obsidian Bases 뷰
|
|
2314
|
+
* Notion 뷰 타입 → Obsidian Bases 뷰 매핑(SSOT).
|
|
1792
2315
|
*
|
|
1793
|
-
* `null` = Bases 에 대응 뷰가 없어 `.base` 로 표현 불가. 이 경우 해당 뷰는 `.base` 에서
|
|
1794
|
-
* 누락되므로, 사이드카(`<db>.notion.json`)가 원본 뷰 설정을 무손실 보존한다.
|
|
1795
2316
|
* 뷰는 현재 pull-authoritative — Notion API 가 2026-03-19 뷰 생성/수정/삭제를 열었지만
|
|
1796
2317
|
* 본 프로젝트는 아직 뷰 write-back 을 구현하지 않았다(로드맵 항목). push 로 되돌릴 수
|
|
1797
2318
|
* 없는 동안 "조용한 유실 금지"는 곧 "보존 + 정직한 degrade 리포트"를 의미한다.
|
|
2319
|
+
*
|
|
2320
|
+
* 달력·타임라인은 예전에 `null` 이었다 — 즉 `.base` 에서 **통째로 사라졌다**. 사이드카에는
|
|
2321
|
+
* 남지만 Obsidian 을 여는 사람 눈에는 그 뷰가 애초에 없었던 것처럼 보인다. 격자·축은
|
|
2322
|
+
* 못 살려도 "날짜 순으로 늘어놓은 목록"이라는 본래 의도는 표로 남길 수 있으므로,
|
|
2323
|
+
* 버리는 대신 격하해 살리고 사유를 사이드카에 적는다.
|
|
1798
2324
|
*/
|
|
1799
|
-
declare const NOTION_TO_BASES_VIEW: Record<string,
|
|
2325
|
+
declare const NOTION_TO_BASES_VIEW: Record<string, BasesViewMapping>;
|
|
2326
|
+
/** Notion 뷰 타입의 Bases 매핑. 미상 타입은 표현 불가로 본다. */
|
|
2327
|
+
declare function basesViewMappingOf(notionType: string): BasesViewMapping;
|
|
1800
2328
|
/** Notion 뷰 타입의 Bases 대응 타입을 돌려준다. 미지원/미상 타입은 `null`. */
|
|
1801
2329
|
declare function basesViewTypeOf(notionType: string): BasesViewType | null;
|
|
1802
2330
|
declare class BaseFileGenerator {
|
|
@@ -1806,6 +2334,19 @@ declare class BaseFileGenerator {
|
|
|
1806
2334
|
private writeFormulas;
|
|
1807
2335
|
private writePropertyDisplayNames;
|
|
1808
2336
|
private writeViews;
|
|
2337
|
+
/** `filters:` 키와 그 값을 쓴다. 단일 식이면 한 줄, 그룹이면 블록. */
|
|
2338
|
+
private writeFilterValue;
|
|
2339
|
+
/** 필터 노드를 시퀀스 항목(`- …`)으로 쓴다. 그룹이면 재귀적으로 중첩한다. */
|
|
2340
|
+
private writeFilterItem;
|
|
2341
|
+
/**
|
|
2342
|
+
* 필터 식을 YAML 스칼라로 쓴다.
|
|
2343
|
+
*
|
|
2344
|
+
* 식은 대개 `note["상태"] == "진행 중"` 처럼 큰따옴표를 품는다. 평문 스칼라는 큰따옴표를
|
|
2345
|
+
* 안에 담을 수 있으므로(선두만 아니면 된다) 기본은 평문으로 두어 기존 `file.ext == "md"`
|
|
2346
|
+
* 라인과 모양을 맞춘다. 다만 `: `·` #`·선두 지시문자는 평문에서 의미가 달라지므로
|
|
2347
|
+
* 그때만 작은따옴표로 감싼다(속성명에 콜론이나 해시가 들어갈 수 있다).
|
|
2348
|
+
*/
|
|
2349
|
+
private yamlExpr;
|
|
1809
2350
|
/**
|
|
1810
2351
|
* 뷰 이름을 Bases 요구사항대로 유일화한다(in-place). 동명 충돌 시 ' 2',' 3'… 를 붙이되
|
|
1811
2352
|
* 이미 존재하는 이름과 다시 충돌하지 않을 때까지 증가시킨다.
|
|
@@ -1820,11 +2361,72 @@ declare class BaseFileGenerator {
|
|
|
1820
2361
|
*/
|
|
1821
2362
|
private propRef;
|
|
1822
2363
|
private resolvePropertyName;
|
|
1823
|
-
private decodeId;
|
|
1824
2364
|
private yamlKey;
|
|
1825
2365
|
private yamlValue;
|
|
1826
2366
|
}
|
|
1827
2367
|
|
|
2368
|
+
/**
|
|
2369
|
+
* 속성 id → 프론트매터 키 해소.
|
|
2370
|
+
*
|
|
2371
|
+
* Notion `views.retrieve` 는 RAW 속성 id(예: `[jiM`)를 주지만 `databases.retrieve` 스키마는
|
|
2372
|
+
* URL-인코딩된 id(예: `%5BjiM`)를 준다. 디코드 후 비교하지 않으면 title 외 모든 속성
|
|
2373
|
+
* (cover/order/sort/groupBy/filter)이 매칭에 실패해 조용히 사라진다.
|
|
2374
|
+
*
|
|
2375
|
+
* `.base` 생성기와 사이드카 생성기가 같은 규칙을 써야 하므로 여기 한 곳에 둔다.
|
|
2376
|
+
*/
|
|
2377
|
+
declare function resolvePropertyName(propertyId: string, schema: Record<string, BasePropertySchema>): string | undefined;
|
|
2378
|
+
|
|
2379
|
+
/**
|
|
2380
|
+
* Notion 뷰 필터식 → Obsidian Bases 필터식 번역기.
|
|
2381
|
+
*
|
|
2382
|
+
* ## 왜 필요한가
|
|
2383
|
+
*
|
|
2384
|
+
* Notion 뷰의 절반 가까이는 필터가 걸려 있다. 그런데 `.base` 는 폴더 필터만 내보냈고,
|
|
2385
|
+
* 사이드카는 "Notion 필터식은 .base 로 표현되지 않음"이라 적어 두고 끝냈다. 그래서
|
|
2386
|
+
* '시작전, 진행중' 이라는 이름의 뷰가 Obsidian 에서는 **완료된 행까지 전부** 보여 줬다 —
|
|
2387
|
+
* 이름과 내용이 어긋나는, 사용자가 오해하기 딱 좋은 상태였다. Bases 는 뷰마다 `filters:`
|
|
2388
|
+
* 를 지원하므로(공식 문서 `views[].filters`) 표현 가능한 조건은 실제로 옮길 수 있다.
|
|
2389
|
+
*
|
|
2390
|
+
* ## 부분 번역의 안전 규칙 — 이 파일의 핵심
|
|
2391
|
+
*
|
|
2392
|
+
* Notion 필터 문법은 Bases 보다 넓다(rollup any/every/none, 상대 날짜, formula 등).
|
|
2393
|
+
* 못 옮기는 리프를 만났을 때 **어느 방향으로 틀릴지**는 논리 위치에 따라 다르다:
|
|
2394
|
+
*
|
|
2395
|
+
* - `and` 안에서 리프 하나를 버리면 조건이 느슨해진다 → 결과는 **상위집합**.
|
|
2396
|
+
* Notion 보다 행이 더 보인다. 눈에 보이는 과다 표시이고, 데이터는 잃지 않는다.
|
|
2397
|
+
* - `or` 안에서 가지 하나를 버리면 조건이 조여진다 → 결과는 **부분집합**.
|
|
2398
|
+
* Notion 에 있던 행이 Obsidian 에서 **조용히 사라진다**. 이건 유실이다.
|
|
2399
|
+
* - `not` 은 안쪽을 버리면 부정의 의미 자체가 뒤집힌다.
|
|
2400
|
+
*
|
|
2401
|
+
* 그래서 `and` 는 번역 가능한 리프만 남기고, `or`/`not` 은 하나라도 못 옮기면 그룹
|
|
2402
|
+
* 전체를 포기한다. 포기한 것은 전부 {@link TranslationResult.untranslated} 로 보고돼
|
|
2403
|
+
* 사이드카 degrade 리포트에 실린다 — 조용히 넘어가는 경로는 없다.
|
|
2404
|
+
*/
|
|
2405
|
+
/** Bases 필터 노드 — 문자열 표현식이거나 and/or/not 그룹. */
|
|
2406
|
+
type BasesFilterNode = string | {
|
|
2407
|
+
readonly and: BasesFilterNode[];
|
|
2408
|
+
} | {
|
|
2409
|
+
readonly or: BasesFilterNode[];
|
|
2410
|
+
} | {
|
|
2411
|
+
readonly not: BasesFilterNode[];
|
|
2412
|
+
};
|
|
2413
|
+
interface TranslationResult {
|
|
2414
|
+
/** 번역된 필터. 옮길 수 있는 조건이 하나도 없으면 null. */
|
|
2415
|
+
readonly node: BasesFilterNode | null;
|
|
2416
|
+
/** 옮기지 못한 조건의 사람용 설명(중복 제거·정렬됨). */
|
|
2417
|
+
readonly untranslated: string[];
|
|
2418
|
+
}
|
|
2419
|
+
/** 속성 id/이름 → 프론트매터 키. 스키마에서 못 찾으면 undefined. */
|
|
2420
|
+
type PropertyNameResolver = (idOrName: string) => string | undefined;
|
|
2421
|
+
/**
|
|
2422
|
+
* Notion 필터 객체를 Bases 필터 노드로 옮긴다.
|
|
2423
|
+
*
|
|
2424
|
+
* @param filter Notion `views.retrieve` 가 준 필터 객체(`unknown` — 문법이 계속 늘어난다).
|
|
2425
|
+
* @param schema DB 속성 스키마. 속성 id 해소와 status 그룹 전개에 쓴다.
|
|
2426
|
+
* @param resolveName 속성 id(또는 이름) → 프론트매터 키 해소기.
|
|
2427
|
+
*/
|
|
2428
|
+
declare function translateNotionFilter(filter: unknown, schema: Record<string, BasePropertySchema>, resolveName: PropertyNameResolver): TranslationResult;
|
|
2429
|
+
|
|
1828
2430
|
/**
|
|
1829
2431
|
* DB 사이드카(`<db>.notion.json`) — `.base` 로 표현할 수 없는 Notion DB 메타데이터를
|
|
1830
2432
|
* 무손실 보존하는 단일 파일.
|
|
@@ -1876,7 +2478,12 @@ interface SidecarView {
|
|
|
1876
2478
|
}
|
|
1877
2479
|
interface DegradeNote {
|
|
1878
2480
|
readonly view: string;
|
|
1879
|
-
|
|
2481
|
+
/**
|
|
2482
|
+
* `view-unrepresentable` = 뷰가 `.base` 에서 통째로 누락.
|
|
2483
|
+
* `view-degraded` = 뷰는 남지만 다른 타입으로 격하(달력→표 등).
|
|
2484
|
+
* `setting-dropped` = 뷰는 그대로지만 설정 하나가 빠짐.
|
|
2485
|
+
*/
|
|
2486
|
+
readonly kind: "view-unrepresentable" | "view-degraded" | "setting-dropped";
|
|
1880
2487
|
readonly detail: string;
|
|
1881
2488
|
}
|
|
1882
2489
|
declare class SidecarGenerator {
|
|
@@ -1902,6 +2509,15 @@ declare class SidecarGenerator {
|
|
|
1902
2509
|
* 미표현 뷰는 이미 view-unrepresentable 로 통째 보고됨).
|
|
1903
2510
|
*/
|
|
1904
2511
|
private extractConfig;
|
|
2512
|
+
/**
|
|
2513
|
+
* 필터 손실을 **실제로 못 옮긴 것만** 보고한다.
|
|
2514
|
+
*
|
|
2515
|
+
* 예전엔 필터가 있기만 하면 "Notion 필터식은 .base 로 표현되지 않음"을 무조건 찍었다.
|
|
2516
|
+
* 이제 `.base` 가 뷰별 `filters:` 를 내보내므로 그 문구는 대부분의 경우 거짓이다.
|
|
2517
|
+
* 거짓 degrade 는 진짜 degrade 를 묻어 버리므로, `.base` 와 **같은 번역기**를 돌려
|
|
2518
|
+
* 남은 것만 적는다(두 산출물이 어긋날 여지를 없앤다).
|
|
2519
|
+
*/
|
|
2520
|
+
private noteFilterDegrade;
|
|
1905
2521
|
}
|
|
1906
2522
|
|
|
1907
2523
|
declare function sortEntries(entries: DBEntry[], sorts: NonNullable<ViewConfig["sorts"]>): DBEntry[];
|
|
@@ -2005,6 +2621,43 @@ declare function summarizeFidelity(perFile: ReadonlyArray<{
|
|
|
2005
2621
|
classification: FidelityClassification;
|
|
2006
2622
|
}>): FidelitySummary;
|
|
2007
2623
|
|
|
2624
|
+
/**
|
|
2625
|
+
* 렌더 감사 — "볼트에 쓰인 마크다운이 Obsidian 에서 깨져 보이는가"를 코드로 고정한다.
|
|
2626
|
+
*
|
|
2627
|
+
* {@link classifyBodyFidelity} 가 **내용이 남았는가**를 보고 {@link verifyPageCompleteness}
|
|
2628
|
+
* 가 **빠짐없이 왔는가**를 본다면, 여기는 **사람 눈에 어떻게 보이는가**를 본다. 셋은 서로의
|
|
2629
|
+
* 사각지대를 맡는다 — 실제로 이 트랙의 결함은 내용도 다 있고 개수도 맞는데 콜아웃이
|
|
2630
|
+
* 코드블록으로 오파싱되거나 표 구분행만 덩그러니 남는 형태였다.
|
|
2631
|
+
*
|
|
2632
|
+
* 원본 대조가 필요 없는 것만 담는다. 한 파일만 보고 판정할 수 있어야 동기화 하니스가
|
|
2633
|
+
* 볼트를 훑는 데 쓸 수 있다. 원본 NFM 과 견주는 지표(구조 드리프트·코드블록 경계)는
|
|
2634
|
+
* 회귀 코퍼스를 쥔 테스트 쪽에 남는다.
|
|
2635
|
+
*
|
|
2636
|
+
* 규칙 번호(①~⑧)는 트랙 초기 실측 조사에서 붙인 결함 분류를 그대로 잇는다.
|
|
2637
|
+
*/
|
|
2638
|
+
/** 한 줄만 보고 판정할 수 있는 렌더 결함. */
|
|
2639
|
+
interface RenderLineRule {
|
|
2640
|
+
readonly code: string;
|
|
2641
|
+
readonly label: string;
|
|
2642
|
+
readonly re: RegExp;
|
|
2643
|
+
}
|
|
2644
|
+
declare const RENDER_LINE_RULES: readonly RenderLineRule[];
|
|
2645
|
+
/** 발견된 렌더 결함 하나. */
|
|
2646
|
+
interface RenderFinding {
|
|
2647
|
+
readonly code: string;
|
|
2648
|
+
readonly label: string;
|
|
2649
|
+
readonly line: number;
|
|
2650
|
+
}
|
|
2651
|
+
/**
|
|
2652
|
+
* 표 구분행 — 가로 공백만 허용한다. `\s` 를 쓰면 **줄바꿈까지 삼킨다**.
|
|
2653
|
+
*
|
|
2654
|
+
* 그러면 빈 표 행(`| | | |`) 다음의 수평선(`---`)이 한 덩어리로 잡혀 구분행 하나가
|
|
2655
|
+
* 날조되고, 데이터 행이 하나 줄어든 것처럼 보인다.
|
|
2656
|
+
*/
|
|
2657
|
+
declare const SEPARATOR_BODY = "[ \\t:|-]*-{3,}";
|
|
2658
|
+
/** 볼트에 실제로 쓰인 마크다운을 훑어 렌더 결함을 모은다. */
|
|
2659
|
+
declare function lintRenderedMarkdown(markdown: string): RenderFinding[];
|
|
2660
|
+
|
|
2008
2661
|
declare function computeHash(content: string): string;
|
|
2009
2662
|
declare function computeBufferHash(buffer: Buffer): string;
|
|
2010
2663
|
|
|
@@ -2020,6 +2673,24 @@ declare function compactNotionId(id: string): string;
|
|
|
2020
2673
|
|
|
2021
2674
|
declare function sanitizeFileName(name: string): string;
|
|
2022
2675
|
|
|
2676
|
+
/**
|
|
2677
|
+
* `--path` 범위 판정 — 볼트 상대경로가 사용자가 지정한 범위 안에 있는지.
|
|
2678
|
+
*
|
|
2679
|
+
* 단순 `startsWith` 는 **경로 조각 경계를 보지 않아** 범위를 넘겨 짚는다.
|
|
2680
|
+
* `--path Notes` 가 `Notes-archive/비밀.md` · `Notes백업.md` 까지 끌어와, 사용자가
|
|
2681
|
+
* 의도한 폴더 밖 파일을 push/pull 대상에 올린다(범위 지정의 의미가 무너진다).
|
|
2682
|
+
*
|
|
2683
|
+
* 규칙은 셋뿐이다.
|
|
2684
|
+
* 1. 완전 일치 — `--path a/b.md` ↔ `a/b.md`
|
|
2685
|
+
* 2. 폴더 하위 — `--path a` ↔ `a/b.md` (경계 `/` 를 반드시 소비)
|
|
2686
|
+
* 3. 그 외는 불일치 — `--path a` ↔ `ab.md` ✗
|
|
2687
|
+
*
|
|
2688
|
+
* 사용자가 `--path a/` 처럼 슬래시를 붙여 쓰는 경우도 같은 범위로 본다.
|
|
2689
|
+
*/
|
|
2690
|
+
declare function matchesPathScope(path: string, scope: string): boolean;
|
|
2691
|
+
/** 범위 목록 중 하나라도 걸리면 true. 목록이 비어 있으면(범위 미지정) 항상 true. */
|
|
2692
|
+
declare function inAnyPathScope(path: string, scopes: readonly string[] | undefined): boolean;
|
|
2693
|
+
|
|
2023
2694
|
interface Logger {
|
|
2024
2695
|
warn(message: string, ...args: unknown[]): void;
|
|
2025
2696
|
error(message: string, ...args: unknown[]): void;
|
|
@@ -2077,4 +2748,4 @@ declare function isInternalPath(path: string): boolean;
|
|
|
2077
2748
|
/** 모든 보존 마커가 공유하는 브랜드 토큰. */
|
|
2078
2749
|
declare const MARKER_BRAND = "im-nobsidian";
|
|
2079
2750
|
|
|
2080
|
-
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 };
|
|
2751
|
+
export { BaseFileGenerator, type BaseFileOptions, type BasePropertyOption, type BasePropertySchema, type BaseStatusGroup, type BaseSyncOptions, type BasesFilterNode, type BasesViewMapping, type BasesViewType, BlockConverter, CONFIG_FILE, type CalendarEntry, CalloutRestorer, CalloutTransformer, ChangeDetector, ColorAnnotator, type CompletenessFailure, type CompletenessLocalSource, type CompletenessOptions, type CompletenessRemoteSource, type CompletenessReport, 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 DatabaseCompleteness, 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 PageCompletenessRemoteSource, type PageCompletenessReport, 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, RENDER_LINE_RULES, type RecordPendingInput, type RegisterFileInput, type RemoteChange, type RenderFinding, type RenderLineRule, type ResolutionChoice, type ResolutionResult, type RichTextSegment, SEPARATOR_BODY, 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, type TranslationResult, TreeMapper, UnsupportedBlockStripper, type UpsertSyncRecord, type VaultCompletenessReport, type VaultFS, type ViewConfig, ViewDataProvider, type ViewPropertyConfig, type ViewRenderData, type ViewType, type WatchSyncOptions, WatchSyncService, type WikilinkEntry, WikilinkResolver, basesViewMappingOf, basesViewTypeOf, classifyBodyFidelity, compactNotionId, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, inAnyPathScope, isInternalPath, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, notionIdsEqual, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, summarizeFidelity, threeWayMerge, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness };
|