@im-nobsidian/core 0.3.1 → 0.4.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 +1983 -329
- package/dist/index.js +17818 -5957
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { Client } from '@notionhq/client';
|
|
3
|
-
import { PageObjectResponse, BlockObjectResponse, PageMarkdownResponse, PartialBlockObjectResponse } from '@notionhq/client/build/src/api-endpoints.js';
|
|
3
|
+
import { PageObjectResponse, BlockObjectResponse, PageMarkdownResponse, PartialBlockObjectResponse, LanguageRequest } from '@notionhq/client/build/src/api-endpoints.js';
|
|
4
4
|
|
|
5
|
+
/** Notion 내부 통합 토큰의 머리 — 설정 검증과 CLI 의 입력 검사가 함께 읽는다. */
|
|
6
|
+
declare const NOTION_TOKEN_PREFIX = "ntn_";
|
|
7
|
+
/** DB 의 제목 속성 이름 기본값 — Notion 이 새 DB 에 붙이는 이름. */
|
|
8
|
+
declare const DEFAULT_TITLE_PROPERTY = "Name";
|
|
5
9
|
declare const DatabaseSyncSchema: z.ZodObject<{
|
|
6
10
|
databaseId: z.ZodString;
|
|
7
11
|
localFolder: z.ZodString;
|
|
@@ -81,20 +85,23 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
81
85
|
conflictStrategy: z.ZodDefault<z.ZodEnum<["local-first", "remote-first", "manual", "duplicate"]>>;
|
|
82
86
|
autoSync: z.ZodDefault<z.ZodBoolean>;
|
|
83
87
|
autoSyncInterval: z.ZodDefault<z.ZodNumber>;
|
|
88
|
+
fullReconcileInterval: z.ZodDefault<z.ZodNumber>;
|
|
84
89
|
deleteSync: z.ZodDefault<z.ZodBoolean>;
|
|
85
90
|
syncFiles: z.ZodDefault<z.ZodBoolean>;
|
|
86
91
|
}, "strip", z.ZodTypeAny, {
|
|
87
|
-
direction: "push" | "
|
|
88
|
-
conflictStrategy: "
|
|
92
|
+
direction: "push" | "both" | "pull";
|
|
93
|
+
conflictStrategy: "manual" | "local-first" | "remote-first" | "duplicate";
|
|
89
94
|
autoSync: boolean;
|
|
90
95
|
autoSyncInterval: number;
|
|
96
|
+
fullReconcileInterval: number;
|
|
91
97
|
deleteSync: boolean;
|
|
92
98
|
syncFiles: boolean;
|
|
93
99
|
}, {
|
|
94
|
-
direction?: "push" | "
|
|
95
|
-
conflictStrategy?: "
|
|
100
|
+
direction?: "push" | "both" | "pull" | undefined;
|
|
101
|
+
conflictStrategy?: "manual" | "local-first" | "remote-first" | "duplicate" | undefined;
|
|
96
102
|
autoSync?: boolean | undefined;
|
|
97
103
|
autoSyncInterval?: number | undefined;
|
|
104
|
+
fullReconcileInterval?: number | undefined;
|
|
98
105
|
deleteSync?: boolean | undefined;
|
|
99
106
|
syncFiles?: boolean | undefined;
|
|
100
107
|
}>;
|
|
@@ -103,25 +110,28 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
103
110
|
exclude: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
|
|
104
111
|
attachments: z.ZodDefault<z.ZodString>;
|
|
105
112
|
}, "strip", z.ZodTypeAny, {
|
|
113
|
+
attachments: string;
|
|
106
114
|
include: string[];
|
|
107
115
|
exclude: string[];
|
|
108
|
-
attachments: string;
|
|
109
116
|
}, {
|
|
117
|
+
attachments?: string | undefined;
|
|
110
118
|
include?: string[] | undefined;
|
|
111
119
|
exclude?: string[] | undefined;
|
|
112
|
-
attachments?: string | undefined;
|
|
113
120
|
}>;
|
|
114
121
|
conversion: z.ZodObject<{
|
|
115
122
|
preferMarkdownApi: z.ZodDefault<z.ZodBoolean>;
|
|
116
123
|
preserveMarkers: z.ZodDefault<z.ZodBoolean>;
|
|
117
124
|
frontmatterMapping: z.ZodDefault<z.ZodBoolean>;
|
|
118
125
|
imageDownload: z.ZodDefault<z.ZodEnum<["immediate", "lazy", "skip"]>>;
|
|
126
|
+
timeZone: z.ZodOptional<z.ZodEffects<z.ZodString, string, string>>;
|
|
119
127
|
}, "strip", z.ZodTypeAny, {
|
|
120
128
|
preferMarkdownApi: boolean;
|
|
121
129
|
preserveMarkers: boolean;
|
|
122
130
|
frontmatterMapping: boolean;
|
|
123
131
|
imageDownload: "immediate" | "lazy" | "skip";
|
|
132
|
+
timeZone?: string | undefined;
|
|
124
133
|
}, {
|
|
134
|
+
timeZone?: string | undefined;
|
|
125
135
|
preferMarkdownApi?: boolean | undefined;
|
|
126
136
|
preserveMarkers?: boolean | undefined;
|
|
127
137
|
frontmatterMapping?: boolean | undefined;
|
|
@@ -142,6 +152,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
142
152
|
mediaRetryBaseMs: z.ZodDefault<z.ZodNumber>;
|
|
143
153
|
fileConcurrency: z.ZodDefault<z.ZodNumber>;
|
|
144
154
|
maxFileSizeBytes: z.ZodDefault<z.ZodNumber>;
|
|
155
|
+
mediaDownloadTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
156
|
+
itemTimeoutMs: z.ZodDefault<z.ZodNumber>;
|
|
157
|
+
markdownCompletionMaxBlocks: z.ZodDefault<z.ZodNumber>;
|
|
145
158
|
}, "strip", z.ZodTypeAny, {
|
|
146
159
|
concurrency: number;
|
|
147
160
|
timeoutMs: number;
|
|
@@ -157,6 +170,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
157
170
|
mediaRetryBaseMs: number;
|
|
158
171
|
fileConcurrency: number;
|
|
159
172
|
maxFileSizeBytes: number;
|
|
173
|
+
mediaDownloadTimeoutMs: number;
|
|
174
|
+
itemTimeoutMs: number;
|
|
175
|
+
markdownCompletionMaxBlocks: number;
|
|
160
176
|
}, {
|
|
161
177
|
concurrency?: number | undefined;
|
|
162
178
|
timeoutMs?: number | undefined;
|
|
@@ -172,6 +188,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
172
188
|
mediaRetryBaseMs?: number | undefined;
|
|
173
189
|
fileConcurrency?: number | undefined;
|
|
174
190
|
maxFileSizeBytes?: number | undefined;
|
|
191
|
+
mediaDownloadTimeoutMs?: number | undefined;
|
|
192
|
+
itemTimeoutMs?: number | undefined;
|
|
193
|
+
markdownCompletionMaxBlocks?: number | undefined;
|
|
175
194
|
}>;
|
|
176
195
|
}, "strip", z.ZodTypeAny, {
|
|
177
196
|
version: 1;
|
|
@@ -190,23 +209,25 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
190
209
|
workspaceId?: string | undefined;
|
|
191
210
|
};
|
|
192
211
|
sync: {
|
|
193
|
-
direction: "push" | "
|
|
194
|
-
conflictStrategy: "
|
|
212
|
+
direction: "push" | "both" | "pull";
|
|
213
|
+
conflictStrategy: "manual" | "local-first" | "remote-first" | "duplicate";
|
|
195
214
|
autoSync: boolean;
|
|
196
215
|
autoSyncInterval: number;
|
|
216
|
+
fullReconcileInterval: number;
|
|
197
217
|
deleteSync: boolean;
|
|
198
218
|
syncFiles: boolean;
|
|
199
219
|
};
|
|
200
220
|
paths: {
|
|
221
|
+
attachments: string;
|
|
201
222
|
include: string[];
|
|
202
223
|
exclude: string[];
|
|
203
|
-
attachments: string;
|
|
204
224
|
};
|
|
205
225
|
conversion: {
|
|
206
226
|
preferMarkdownApi: boolean;
|
|
207
227
|
preserveMarkers: boolean;
|
|
208
228
|
frontmatterMapping: boolean;
|
|
209
229
|
imageDownload: "immediate" | "lazy" | "skip";
|
|
230
|
+
timeZone?: string | undefined;
|
|
210
231
|
};
|
|
211
232
|
advanced: {
|
|
212
233
|
concurrency: number;
|
|
@@ -223,6 +244,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
223
244
|
mediaRetryBaseMs: number;
|
|
224
245
|
fileConcurrency: number;
|
|
225
246
|
maxFileSizeBytes: number;
|
|
247
|
+
mediaDownloadTimeoutMs: number;
|
|
248
|
+
itemTimeoutMs: number;
|
|
249
|
+
markdownCompletionMaxBlocks: number;
|
|
226
250
|
};
|
|
227
251
|
}, {
|
|
228
252
|
version: 1;
|
|
@@ -241,19 +265,21 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
241
265
|
}[] | undefined;
|
|
242
266
|
};
|
|
243
267
|
sync: {
|
|
244
|
-
direction?: "push" | "
|
|
245
|
-
conflictStrategy?: "
|
|
268
|
+
direction?: "push" | "both" | "pull" | undefined;
|
|
269
|
+
conflictStrategy?: "manual" | "local-first" | "remote-first" | "duplicate" | undefined;
|
|
246
270
|
autoSync?: boolean | undefined;
|
|
247
271
|
autoSyncInterval?: number | undefined;
|
|
272
|
+
fullReconcileInterval?: number | undefined;
|
|
248
273
|
deleteSync?: boolean | undefined;
|
|
249
274
|
syncFiles?: boolean | undefined;
|
|
250
275
|
};
|
|
251
276
|
paths: {
|
|
277
|
+
attachments?: string | undefined;
|
|
252
278
|
include?: string[] | undefined;
|
|
253
279
|
exclude?: string[] | undefined;
|
|
254
|
-
attachments?: string | undefined;
|
|
255
280
|
};
|
|
256
281
|
conversion: {
|
|
282
|
+
timeZone?: string | undefined;
|
|
257
283
|
preferMarkdownApi?: boolean | undefined;
|
|
258
284
|
preserveMarkers?: boolean | undefined;
|
|
259
285
|
frontmatterMapping?: boolean | undefined;
|
|
@@ -274,6 +300,9 @@ declare const ConfigSchema: z.ZodObject<{
|
|
|
274
300
|
mediaRetryBaseMs?: number | undefined;
|
|
275
301
|
fileConcurrency?: number | undefined;
|
|
276
302
|
maxFileSizeBytes?: number | undefined;
|
|
303
|
+
mediaDownloadTimeoutMs?: number | undefined;
|
|
304
|
+
itemTimeoutMs?: number | undefined;
|
|
305
|
+
markdownCompletionMaxBlocks?: number | undefined;
|
|
277
306
|
};
|
|
278
307
|
}>;
|
|
279
308
|
type Config = z.infer<typeof ConfigSchema>;
|
|
@@ -291,6 +320,12 @@ interface SyncRecord {
|
|
|
291
320
|
readonly notionParentId: string | null;
|
|
292
321
|
readonly contentHash: string;
|
|
293
322
|
readonly notionLastEdited: string | null;
|
|
323
|
+
/** {@link RemoteObservation.lastEditedBy} — 모르면 null. */
|
|
324
|
+
readonly notionLastEditedBy: string | null;
|
|
325
|
+
/** {@link RemoteObservation.seenAt} — 모르면 null. */
|
|
326
|
+
readonly notionSeenAt: string | null;
|
|
327
|
+
/** 지난 동기화 사본에 해당하는 원격 본문의 지문(`remoteBodyFingerprint`). 모르면 null. */
|
|
328
|
+
readonly notionBodyFingerprint: string | null;
|
|
294
329
|
readonly localLastModified: string;
|
|
295
330
|
readonly syncDirection: SyncDirection;
|
|
296
331
|
readonly fileType: FileType;
|
|
@@ -302,6 +337,23 @@ interface SyncRecord {
|
|
|
302
337
|
readonly createdAt: string;
|
|
303
338
|
readonly updatedAt: string;
|
|
304
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* 원격 페이지를 본 기록(N-05). Notion 의 수정 시각은 분 단위로 잘려, 시각만으로는 우리가 본 뒤
|
|
342
|
+
* 같은 분 안에서 고친 것을 알 수 없다 — 누가 고쳤는지와 언제 봤는지를 함께 적는다.
|
|
343
|
+
*/
|
|
344
|
+
interface RemoteObservation {
|
|
345
|
+
/** Notion `last_edited_time` — 분 단위로 잘린 값. */
|
|
346
|
+
readonly lastEdited: string;
|
|
347
|
+
/** Notion `last_edited_by.id`. 응답에 없으면 null. */
|
|
348
|
+
readonly lastEditedBy: string | null;
|
|
349
|
+
/**
|
|
350
|
+
* 이 기기 시계로 본 때 — 그 실행을 시작한 때(실제로 읽은 때보다 이르다). 이때까지의 원격
|
|
351
|
+
* 편집은 레코드에 들어 있다. 모르면 null — 다음에 내용으로 확인한다.
|
|
352
|
+
*/
|
|
353
|
+
readonly seenAt: string | null;
|
|
354
|
+
/** 원격 본문 지문. 적지 않으면(undefined) 있던 값을 둔다 — 본문을 건드리지 않은 관측. */
|
|
355
|
+
readonly bodyFingerprint?: string | null;
|
|
356
|
+
}
|
|
305
357
|
interface LocalChange {
|
|
306
358
|
readonly path: string;
|
|
307
359
|
readonly type: "created" | "modified" | "deleted" | "moved";
|
|
@@ -309,12 +361,34 @@ interface LocalChange {
|
|
|
309
361
|
readonly previousHash: string | null;
|
|
310
362
|
readonly movedFrom?: string;
|
|
311
363
|
}
|
|
364
|
+
/**
|
|
365
|
+
* DB 행 frontmatter 의 속성 차이 — 지난 동기화 시점과 비교해 바뀐 것만(S-01).
|
|
366
|
+
* `title` 은 속성이 아니라 행 제목이라 여기 들어가지 않는다.
|
|
367
|
+
*/
|
|
368
|
+
interface RowPropertyChanges {
|
|
369
|
+
/** 새로 생겼거나 값이 바뀐 속성과 그 새 값. 날짜는 적힌 모양의 문자열로 되돌려 둔다. */
|
|
370
|
+
readonly changed: Readonly<Record<string, unknown>>;
|
|
371
|
+
/** 로컬에서 비웠거나 지운 속성 이름. */
|
|
372
|
+
readonly cleared: readonly string[];
|
|
373
|
+
}
|
|
312
374
|
interface RemoteChange {
|
|
313
375
|
readonly pageId: string;
|
|
314
376
|
readonly type: "created" | "modified" | "deleted" | "moved";
|
|
315
377
|
readonly lastEdited: string;
|
|
316
378
|
readonly previousEdited: string | null;
|
|
317
379
|
readonly movedFromParent?: string;
|
|
380
|
+
/**
|
|
381
|
+
* 추적 중인 노트의 볼트 경로. 화면이 내부 id 대신 노트 이름을 보이고, 그 노트만 받을 때 쓴다.
|
|
382
|
+
* 아직 볼트에 없는 새 페이지는 없다.
|
|
383
|
+
*/
|
|
384
|
+
readonly path?: string;
|
|
385
|
+
/** Notion 제목 — 볼트 경로가 아직 없는 새 페이지를 화면이 이름으로 보일 때 쓴다. */
|
|
386
|
+
readonly title?: string;
|
|
387
|
+
/**
|
|
388
|
+
* 수정 시각 · 편집자로는 바뀌었는지 가를 수 없다 — 같은 분 안의 편집일 수 있다(N-05).
|
|
389
|
+
* 받는 쪽이 내용으로 확인한다. `modified` 에만 붙는다.
|
|
390
|
+
*/
|
|
391
|
+
readonly unverified?: boolean;
|
|
318
392
|
}
|
|
319
393
|
interface Conflict {
|
|
320
394
|
readonly syncRecord: SyncRecord;
|
|
@@ -326,7 +400,16 @@ interface Conflict {
|
|
|
326
400
|
}
|
|
327
401
|
interface ProgressItem {
|
|
328
402
|
readonly path: string;
|
|
329
|
-
|
|
403
|
+
/** `move` — 볼트에서 옮기거나 이름을 바꾼 노트 · 폴더를 Notion 에 반영한다(push). */
|
|
404
|
+
readonly operation: "create" | "update" | "delete" | "move";
|
|
405
|
+
/** 받기(pull) · 올리기(push) 중 어느 쪽 항목인가 — `sync` 만 채운다. 둘을 한 번에 돌려 가를 수 없다. */
|
|
406
|
+
readonly direction?: "pull" | "push";
|
|
407
|
+
/**
|
|
408
|
+
* 받아 봤지만 볼트에 쓰지 않았다(pull) — 바뀐 것이 없거나 로컬을 지켰다. 진행 수에는 들지만 화면이 바뀐
|
|
409
|
+
* 노트로 적지 않는다. 다시 그리기(B14)는
|
|
410
|
+
* 받아 둔 노트를 모두 받아 보므로, 적으면 바뀌지 않은 노트마다 「updated」 줄이 찍혔다.
|
|
411
|
+
*/
|
|
412
|
+
readonly unchanged?: boolean;
|
|
330
413
|
}
|
|
331
414
|
type ProgressCallback = (current: number, total: number, item: ProgressItem) => void;
|
|
332
415
|
/** push/pull/sync 가 공통으로 받는 동기화 옵션. */
|
|
@@ -337,22 +420,76 @@ interface BaseSyncOptions {
|
|
|
337
420
|
readonly onProgress?: ProgressCallback;
|
|
338
421
|
readonly signal?: AbortSignal;
|
|
339
422
|
}
|
|
423
|
+
/** 원격까지 보는 상태 확인(`status`)의 옵션. */
|
|
424
|
+
interface StatusOptions {
|
|
425
|
+
/** 취소하면 원격을 훑다 멈추고 {@link OperationAbortedError} 를 던진다 — 반쯤 본 상태를 돌려주지 않는다. */
|
|
426
|
+
readonly signal?: AbortSignal;
|
|
427
|
+
}
|
|
340
428
|
interface PushOptions extends BaseSyncOptions {
|
|
341
429
|
readonly excludePaths?: string[];
|
|
342
430
|
}
|
|
343
|
-
|
|
431
|
+
interface PullOptions extends BaseSyncOptions {
|
|
432
|
+
/**
|
|
433
|
+
* Notion 에서 바뀌지 않은 받아 둔 노트도 지금 변환기로 다시 그린다(B14, `sync/rerender-targets.ts`).
|
|
434
|
+
* 볼트에서 고친 노트는 건드리지 않는다. dry-run 에서는 쓰지 않는다 — 다시 그려 봐야 바뀌는지 안다.
|
|
435
|
+
*/
|
|
436
|
+
readonly rerender?: boolean;
|
|
437
|
+
}
|
|
344
438
|
type SyncOptions = BaseSyncOptions;
|
|
345
439
|
interface PushResult {
|
|
346
440
|
readonly created: number;
|
|
347
441
|
readonly updated: number;
|
|
348
442
|
readonly deleted: number;
|
|
443
|
+
/**
|
|
444
|
+
* Notion 에 옮김을 반영한 노트 · 폴더 수. 옮기며 본문도 고친 노트도 여기서만 센다 — updated 와
|
|
445
|
+
* 겹치지 않는다. 예전에는 updated 에 섞여, 이름만 바꾼 노트도 「수정」 으로 보였다.
|
|
446
|
+
*/
|
|
447
|
+
readonly moved: number;
|
|
349
448
|
readonly failed: FailedOperation[];
|
|
350
449
|
readonly duration: number;
|
|
351
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* 전체 대조를 한 이유 (ADR-027).
|
|
453
|
+
*
|
|
454
|
+
* - `first` — 받은 것이 없다(처음 pull · 빈 상태 DB).
|
|
455
|
+
* - `forced` — `--force`.
|
|
456
|
+
* - `database-mode` — 루트가 DB 하나라 그 DB 조회가 곧 전체 대조다.
|
|
457
|
+
* - `every-pull` — 주기(`sync.fullReconcileInterval`)가 0 이다.
|
|
458
|
+
* - `due` — 주기가 됐다.
|
|
459
|
+
*/
|
|
460
|
+
type FullScanReason = "first" | "forced" | "database-mode" | "every-pull" | "due";
|
|
461
|
+
/**
|
|
462
|
+
* 이번 실행이 원격을 얼마나 훑었나 (ADR-027). `incremental` 은 지난 pull 뒤에 바뀐 페이지 · 행 · DB 만
|
|
463
|
+
* 찾았다는 뜻이다 — 원격에서 지운 것 · 범위 밖으로 옮긴 것은 다음 전체 대조가 반영한다. 화면은 이것으로
|
|
464
|
+
* 그 사실과 다음 전체 대조 시각을 알린다.
|
|
465
|
+
*/
|
|
466
|
+
interface RemoteScanInfo {
|
|
467
|
+
readonly kind: "full" | "incremental";
|
|
468
|
+
/** 전체 대조한 이유 — `full` 에만. */
|
|
469
|
+
readonly reason?: FullScanReason;
|
|
470
|
+
/** 마지막으로 전체 대조를 마친 때. 한 번도 없으면 null. */
|
|
471
|
+
readonly lastFullAt: string | null;
|
|
472
|
+
/** 다음 전체 대조 예정 시각 — `incremental` 에만. null 이면 이미 때가 됐다(다음 pull 이 한다). */
|
|
473
|
+
readonly nextFullAt: string | null;
|
|
474
|
+
/**
|
|
475
|
+
* Notion 에서 지운 노트가 아직 볼트에 반영되지 않았을 수 있나 — 바뀐 것만 찾았고 원격 삭제를 볼트에
|
|
476
|
+
* 반영하는 설정(`sync.deleteSync`)일 때. 다음 전체 대조가 반영한다. 설정이 꺼져 있으면 원격 삭제는
|
|
477
|
+
* 볼트에 반영하지 않으므로 늘 false 다.
|
|
478
|
+
*/
|
|
479
|
+
readonly deletionsDeferred: boolean;
|
|
480
|
+
/** 바뀐 것이 보이지 않아 조회하지 않은 DB 수 — pull 에만. */
|
|
481
|
+
readonly skippedDatabases?: number;
|
|
482
|
+
}
|
|
352
483
|
interface PullResult {
|
|
353
484
|
readonly created: number;
|
|
354
485
|
readonly updated: number;
|
|
355
486
|
readonly deleted: number;
|
|
487
|
+
/**
|
|
488
|
+
* 로컬에서 사라졌다가 리모트 원본으로 되살린 파일 수.
|
|
489
|
+
* updated 와 분리해 보고한다 — "수정 N건" 에 섞이면 사용자는 자기 볼트에서
|
|
490
|
+
* 파일이 없어졌다가 복구됐다는 사실 자체를 알 수 없다.
|
|
491
|
+
*/
|
|
492
|
+
readonly restored: number;
|
|
356
493
|
readonly conflicts: Conflict[];
|
|
357
494
|
readonly writtenPaths: string[];
|
|
358
495
|
readonly failed: FailedOperation[];
|
|
@@ -360,6 +497,8 @@ interface PullResult {
|
|
|
360
497
|
readonly imageCount: number;
|
|
361
498
|
readonly fileCount: number;
|
|
362
499
|
readonly linkCount: number;
|
|
500
|
+
/** 원격을 얼마나 훑었나. 원격을 보지 않은 실행(받기 방향이 아님 · 취소)에는 없다. */
|
|
501
|
+
readonly remoteScan?: RemoteScanInfo;
|
|
363
502
|
}
|
|
364
503
|
interface SyncResult {
|
|
365
504
|
readonly push: PushResult;
|
|
@@ -367,13 +506,42 @@ interface SyncResult {
|
|
|
367
506
|
readonly conflicts: Conflict[];
|
|
368
507
|
readonly duration: number;
|
|
369
508
|
}
|
|
509
|
+
/** 볼트에서 옮기거나 이름을 바꾼 폴더 — 다음 push 가 Notion 에 반영한다(DB 폴더는 볼트 쪽만). */
|
|
510
|
+
interface FolderMoveChange {
|
|
511
|
+
readonly from: string;
|
|
512
|
+
readonly to: string;
|
|
513
|
+
}
|
|
514
|
+
/**
|
|
515
|
+
* 변경 하나를 줄 비교로 보이는 두 글 — `SyncOrchestrator.localChangeDiff` · `remoteChangeDiff` 가 내고,
|
|
516
|
+
* `lineDiff` 로 견준다.
|
|
517
|
+
*/
|
|
518
|
+
interface ChangeDiff {
|
|
519
|
+
/** 볼트 경로 — 옮긴 노트는 새 자리. */
|
|
520
|
+
readonly path: string;
|
|
521
|
+
readonly type: LocalChange["type"];
|
|
522
|
+
/** 옮긴 노트의 옛 자리. */
|
|
523
|
+
readonly movedFrom?: string;
|
|
524
|
+
/** 지난 동기화 때의 글. 새 노트는 null — 견줄 옛 글이 없다. */
|
|
525
|
+
readonly before: string | null;
|
|
526
|
+
/** 지금 글 — 로컬 변경은 볼트의 글, 원격 변경은 Notion 의 글. 지운 노트 · 페이지는 null. */
|
|
527
|
+
readonly after: string | null;
|
|
528
|
+
}
|
|
370
529
|
interface StatusResult {
|
|
371
530
|
readonly localChanges: LocalChange[];
|
|
531
|
+
/** 옮긴 폴더. 그 안의 노트는 `localChanges` 에 `moved` 로 따로 있다. */
|
|
532
|
+
readonly folderMoves: readonly FolderMoveChange[];
|
|
372
533
|
readonly remoteChanges: RemoteChange[];
|
|
373
534
|
readonly conflicts: Conflict[];
|
|
374
535
|
readonly conflictRecords: SyncRecord[];
|
|
375
536
|
readonly pendingOperations: number;
|
|
376
537
|
readonly lastSyncAt: string | null;
|
|
538
|
+
/**
|
|
539
|
+
* 마지막으로 원격을 전체 대조한 pull 이 시작한 때. 한 번도 없으면 null. Notion 에서 지운 노트는 전체
|
|
540
|
+
* 대조 때 볼트에 반영된다 — 화면은 이것으로 그때를 알린다 (ADR-027).
|
|
541
|
+
*/
|
|
542
|
+
readonly lastFullScanAt: string | null;
|
|
543
|
+
/** 원격을 얼마나 훑었나. 로컬만 본 확인(`statusLocal`)에는 없다. */
|
|
544
|
+
readonly remoteScan?: RemoteScanInfo;
|
|
377
545
|
}
|
|
378
546
|
interface FailedOperation {
|
|
379
547
|
readonly path: string;
|
|
@@ -381,6 +549,17 @@ interface FailedOperation {
|
|
|
381
549
|
readonly error: string;
|
|
382
550
|
}
|
|
383
551
|
|
|
552
|
+
/** Notion 이 돌려준 본문에 실린 자식 하나. */
|
|
553
|
+
interface ChildTag {
|
|
554
|
+
readonly kind: "page" | "database";
|
|
555
|
+
/** 하이픈 없는 32자리 hex. */
|
|
556
|
+
readonly id: string;
|
|
557
|
+
/** 태그 안의 제목 그대로(굵게 `**` 가 섞여 있을 수 있다). */
|
|
558
|
+
readonly title: string;
|
|
559
|
+
/** 태그 원문 — 복원은 받은 그대로 다시 싣는다(실측: 받은 태그를 보내면 자식이 그대로 남는다). */
|
|
560
|
+
readonly tag: string;
|
|
561
|
+
}
|
|
562
|
+
|
|
384
563
|
type ConversionPath = "markdown-api" | "block-api";
|
|
385
564
|
interface ProcessorInput {
|
|
386
565
|
readonly content: string;
|
|
@@ -402,6 +581,23 @@ interface ProcessorMetadata {
|
|
|
402
581
|
* true: 무조건 재간격 / false: 무동작 / 미지정: 내용 기반 휴리스틱 폴백.
|
|
403
582
|
*/
|
|
404
583
|
readonly notionExportCompact?: boolean;
|
|
584
|
+
/**
|
|
585
|
+
* pull 직전 로컬 노트 원문 — 있을 때만. Notion 에 남길 자리가 없는 표기(코드 펜스의 원래 언어 ·
|
|
586
|
+
* 펜스 기호 S-20, 주석 S-29)를 되살리는 근거다. 호출측이 로컬 파일을 읽어 넘긴다.
|
|
587
|
+
*/
|
|
588
|
+
readonly localContent?: string;
|
|
589
|
+
/**
|
|
590
|
+
* 행 노트에서 Notion 이 정하는 frontmatter 키 — 행을 받을 때만. 받은 속성을 로컬 frontmatter 에 합칠 때
|
|
591
|
+
* 이 밖의 키(`aliases` 등)는 로컬 것으로 두고, 키 차례 · 적은 모양도 로컬을 따른다(F-08).
|
|
592
|
+
*/
|
|
593
|
+
readonly notionKeys?: readonly string[];
|
|
594
|
+
/**
|
|
595
|
+
* 받은 본문(원시 글)에 실린 자식 페이지 · 자식 DB — 페이지를 받을 때만. push 는 가리키는 줄이 없는 자식을
|
|
596
|
+
* 본문 끝에 되돌려 놓으므로(`restoreChildTags`), 로컬 표기를 되살릴 때 보낼 꼴을 그와 같이 만든다.
|
|
597
|
+
*/
|
|
598
|
+
readonly remoteChildren?: readonly ChildTag[];
|
|
599
|
+
/** Markdown API 로 보낼 수 없어 본문을 쓴 뒤 블록으로 채울 코드({@link DeferredCode}). */
|
|
600
|
+
readonly deferredCode?: DeferredCode[];
|
|
405
601
|
readonly [key: string]: unknown;
|
|
406
602
|
}
|
|
407
603
|
interface ConversionContext {
|
|
@@ -420,6 +616,27 @@ interface ConversionResult {
|
|
|
420
616
|
readonly properties: Record<string, unknown>;
|
|
421
617
|
readonly images: ImageReference[];
|
|
422
618
|
readonly preserveMarkers: PreserveMarker[];
|
|
619
|
+
readonly deferredCode: DeferredCode[];
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* 본문을 쓴 뒤 블록으로 채울 코드(S-22). Notion Markdown API 는 코드에 ``` 로 시작하는 줄이 있으면
|
|
623
|
+
* 어떤 펜스로 보내도 그 줄에서 블록을 가른다 — 코드 자리에 자리표시(`token`)만 보내고, 페이지를
|
|
624
|
+
* 쓴 뒤 그 자리표시를 가진 코드 블록의 글을 `code` 로 바꾼다.
|
|
625
|
+
*/
|
|
626
|
+
interface DeferredCode {
|
|
627
|
+
/** 코드 블록에 대신 보낸 자리표시 — 페이지 안에서 하나뿐이다. */
|
|
628
|
+
readonly token: string;
|
|
629
|
+
/** Obsidian 이 보여 주는 그대로의 코드 — 컨테이너 접두 · 펜스 들여쓰기를 뗀 것. */
|
|
630
|
+
readonly code: string;
|
|
631
|
+
}
|
|
632
|
+
/**
|
|
633
|
+
* 북마크 블록의 주소와 캡션(F-09). Markdown API 는 북마크를 주소 · 캡션 없는 자리 태그로만 보내,
|
|
634
|
+
* 블록으로 따로 읽는다(`NotionClient.getBookmarks`).
|
|
635
|
+
*/
|
|
636
|
+
interface BookmarkTarget {
|
|
637
|
+
readonly url: string;
|
|
638
|
+
/** 캡션의 글 — 없으면 빈 문자열. */
|
|
639
|
+
readonly caption: string;
|
|
423
640
|
}
|
|
424
641
|
interface PreserveMarker {
|
|
425
642
|
readonly type: string;
|
|
@@ -485,6 +702,37 @@ interface DatabaseViewsConfig {
|
|
|
485
702
|
readonly lastSynced: string;
|
|
486
703
|
readonly views: ViewConfig[];
|
|
487
704
|
}
|
|
705
|
+
/** select/multi_select/status 의 선택지 하나. */
|
|
706
|
+
interface BasePropertyOption {
|
|
707
|
+
readonly name: string;
|
|
708
|
+
readonly color?: string;
|
|
709
|
+
/**
|
|
710
|
+
* Notion 내부 옵션 id. 사람이 보는 산출물에는 절대 나가지 않지만,
|
|
711
|
+
* status 그룹(`BaseStatusGroup.optionIds`)을 옵션명으로 되돌리는 유일한 열쇠다 —
|
|
712
|
+
* 뷰 필터가 옵션명 대신 그룹명으로 오기 때문에(예: `["To-do"]`) 이게 없으면
|
|
713
|
+
* 필터를 옮길 수 없다.
|
|
714
|
+
*/
|
|
715
|
+
readonly id?: string;
|
|
716
|
+
}
|
|
717
|
+
/** status 속성의 그룹(To-do/In progress/Complete). 소속 옵션을 id 로 가리킨다. */
|
|
718
|
+
interface BaseStatusGroup {
|
|
719
|
+
readonly name: string;
|
|
720
|
+
readonly color?: string;
|
|
721
|
+
readonly optionIds?: string[];
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* Notion DB 속성 스키마 — `.base` 생성·사이드카·필터 번역이 함께 쓰는 SSOT.
|
|
725
|
+
*
|
|
726
|
+
* 같은 모양을 세 군데(클라이언트 반환형·생성기 입력형)에 따로 적어 두면 한쪽만
|
|
727
|
+
* 넓혔을 때 조용히 어긋난다. 실제로 옵션 `id` 가 클라이언트에서만 빠져 있어
|
|
728
|
+
* status 그룹 필터를 옮길 수 없었다.
|
|
729
|
+
*/
|
|
730
|
+
interface BasePropertySchema {
|
|
731
|
+
readonly id: string;
|
|
732
|
+
readonly type: string;
|
|
733
|
+
readonly options?: BasePropertyOption[];
|
|
734
|
+
readonly groups?: BaseStatusGroup[];
|
|
735
|
+
}
|
|
488
736
|
interface PageCover {
|
|
489
737
|
readonly type: "file" | "external";
|
|
490
738
|
readonly url: string;
|
|
@@ -515,7 +763,8 @@ declare class ConfigManager {
|
|
|
515
763
|
}
|
|
516
764
|
|
|
517
765
|
interface IStateDB {
|
|
518
|
-
|
|
766
|
+
/** 닫는다 — 파일에 남은 쓰기를 마쳐야 하는 구현(플러그인의 sql.js)은 그 일을 Promise 로 돌려준다. */
|
|
767
|
+
close(): void | Promise<void>;
|
|
519
768
|
getByPath(path: string): SyncRecord | null;
|
|
520
769
|
getByNotionId(pageId: string): SyncRecord | null;
|
|
521
770
|
getByStatus(status: SyncStatus): SyncRecord[];
|
|
@@ -523,7 +772,10 @@ interface IStateDB {
|
|
|
523
772
|
upsert(record: UpsertSyncRecord): SyncRecord;
|
|
524
773
|
updateStatus(id: string, status: SyncStatus): void;
|
|
525
774
|
updateHash(id: string, hash: string, snapshot?: Buffer | Uint8Array | null): void;
|
|
526
|
-
|
|
775
|
+
/** 원격을 본 기록을 적는다(N-05). 지문을 적지 않으면 있던 지문을 둔다. */
|
|
776
|
+
setRemoteObservation(id: string, observation: RemoteObservation): void;
|
|
777
|
+
/** 원격 본문 지문만 바꾼다 — 본문을 보냈지만 수정 시각은 올리지 않을 때. */
|
|
778
|
+
setNotionBodyFingerprint(id: string, fingerprint: string | null): void;
|
|
527
779
|
updateStatCache(id: string, mtime: string, fileSize: number): void;
|
|
528
780
|
setNotionParentId(id: string, parentId: string): void;
|
|
529
781
|
/** 레코드의 로컬 경로를 갱신한다 (파일 rename/move 추적용). */
|
|
@@ -553,11 +805,19 @@ interface IStateDB {
|
|
|
553
805
|
}
|
|
554
806
|
|
|
555
807
|
declare class StateDB implements IStateDB {
|
|
808
|
+
private readonly lock;
|
|
556
809
|
private readonly db;
|
|
557
810
|
private constructor();
|
|
811
|
+
/**
|
|
812
|
+
* 상태 DB 파일을 연다 — 없으면 새로 만든다. 한 볼트의 상태 DB 는 한 곳만 연다 — 플러그인이나 다른 CLI 가 쓰는
|
|
813
|
+
* 중이면 {@link StateDbLockedError} 를 던진다(ADR-026). 저장된 파일이 비었거나 잘렸으면 빈 DB 로 열지 않고
|
|
814
|
+
* {@link SavedStateDbError} 를 던진다. 예전에는 0 · 1바이트 파일을 빈 DB 로 열고 닫을 때 빈 DB 로 덮었다.
|
|
815
|
+
*/
|
|
558
816
|
static open(dbPath: string): StateDB;
|
|
817
|
+
private static openLocked;
|
|
559
818
|
private migrate;
|
|
560
819
|
private getSchemaVersion;
|
|
820
|
+
/** 닫고 잠금을 푼다 — 닫는 사이 다른 곳이 열지 않게 닫은 뒤에 푼다. */
|
|
561
821
|
close(): void;
|
|
562
822
|
getByPath(path: string): SyncRecord | null;
|
|
563
823
|
getByNotionId(pageId: string): SyncRecord | null;
|
|
@@ -566,7 +826,8 @@ declare class StateDB implements IStateDB {
|
|
|
566
826
|
upsert(record: UpsertSyncRecord): SyncRecord;
|
|
567
827
|
updateStatus(id: string, status: SyncStatus): void;
|
|
568
828
|
updateHash(id: string, hash: string, snapshot?: Buffer | null): void;
|
|
569
|
-
|
|
829
|
+
setRemoteObservation(id: string, observation: RemoteObservation): void;
|
|
830
|
+
setNotionBodyFingerprint(id: string, fingerprint: string | null): void;
|
|
570
831
|
updateStatCache(id: string, mtime: string, fileSize: number): void;
|
|
571
832
|
setNotionParentId(id: string, parentId: string): void;
|
|
572
833
|
updatePath(id: string, newPath: string): void;
|
|
@@ -602,6 +863,9 @@ interface UpsertSyncRecord {
|
|
|
602
863
|
readonly notionParentId?: string | null;
|
|
603
864
|
readonly contentHash: string;
|
|
604
865
|
readonly notionLastEdited?: string | null;
|
|
866
|
+
readonly notionLastEditedBy?: string | null;
|
|
867
|
+
readonly notionSeenAt?: string | null;
|
|
868
|
+
readonly notionBodyFingerprint?: string | null;
|
|
605
869
|
readonly localLastModified: string;
|
|
606
870
|
readonly syncDirection: SyncRecord["syncDirection"];
|
|
607
871
|
readonly fileType: SyncRecord["fileType"];
|
|
@@ -647,26 +911,339 @@ interface RecordPendingInput {
|
|
|
647
911
|
readonly payload?: string | null;
|
|
648
912
|
}
|
|
649
913
|
|
|
914
|
+
/** 스키마 하나 — 이 버전보다 낮은 DB 에 `sql` 을 돌린다. `sql` 은 끝에 `schema_version` 을 올린다. */
|
|
915
|
+
interface StateDbMigration {
|
|
916
|
+
readonly version: number;
|
|
917
|
+
readonly sql: string;
|
|
918
|
+
}
|
|
919
|
+
/**
|
|
920
|
+
* 상태 DB 스키마 — 차례대로. CLI(better-sqlite3)와 플러그인(sql.js)이 이 목록 하나를 돈다. 예전에는 플러그인이
|
|
921
|
+
* SQL 을 손으로 베껴, 처음 두 스키마의 CHECK 제약이 플러그인 DB 에만 없었다.
|
|
922
|
+
*/
|
|
923
|
+
declare const STATE_DB_MIGRATIONS: readonly StateDbMigration[];
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* 상태 DB 를 지금 쓸 수 없다 — 사용자가 풀 수 있는 실패다. 파일이 깨졌거나({@link SavedStateDbError}), 다른 곳이
|
|
927
|
+
* 쓰는 중이거나({@link StateDbLockedError}), CLI 가 남긴 기록이 아직 WAL 파일에만 있다({@link PendingWalError}).
|
|
928
|
+
*
|
|
929
|
+
* 할 일(`guidance`)을 같이 싣는다. 엔진을 띄우지 못한 것 같은 다른 실패와 가른다 — 거기에 할 일을 붙이면 멀쩡한
|
|
930
|
+
* 기록을 치우게 된다. 화면(CLI · 플러그인)은 이 종류인지만 보고, 다시 해 보는 법만 넘긴다.
|
|
931
|
+
*
|
|
932
|
+
* @see ./saved-state-db-error.ts
|
|
933
|
+
* @see ./state-lock.ts
|
|
934
|
+
*/
|
|
935
|
+
declare abstract class StateDbUnavailableError extends Error {
|
|
936
|
+
/** 할 일 — 화면마다 다시 해 보는 법(`retry`, 예: 「명령을 다시 실행하세요」)만 다르다. */
|
|
937
|
+
abstract guidance(retry: string): string;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
/**
|
|
941
|
+
* 저장된 상태 DB 파일로 DB 를 열지 못했다 — 비었거나 잘렸거나 상태 DB 가 아닌 파일이다. 두 구현(CLI 의
|
|
942
|
+
* better-sqlite3 · 플러그인의 sql.js)이 같이 던진다. 엔진을 띄우지 못한 것 같은 다른 실패와 가른다 — 파일을
|
|
943
|
+
* 치우라는 안내({@link SavedStateDbError.guidance})는 이 경우에만 맞다.
|
|
944
|
+
*/
|
|
945
|
+
declare class SavedStateDbError extends StateDbUnavailableError {
|
|
946
|
+
constructor(message: string);
|
|
947
|
+
/**
|
|
948
|
+
* 동기화 기록 표가 없다 — SQLite 는 비었거나(0바이트) 앞 한 바이트만 남은 파일을 «빈 DB» 로 연다. 보지 않으면
|
|
949
|
+
* 기록을 잃은 채 처음부터 시작하고, 다음 push 가 모든 노트의 페이지를 또 만든다.
|
|
950
|
+
*/
|
|
951
|
+
static noTables(bytes: number): SavedStateDbError;
|
|
952
|
+
/** SQLite 가 파일을 열지 못했다 — 잘렸거나(malformed) 상태 DB 가 아니다(not a database). */
|
|
953
|
+
static unreadable(error: unknown): SavedStateDbError;
|
|
954
|
+
/**
|
|
955
|
+
* 파일을 사본으로 바꾸거나 치운다. 폴더 이름이 점으로 시작해 Obsidian 의 파일 탐색기에는 보이지 않으므로 볼트
|
|
956
|
+
* 폴더에서 찾으라고 적는다.
|
|
957
|
+
*/
|
|
958
|
+
guidance(retry: string): string;
|
|
959
|
+
}
|
|
960
|
+
/**
|
|
961
|
+
* 저장된 파일에 동기화 기록 표가 있는지 묻는 조회 — 한 번이라도 쓴 파일에는 마이그레이션이 만든 표가 있다. 한
|
|
962
|
+
* 페이지 이상 잘린 파일은 SQLite 가 이 조회에서 던진다(malformed).
|
|
963
|
+
*/
|
|
964
|
+
declare const SAVED_STATE_TABLES_QUERY = "SELECT 1 FROM sqlite_master WHERE type = 'table' AND name = 'sync_metadata'";
|
|
965
|
+
|
|
966
|
+
/**
|
|
967
|
+
* 위키링크 글자 → `wikilink_map` 행을 찾는 조회들 — 앞에서부터 처음 맞는 행을 쓴다.
|
|
968
|
+
*
|
|
969
|
+
* CLI 의 better-sqlite3(`StateDB`)와 플러그인의 sql.js(`SqlJsStateDB`)가 같은 차례 · 같은 SQL 을 쓰도록
|
|
970
|
+
* 여기 한 벌만 둔다. 예전에는 두 곳에 손으로 베껴 있었다.
|
|
971
|
+
*
|
|
972
|
+
* - 제목 → 별칭 → 파일 이름 차례. 파일 이름은 `[[노트]]` 와 `[[폴더/노트]]` 둘 다 받는다.
|
|
973
|
+
* 예전 조회(`LIKE '%/노트.md'`)는 슬래시가 앞에 있어야 맞아서 볼트 맨 위 노트와 경로 붙은 링크를 못 찾았다 —
|
|
974
|
+
* 제목이 파일 이름과 다른 노트(파일 이름에 못 쓰는 글자를 바꾼 노트)로 가는 링크가 Notion 에서 글로 남았다.
|
|
975
|
+
* - LIKE 의 `%` · `_` 는 글자로 견준다. 예전에는 `a_b` 가 `a-b.md` 에도 맞았다.
|
|
976
|
+
*/
|
|
977
|
+
interface WikilinkLookup {
|
|
978
|
+
readonly sql: string;
|
|
979
|
+
readonly params: (text: string) => string[];
|
|
980
|
+
}
|
|
981
|
+
declare const WIKILINK_LOOKUPS: readonly WikilinkLookup[];
|
|
982
|
+
|
|
983
|
+
/** 상태 DB 파일 옆의 잠금 파일. */
|
|
984
|
+
declare function stateDbLockPath(dbPath: string): string;
|
|
985
|
+
/** 상태 DB 를 여는 도구. */
|
|
986
|
+
type StateDbTool = "cli" | "plugin";
|
|
987
|
+
/** 잠금을 잡는 쪽 — 화면에 보일 이름(`label`, 예: 플러그인 이름)은 있으면 적는다. */
|
|
988
|
+
interface StateLockClaim {
|
|
989
|
+
readonly tool: StateDbTool;
|
|
990
|
+
readonly label?: string;
|
|
991
|
+
}
|
|
992
|
+
/** 잠금 파일에 적는 주인. */
|
|
993
|
+
interface StateLockOwner extends StateLockClaim {
|
|
994
|
+
readonly pid: number;
|
|
995
|
+
/** PID 가 같은 뜻을 갖는 범위 — {@link processScope}. */
|
|
996
|
+
readonly scope: string;
|
|
997
|
+
/** 잠금을 잡은 실행 환경 — 같은 PID 의 옛 환경이 남긴 잠금을 가른다({@link processContext}). */
|
|
998
|
+
readonly context: string;
|
|
999
|
+
readonly startedAt: string;
|
|
1000
|
+
/** 잠금마다 새로 만드는 표 — 넘겨받힌 뒤 남의 잠금을 지우지 않도록 내 것인지 가른다. */
|
|
1001
|
+
readonly token: string;
|
|
1002
|
+
}
|
|
1003
|
+
/** 잠금이 보는 바깥 — 시험이 갈아 끼운다. */
|
|
1004
|
+
interface StateLockEnv {
|
|
1005
|
+
readonly pid: number;
|
|
1006
|
+
readonly scope: string;
|
|
1007
|
+
readonly context: string;
|
|
1008
|
+
now(): number;
|
|
1009
|
+
isAlive(pid: number): boolean;
|
|
1010
|
+
}
|
|
1011
|
+
declare class StateLock {
|
|
1012
|
+
private readonly path;
|
|
1013
|
+
readonly owner: StateLockOwner;
|
|
1014
|
+
private readonly env;
|
|
1015
|
+
private heartbeat;
|
|
1016
|
+
private constructor();
|
|
1017
|
+
/**
|
|
1018
|
+
* 잠금을 잡는다. 다른 곳이 쥐고 있으면 {@link StateDbLockedError} 를 던진다. 주인이 끝나지 않고 죽었으면 넘겨받는다
|
|
1019
|
+
* — 같은 PID 공간이면 PID 가 없거나 이 프로세스의 옛 실행 환경일 때, 아니면 신호가
|
|
1020
|
+
* {@link STATE_LOCK_STALE_MS} 넘게 끊겼을 때다({@link isAbandoned}).
|
|
1021
|
+
*/
|
|
1022
|
+
static acquire(path: string, claim: StateLockClaim, env?: StateLockEnv): StateLock;
|
|
1023
|
+
/** 아직 내 잠금인가 — 잠금 파일에 내 표가 적혀 있다. */
|
|
1024
|
+
isOwned(): boolean;
|
|
1025
|
+
/**
|
|
1026
|
+
* 내 잠금이 아니면 던진다 — 파일에 쓰기 전에 부른다. 멈춘 줄 알고 다른 곳이 넘겨받았는데 옛 사본을 쓰면 그쪽
|
|
1027
|
+
* 기록을 지운다.
|
|
1028
|
+
*/
|
|
1029
|
+
assertOwned(): void;
|
|
1030
|
+
/**
|
|
1031
|
+
* 잠금을 푼다 — 내 잠금일 때만 파일을 지운다. 여러 번 불러도 된다. 풀지 못해도 던지지 않는다 — 할 일은 이미
|
|
1032
|
+
* 끝났고, 남은 잠금은 이 프로세스가 끝나면 다음에 여는 곳이 넘겨받는다.
|
|
1033
|
+
*/
|
|
1034
|
+
release(): void;
|
|
1035
|
+
private beat;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* 상태 DB 를 다른 곳이 쓰는 중이다 — 기다렸다가 다시 하면 된다. 쓰는 곳이 없는데도 계속 막히면(PID 를 다른
|
|
1039
|
+
* 프로세스가 물려받음) 잠금 파일을 지우는 법까지 알린다.
|
|
1040
|
+
*/
|
|
1041
|
+
declare class StateDbLockedError extends StateDbUnavailableError {
|
|
1042
|
+
readonly holder: StateLockOwner | null;
|
|
1043
|
+
private constructor();
|
|
1044
|
+
/** 다른 곳이 잠금을 쥐고 있다. */
|
|
1045
|
+
static held(holder: StateLockOwner | null, signalAgeMs: number): StateDbLockedError;
|
|
1046
|
+
/** 쥐고 있던 잠금을 다른 곳이 넘겨받았다 — 멈춘 것으로 보였다. 옛 사본을 파일에 쓰지 않는다. */
|
|
1047
|
+
static lost(holder: StateLockOwner | null): StateDbLockedError;
|
|
1048
|
+
guidance(retry: string): string;
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
/** SQLite 가 WAL 모드에서 쓰는 옆 파일 — 이름은 SQLite 가 정한다. */
|
|
1052
|
+
declare function stateDbWalPath(dbPath: string): string;
|
|
1053
|
+
/**
|
|
1054
|
+
* 최근 기록이 아직 WAL 파일에만 있다 — CLI 가 쓰는 중이거나 끝까지 닫지 못했다. CLI 가 닫으면(끝났으면 명령 하나를
|
|
1055
|
+
* 다시 실행하면) 본 파일로 합쳐진다.
|
|
1056
|
+
*/
|
|
1057
|
+
declare class PendingWalError extends StateDbUnavailableError {
|
|
1058
|
+
readonly walBytes: number;
|
|
1059
|
+
constructor(walBytes: number);
|
|
1060
|
+
guidance(retry: string): string;
|
|
1061
|
+
}
|
|
1062
|
+
/** WAL 파일에 쌓인 바이트 — 파일이 없으면 0. */
|
|
1063
|
+
declare function pendingWalBytes(dbPath: string): number;
|
|
1064
|
+
/**
|
|
1065
|
+
* WAL 파일에 기록이 남았으면 던진다. 잠금을 잡은 뒤에 부른다 — 지금 CLI 는 잠금이 먼저 막으므로, 여기서 본 WAL 은
|
|
1066
|
+
* 끝나지 않고 죽은 CLI 나 잠금을 모르는 옛 CLI 의 것이다.
|
|
1067
|
+
*/
|
|
1068
|
+
declare function assertNoPendingWal(dbPath: string): void;
|
|
1069
|
+
|
|
650
1070
|
type NotionPropertyValue = Record<string, unknown>;
|
|
651
1071
|
interface WikilinkResolver$1 {
|
|
652
1072
|
resolve(title: string): string | null;
|
|
653
1073
|
resolvePageId(pageId: string): string | null;
|
|
654
1074
|
}
|
|
1075
|
+
interface PropertyMapperOptions {
|
|
1076
|
+
/**
|
|
1077
|
+
* 날짜시각을 적고 보낼 시간대(IANA 이름, 설정 `conversion.timeZone`). 없으면 이 컴퓨터의 시간대.
|
|
1078
|
+
*/
|
|
1079
|
+
readonly timeZone?: string;
|
|
1080
|
+
}
|
|
1081
|
+
/** {@link PropertyMapper.toNotionPropertyChanges} 가 기준으로 삼는 지금 값들. */
|
|
1082
|
+
interface PropertyChangeContext {
|
|
1083
|
+
/**
|
|
1084
|
+
* 지금 로컬 frontmatter — 기간의 시작과 끝 중 하나만 바뀌어도 둘 다 보내야 해서 읽는다. 없으면 바뀐
|
|
1085
|
+
* 값만 본다(새 행처럼 비교할 기준이 없을 때와 같다).
|
|
1086
|
+
*/
|
|
1087
|
+
readonly current?: Readonly<Record<string, unknown>>;
|
|
1088
|
+
/**
|
|
1089
|
+
* 원격 페이지의 지금 속성(`page.properties`) — 글 속성 · 제목은 이 서식 위에 바뀐 글자만 고친다
|
|
1090
|
+
* ({@link patchRichText}). 없으면 평문으로 보낸다.
|
|
1091
|
+
*/
|
|
1092
|
+
readonly remote?: Readonly<Record<string, unknown>>;
|
|
1093
|
+
}
|
|
655
1094
|
declare class PropertyMapper {
|
|
1095
|
+
private readonly options;
|
|
656
1096
|
private schema;
|
|
657
1097
|
private wikilinkResolver;
|
|
1098
|
+
constructor(options?: PropertyMapperOptions);
|
|
1099
|
+
/** 설정의 변환 옵션으로 만든다 — 시간대를 이 한 곳에서 읽는다. */
|
|
1100
|
+
static fromConfig(config: Pick<Config, "conversion">): PropertyMapper;
|
|
658
1101
|
setWikilinkResolver(resolver: WikilinkResolver$1): void;
|
|
659
1102
|
loadSchema(properties: Record<string, {
|
|
660
1103
|
id: string;
|
|
661
1104
|
type: string;
|
|
662
1105
|
}>): void;
|
|
1106
|
+
/**
|
|
1107
|
+
* frontmatter 전체를 Notion 속성 값으로 바꾼다 — 비교할 기준이 없는 새 행을 만들 때 쓴다.
|
|
1108
|
+
*
|
|
1109
|
+
* 스키마를 읽었으면 스키마에 없는 키는 보내지 않는다. pull 이 적는 `cover` · `icon` 이나
|
|
1110
|
+
* 사용자가 붙인 `aliases` 처럼 DB 속성이 아닌 키를 보내면 Notion 이 «그런 속성은 없다»
|
|
1111
|
+
* 며 요청 전체를 거부한다 — 행의 다른 속성까지 하나도 반영되지 않는다.
|
|
1112
|
+
*/
|
|
663
1113
|
toNotionProperties(frontmatter: Record<string, unknown>, title: string): Record<string, NotionPropertyValue>;
|
|
1114
|
+
/**
|
|
1115
|
+
* 바뀐 속성만 Notion 속성 값으로 바꾼다(S-01).
|
|
1116
|
+
*
|
|
1117
|
+
* 비운 속성은 그 타입의 빈 값으로 보낸다. 되돌릴 수 없는 것은 비우지 않는다 — `files`
|
|
1118
|
+
* 를 비우면 Notion 에 올라간 첨부가 지워지고 볼트에는 그 파일이 없다. `status` 는 빈 값이
|
|
1119
|
+
* 없다. 스키마에 없는 키와 읽기 전용 속성(수식 · 롤업 · 생성일 …)도 보내지 않는다.
|
|
1120
|
+
*
|
|
1121
|
+
* 날짜는 오프셋 없는 시각에 시간대의 오프셋을 붙여 보낸다 — Notion 은 오프셋 없는 시각을 UTC 로 읽는다.
|
|
1122
|
+
* 기간은 시작과 끝(짝 키) 중 하나만 바뀌어도 둘을 함께 보낸다 — 날짜 값은 통째로 바뀐다. 글 속성 ·
|
|
1123
|
+
* 제목은 원격 서식 위에 바뀐 글자만 고친다(`context.remote`).
|
|
1124
|
+
*
|
|
1125
|
+
* @param title 제목이 바뀌었을 때만 새 제목, 아니면 null.
|
|
1126
|
+
* @returns `skipped` — 바뀌었지만 보내지 않은 속성 이름. 호출측이 알린다.
|
|
1127
|
+
*/
|
|
1128
|
+
toNotionPropertyChanges(changes: RowPropertyChanges, title: string | null, context?: PropertyChangeContext): {
|
|
1129
|
+
properties: Record<string, NotionPropertyValue>;
|
|
1130
|
+
skipped: string[];
|
|
1131
|
+
};
|
|
1132
|
+
/**
|
|
1133
|
+
* 값을 보낼 수 있는 속성만, pull 이 적는 모양으로 — 원격 값과 로컬 값을 견줄 때 쓴다.
|
|
1134
|
+
*
|
|
1135
|
+
* 고르는 것은 스키마에 있고 읽기 전용이 아니고 제목이 아닌 키와 날짜 속성의 짝 키다. 수식 · 롤업 같은
|
|
1136
|
+
* 값은 달라도 보낼 수 없으니 견주지 않는다. 날짜는 예전 버전이 적은 모양(`…+09:00` · `{start, end}`)도
|
|
1137
|
+
* 시간대의 벽시계 시각 · 짝 키로 바꿔 같은 순간이면 같게 본다(F-08).
|
|
1138
|
+
*/
|
|
1139
|
+
writableValues(values: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
1140
|
+
/**
|
|
1141
|
+
* `written` 에서 예전 버전이 적은 날짜 모양이 남은 키(F-08) — 날짜 · 생성일 · 수정일 · 날짜 수식 · 날짜
|
|
1142
|
+
* 롤업 가운데, 지금 받으면 적을 값과 같은 순간인데 적힌 모양이 다른 것(`…Z` · `…+09:00` · `{start, end}`).
|
|
1143
|
+
* 기간은 짝 키(`<이름>_end`)도 함께 — 둘 중 하나라도 적혀 있거나 받으면 적을 키만. 원격이 그대로인 행은
|
|
1144
|
+
* 다시 받지 않으므로, 이런 행은 따로 골라 이 키들을 새 모양으로 고쳐 써야 한다.
|
|
1145
|
+
*
|
|
1146
|
+
* 값이 다른 키는 보지 않는다 — 수식 · 롤업은 행의 수정 시각을 바꾸지 않고 다시 계산되고, 시간대 설정을
|
|
1147
|
+
* 바꾸면 벽시계 시각이 달라진다. 모양 때문이 아니다.
|
|
1148
|
+
*
|
|
1149
|
+
* @param written 행 노트에 적힌 frontmatter.
|
|
1150
|
+
* @param notionProps 원격 행의 속성(`page.properties`).
|
|
1151
|
+
* @returns 고쳐 쓸 키. 없으면 빈 배열.
|
|
1152
|
+
*/
|
|
1153
|
+
outdatedDateKeys(written: Readonly<Record<string, unknown>>, notionProps: Record<string, unknown>): string[];
|
|
664
1154
|
fromNotionProperties(notionProps: Record<string, unknown>): Record<string, unknown>;
|
|
1155
|
+
/**
|
|
1156
|
+
* 이 행에서 기간의 끝을 적는 짝 키 — 날짜 속성 이름 → 짝 키. 값이 기간인 속성만이다
|
|
1157
|
+
* ({@link fromNotionProperties} 가 끝을 짝 키에 적는 것과 같다).
|
|
1158
|
+
*/
|
|
1159
|
+
rangeEndKeysOf(notionProps: Readonly<Record<string, unknown>>): Map<string, string>;
|
|
1160
|
+
/**
|
|
1161
|
+
* 이 행에서 Notion 이 정하는 frontmatter 키 — {@link fromNotionProperties} 가 적을 수 있는 키 전부다.
|
|
1162
|
+
* 값이 비어 적지 않은 속성과 지금은 기간이 아니어서 적지 않은 짝 키도 든다. pull 이 받은 속성을 로컬
|
|
1163
|
+
* frontmatter 와 합칠 때 이 밖의 키는 로컬 것으로 둔다.
|
|
1164
|
+
*/
|
|
1165
|
+
ownedKeys(notionProps: Readonly<Record<string, unknown>>): string[];
|
|
1166
|
+
private zone;
|
|
1167
|
+
/** `key` 가 날짜 속성의 짝 키면 그 날짜 속성 이름. */
|
|
1168
|
+
private rangeOwnerOf;
|
|
1169
|
+
/** 날짜 속성인데 짝 키를 쓰는가 — 같은 이름의 속성이 DB 에 따로 없을 때. */
|
|
1170
|
+
private hasRangeKey;
|
|
1171
|
+
/**
|
|
1172
|
+
* 짝 키를 쓰는 날짜 속성에 보낼 값 — 시작(`name`)과 끝(`name_end`)을 함께. 둘 다 비었으면 지운다.
|
|
1173
|
+
* 끝만 있으면 보낼 수 없다(Notion 의 날짜는 시작이 있어야 한다) — null.
|
|
1174
|
+
*/
|
|
1175
|
+
private rangeRequest;
|
|
665
1176
|
private convertBySchema;
|
|
666
1177
|
private inferAndConvert;
|
|
667
1178
|
private extractValue;
|
|
668
1179
|
}
|
|
669
1180
|
|
|
1181
|
+
/**
|
|
1182
|
+
* 취소 신호. `AbortSignal` 과 호환되며, 테스트에서 가벼운 목 객체로도 쓸 수 있다.
|
|
1183
|
+
*/
|
|
1184
|
+
interface AbortLike {
|
|
1185
|
+
readonly aborted: boolean;
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
/** 만들 블록 — 블록 API 요청 모양. 자식은 몸통의 `children` 에 깊이 제한 없이 담는다. */
|
|
1189
|
+
interface BlockRequest {
|
|
1190
|
+
readonly type: string;
|
|
1191
|
+
readonly [body: string]: unknown;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
/** {@link NotionClient.searchRecentPages} 의 한 줄. */
|
|
1195
|
+
interface RecentPage {
|
|
1196
|
+
readonly id: string;
|
|
1197
|
+
readonly last_edited_time: string;
|
|
1198
|
+
readonly last_edited_by: {
|
|
1199
|
+
readonly id: string;
|
|
1200
|
+
} | null;
|
|
1201
|
+
/**
|
|
1202
|
+
* DB 행이면 그 DB 의 id, 페이지면 null — 바뀐 행이 있는 DB 만 조회하는 데 쓴다(ADR-027). search 는 행의
|
|
1203
|
+
* 부모를 `data_source_id` 로 주면서 DB id 를 함께 싣는다(실측 2026-09-28).
|
|
1204
|
+
*/
|
|
1205
|
+
readonly parentDatabaseId: string | null;
|
|
1206
|
+
/**
|
|
1207
|
+
* 부모와 제목 — search 는 페이지 객체를 통째로 준다(실측 2026-10-21: 부모 · 제목 속성 포함). 새 페이지가 받을
|
|
1208
|
+
* 것인지 가르는 데 쓴다. 예전에는 추적하지 않는 페이지마다 다시 읽어 이 둘을 얻었다 — 워크스페이스에서 최근
|
|
1209
|
+
* 고친 페이지가 많을수록 pull 마다 요청이 그만큼 늘었다(B8).
|
|
1210
|
+
*/
|
|
1211
|
+
readonly parent: PageObjectResponse["parent"];
|
|
1212
|
+
readonly title: string;
|
|
1213
|
+
}
|
|
1214
|
+
/**
|
|
1215
|
+
* {@link NotionClient.searchRecentDataSources} 의 한 줄 — 속성(스키마) · 제목이 바뀐 data source. 행을 고쳐도
|
|
1216
|
+
* data source 의 수정 시각은 오르지 않는다(실측 2026-09-28) — 행의 변경은 {@link RecentPage} 로 본다.
|
|
1217
|
+
*/
|
|
1218
|
+
interface RecentDataSource {
|
|
1219
|
+
readonly id: string;
|
|
1220
|
+
readonly last_edited_time: string;
|
|
1221
|
+
/** 이 data source 가 속한 DB. 모르면 null. */
|
|
1222
|
+
readonly databaseId: string | null;
|
|
1223
|
+
}
|
|
1224
|
+
/** 블록 API 의 블록 하나 — 자식이 있으면 `children` 에 담겨 온다({@link NotionClient.getBlockTree}). */
|
|
1225
|
+
type BlockTreeNode = BlockObjectResponse & {
|
|
1226
|
+
children?: BlockTreeNode[];
|
|
1227
|
+
};
|
|
1228
|
+
/**
|
|
1229
|
+
* DB 하나를 한 번 받은 것 — 행 조회 · 스키마 · 제목이 같은 것을 다시 받지 않고 이것을 읽는다. 예전에는 DB 하나를
|
|
1230
|
+
* 받는 데 같은 DB 를 서너 번 다시 받아(요청 6~8회) 발견 DB 166개의 pull 이 분 단위였다.
|
|
1231
|
+
*/
|
|
1232
|
+
interface DatabaseMeta {
|
|
1233
|
+
readonly databaseId: string;
|
|
1234
|
+
/** DB 제목. 없으면 "". */
|
|
1235
|
+
readonly title: string;
|
|
1236
|
+
/** 행을 조회할 data source. 비었으면 옛 단일 소스 DB 다 — DB id 로 조회한다. */
|
|
1237
|
+
readonly dataSources: ReadonlyArray<{
|
|
1238
|
+
readonly id: string;
|
|
1239
|
+
readonly name: string;
|
|
1240
|
+
}>;
|
|
1241
|
+
/**
|
|
1242
|
+
* 속성 스키마 — 읽을 수 있는 모든 data source 의 속성을 합친 것(이름이 겹치면 앞의 것). 읽을 수 있는 data
|
|
1243
|
+
* source 가 없으면 DB 객체의 속성.
|
|
1244
|
+
*/
|
|
1245
|
+
readonly properties: Readonly<Record<string, unknown>>;
|
|
1246
|
+
}
|
|
670
1247
|
interface NotionClientOptions {
|
|
671
1248
|
readonly token: string;
|
|
672
1249
|
readonly concurrency?: number;
|
|
@@ -677,6 +1254,10 @@ interface NotionClientOptions {
|
|
|
677
1254
|
readonly retryBackoffFactor?: number;
|
|
678
1255
|
readonly pageSize?: number;
|
|
679
1256
|
readonly batchSize?: number;
|
|
1257
|
+
/** 잘린 Markdown 을 채우려고 다시 받는 블록 수 상한 (S-06) */
|
|
1258
|
+
readonly markdownCompletionMaxBlocks?: number;
|
|
1259
|
+
/** 페이지 속성의 날짜시각을 적을 시간대(`conversion.timeZone`) — 없으면 이 컴퓨터의 시간대. */
|
|
1260
|
+
readonly timeZone?: string;
|
|
680
1261
|
readonly fetch?: typeof globalThis.fetch;
|
|
681
1262
|
}
|
|
682
1263
|
declare class NotionClient {
|
|
@@ -689,10 +1270,30 @@ declare class NotionClient {
|
|
|
689
1270
|
private readonly retryBackoffFactor;
|
|
690
1271
|
private readonly defaultPageSize;
|
|
691
1272
|
private readonly batchSize;
|
|
1273
|
+
private readonly markdownCompletionMaxBlocks;
|
|
1274
|
+
/** {@link getBotUserId} 가 받은 값 — 받는 중이면 그 약속. */
|
|
1275
|
+
private botUserId;
|
|
1276
|
+
/** {@link getUserNames} 가 물은 사용자 이름 — 읽지 못한 사용자는 null. */
|
|
1277
|
+
private readonly userNames;
|
|
692
1278
|
constructor(options: NotionClientOptions);
|
|
693
1279
|
/** config.advanced 의 운영 튜닝값으로 클라이언트를 생성한다 (매직넘버 단일 진실원). */
|
|
694
1280
|
static fromConfig(config: Config, fetch?: typeof globalThis.fetch): NotionClient;
|
|
695
1281
|
getInternalClient(): Client;
|
|
1282
|
+
/**
|
|
1283
|
+
* 이 토큰의 봇 사용자 id(`GET /v1/users/me`). 이 통합이 고친 페이지는 `last_edited_by` 가 이
|
|
1284
|
+
* id 다 — 같은 분 안에서 우리 뒤에 누가 고쳤는지 가르는 데 쓴다(`sync/remote-observation.ts`).
|
|
1285
|
+
* 받은 값은 기억한다. 받지 못하면 던지고 기억하지 않는다 — 다음 호출이 다시 묻는다.
|
|
1286
|
+
*/
|
|
1287
|
+
getBotUserId(): Promise<string>;
|
|
1288
|
+
/**
|
|
1289
|
+
* 사용자 멘션에 보일 이름(`GET /v1/users/:id`). Markdown API 는 사용자 멘션을 이름 없는 태그로
|
|
1290
|
+
* 내보낸다(`converter/mention.ts`).
|
|
1291
|
+
*
|
|
1292
|
+
* 물은 사용자는 기억한다 — 같은 사람이 여러 노트에 나와도 한 번만 묻는다. 읽지 못한 사용자(권한 없는
|
|
1293
|
+
* 통합 · 나간 사용자)는 이유를 남기고 맵에서 뺀다. 그 멘션은 이름 대신 정해 둔 글로 보이지만, 멘션
|
|
1294
|
+
* 자체는 id 로 보존된다.
|
|
1295
|
+
*/
|
|
1296
|
+
getUserNames(ids: readonly string[]): Promise<Map<string, string>>;
|
|
696
1297
|
getPage(pageId: string): Promise<PageObjectResponse>;
|
|
697
1298
|
createPage(params: {
|
|
698
1299
|
parentId: string;
|
|
@@ -708,27 +1309,63 @@ declare class NotionClient {
|
|
|
708
1309
|
markdown: string;
|
|
709
1310
|
properties?: Record<string, unknown>;
|
|
710
1311
|
}): Promise<PageObjectResponse>;
|
|
1312
|
+
/**
|
|
1313
|
+
* {@link createPageWithMarkdown} 이 버린 맨 앞 `# H1` 을 되살린다 — 그 markdown 이 H1 으로
|
|
1314
|
+
* 시작할 때만 같은 markdown 으로 본문을 한 번 바꾼다({@link startsWithHeading1}).
|
|
1315
|
+
*
|
|
1316
|
+
* 만든 직후, 하위 페이지를 만들기 전에 부른다 — 교체가 지울 자식이 없다. 호출측은 먼저 페이지
|
|
1317
|
+
* 매핑을 적어 둔다: 여기서 실패해도 다음 push 가 새로 만들지 않고 본문을 다시 보낸다.
|
|
1318
|
+
*
|
|
1319
|
+
* @returns 본문을 바꿨으면 true
|
|
1320
|
+
*/
|
|
1321
|
+
restoreLeadingHeading(pageId: string, markdown: string): Promise<boolean>;
|
|
711
1322
|
updatePageProperties(pageId: string, properties: Record<string, unknown>): Promise<PageObjectResponse>;
|
|
1323
|
+
/**
|
|
1324
|
+
* 페이지 마크다운. 블록이 많아 잘린 부분은 블록 ID 로 다시 받아 채운다(S-06 —
|
|
1325
|
+
* {@link completeTruncatedMarkdown}). 돌려준 `unknown_block_ids` 는 권한이 닿지 않아 끝내
|
|
1326
|
+
* 읽지 못한 블록이고, 그 자리는 원래 블록을 지키는 태그로 남아 있다.
|
|
1327
|
+
*/
|
|
712
1328
|
getPageMarkdown(pageId: string): Promise<PageMarkdownResponse>;
|
|
1329
|
+
private retrieveMarkdown;
|
|
1330
|
+
/**
|
|
1331
|
+
* 본문을 통째로 바꾼다. 자식 페이지 · 자식 DB 는 **지우지 않는다**(S-03).
|
|
1332
|
+
*
|
|
1333
|
+
* `allow_deleting_content: true` 로 보내면 새 본문에 `<page>` · `<database>` 태그가 없는
|
|
1334
|
+
* 자식을 Notion 이 휴지통으로 보낸다. 삭제를 허용하지 않으면 무엇이 지워질지 나열하며
|
|
1335
|
+
* `validation_error` 로 거절하고 아무것도 바꾸지 않는다(2026-09-27 실측) — 호출측이
|
|
1336
|
+
* 자식 태그를 되돌려 놓고 다시 보낸다(`sync/page-body.ts`).
|
|
1337
|
+
*/
|
|
713
1338
|
replacePageMarkdown(pageId: string, markdown: string): Promise<PageMarkdownResponse>;
|
|
714
1339
|
updatePageMarkdownPartial(pageId: string, patches: Array<{
|
|
715
1340
|
oldStr: string;
|
|
716
1341
|
newStr: string;
|
|
717
1342
|
replaceAll?: boolean;
|
|
718
1343
|
}>): Promise<PageMarkdownResponse>;
|
|
719
|
-
movePage(pageId: string, newParentId: string, newParentType: "page" | "database"): Promise<PageObjectResponse>;
|
|
720
1344
|
/**
|
|
721
|
-
*
|
|
722
|
-
*
|
|
723
|
-
*
|
|
1345
|
+
* 페이지를 다른 페이지 아래로 옮긴다 — `POST /v1/pages/{id}/move`.
|
|
1346
|
+
*
|
|
1347
|
+
* 페이지 갱신(pages.update)은 부모를 바꾸지 않는다 — `parent` 를 보내도 오류 없이 무시한다.
|
|
1348
|
+
* 예전 movePage 가 그 길을 써서, 옮긴 노트의 Notion 부모가 한 번도 바뀌지 않았다(S-11).
|
|
1349
|
+
* 같은 부모로 다시 옮겨도 결과가 같아 모호한 실패는 다시 보낸다.
|
|
1350
|
+
*/
|
|
1351
|
+
movePage(pageId: string, parentPageId: string): Promise<void>;
|
|
1352
|
+
/**
|
|
1353
|
+
* 데이터베이스 1개를 신 모델(2025-09-03 data source 분리)로 조회한다 — DB 1회 + data source 마다 1회.
|
|
1354
|
+
* - title · data source 목록: database 객체에 그대로 존재한다.
|
|
1355
|
+
* - properties(스키마): 신 모델에선 data source 에 있으므로 data source 를 조회해 채운다.
|
|
724
1356
|
* data source 접근 불가(링크드 DB 등)면 database 객체의 properties 로 폴백한다(제목은 보존).
|
|
725
1357
|
*
|
|
1358
|
+
* 같은 DB 의 행 조회 · 스키마 · 제목은 받은 것({@link DatabaseMeta})을 넘겨 다시 받지 않는다.
|
|
1359
|
+
*
|
|
726
1360
|
* 폐기한 레거시 raw fetch(`GET /v1/databases/{id}`, Notion-Version 2022-06-28)는 신 모델로
|
|
727
1361
|
* 업그레이드된 다수 DB에 400 을 반환해 자동 발견 DB 가 통째 드롭(내용 손실)되던 원인이었다.
|
|
728
1362
|
* SDK(`databases.retrieve`/`dataSources.retrieve`)는 동일 토큰으로 정상 동작한다.
|
|
729
1363
|
*/
|
|
730
|
-
|
|
731
|
-
|
|
1364
|
+
getDatabaseMeta(databaseId: string): Promise<DatabaseMeta>;
|
|
1365
|
+
/** `from` 이 이 DB 를 받은 것이면 그것을, 아니면 새로 받는다. */
|
|
1366
|
+
private metaOf;
|
|
1367
|
+
/** @param from 이미 받은 이 DB — 주면 다시 받지 않는다. */
|
|
1368
|
+
getDatabaseTitle(databaseId: string, from?: DatabaseMeta): Promise<string>;
|
|
732
1369
|
/**
|
|
733
1370
|
* 발견된 DB 가 **동기화 가능한지**(접근 가능한 data source 가 있는지) 판별한다.
|
|
734
1371
|
* 신 모델(2025-09-03)에서 링크드 DB·미공유 데이터 소스·삭제 DB 는 `data_sources` 가
|
|
@@ -757,23 +1394,22 @@ declare class NotionClient {
|
|
|
757
1394
|
originalDbId: string;
|
|
758
1395
|
viewName: string;
|
|
759
1396
|
} | null>;
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
type: string;
|
|
763
|
-
}>>;
|
|
764
|
-
getDatabaseSchemaFull(databaseId: string): Promise<Record<string, {
|
|
1397
|
+
/** @param from 이미 받은 이 DB — 주면 다시 받지 않는다. */
|
|
1398
|
+
getDatabaseSchema(databaseId: string, from?: DatabaseMeta): Promise<Record<string, {
|
|
765
1399
|
id: string;
|
|
766
1400
|
type: string;
|
|
767
|
-
options?: Array<{
|
|
768
|
-
name: string;
|
|
769
|
-
color?: string;
|
|
770
|
-
}>;
|
|
771
|
-
groups?: Array<{
|
|
772
|
-
name: string;
|
|
773
|
-
color?: string;
|
|
774
|
-
optionIds?: string[];
|
|
775
|
-
}>;
|
|
776
1401
|
}>>;
|
|
1402
|
+
/**
|
|
1403
|
+
* DB 속성 스키마 전체(선택지·status 그룹 포함).
|
|
1404
|
+
*
|
|
1405
|
+
* 옵션 `id` 까지 그대로 실어 보낸다 — 산출물에는 안 나가지만, status 뷰 필터가
|
|
1406
|
+
* 옵션명이 아니라 **그룹명**(`To-do` 등)으로 오기 때문에 `groups[].optionIds` 를
|
|
1407
|
+
* 옵션명으로 되돌리려면 이 id 가 있어야 한다. 예전엔 여기서 id 를 떨궈,
|
|
1408
|
+
* 그룹으로 필터링된 뷰를 Bases 로 옮길 방법이 아예 없었다.
|
|
1409
|
+
*
|
|
1410
|
+
* @param from 이미 받은 이 DB — 주면 다시 받지 않는다.
|
|
1411
|
+
*/
|
|
1412
|
+
getDatabaseSchemaFull(databaseId: string, from?: DatabaseMeta): Promise<Record<string, BasePropertySchema>>;
|
|
777
1413
|
/** 특정 data source 1개를 직접 쿼리한다(저수준). 다중 data source 순회의 빌딩블록. */
|
|
778
1414
|
private queryDataSource;
|
|
779
1415
|
/**
|
|
@@ -794,8 +1430,10 @@ declare class NotionClient {
|
|
|
794
1430
|
* 페이지네이션하면 2번째+ 의 행이 통째 침묵 유실된다. 전 data source 를 순회·페이지네이션
|
|
795
1431
|
* 하고 page_id 로 디듀프(소스 간 동일 페이지 방어)한 뒤 합친다. 다중 소스면 1회 경고해
|
|
796
1432
|
* "소스별 탭 구분이 한 폴더로 병합"되는 점을 비침묵으로 알린다.
|
|
1433
|
+
*
|
|
1434
|
+
* @param from 이미 받은 이 DB — 주면 data source 목록을 다시 받지 않는다.
|
|
797
1435
|
*/
|
|
798
|
-
queryAllDatabasePages(databaseId: string, filter?: unknown): Promise<PageObjectResponse[]>;
|
|
1436
|
+
queryAllDatabasePages(databaseId: string, filter?: unknown, from?: DatabaseMeta): Promise<PageObjectResponse[]>;
|
|
799
1437
|
/** database 의 1차 data source id(없으면 databaseId 폴백). 단일 소스 경로 호환용. */
|
|
800
1438
|
getDataSourceId(databaseId: string): Promise<string>;
|
|
801
1439
|
/**
|
|
@@ -807,7 +1445,19 @@ declare class NotionClient {
|
|
|
807
1445
|
name: string;
|
|
808
1446
|
}>>;
|
|
809
1447
|
archivePage(pageId: string): Promise<void>;
|
|
1448
|
+
/**
|
|
1449
|
+
* 파일 블록의 지금 받을 수 있는 주소. 블록이 없거나 볼 수 없으면 null 이다.
|
|
1450
|
+
* 그 밖의 실패(일시 오류 등)는 던진다 — 예전에는 모두 null 로 삼켜 이유가 남지 않았다.
|
|
1451
|
+
*/
|
|
810
1452
|
getFileBlockUrl(blockId: string): Promise<string | null>;
|
|
1453
|
+
/**
|
|
1454
|
+
* 북마크 블록의 주소와 캡션(F-09). Markdown API 는 북마크를 주소 없는 자리 태그로만 보낸다
|
|
1455
|
+
* (`converter/bookmark.ts`) — 블록마다 한 번 읽는다.
|
|
1456
|
+
*
|
|
1457
|
+
* 읽지 못한 블록은 이유를 남기고 맵에서 뺀다. 그 북마크는 Notion 의 블록을 가리키는 링크로 보이고,
|
|
1458
|
+
* 블록 자체는 마커로 보존된다. 북마크가 아닌 블록도 뺀다.
|
|
1459
|
+
*/
|
|
1460
|
+
getBookmarks(blockIds: readonly string[]): Promise<Map<string, BookmarkTarget>>;
|
|
811
1461
|
getBlock(blockId: string): Promise<BlockObjectResponse>;
|
|
812
1462
|
listChildren(blockId: string, options?: {
|
|
813
1463
|
startCursor?: string;
|
|
@@ -819,8 +1469,51 @@ declare class NotionClient {
|
|
|
819
1469
|
fetchAllChildren(blockId: string): Promise<BlockObjectResponse[]>;
|
|
820
1470
|
private static readonly CONTAINER_BLOCK_TYPES;
|
|
821
1471
|
fetchAllChildrenDeep(blockId: string): Promise<BlockObjectResponse[]>;
|
|
822
|
-
|
|
1472
|
+
/**
|
|
1473
|
+
* 블록 트리 — 자식이 있는 블록은 `children` 에 자식을 담는다. 자식 페이지 · DB 는 열지 않는다. 동기화
|
|
1474
|
+
* 참조 블록은 Notion 이 원본의 자식을 돌려주므로 그대로 담는다(Markdown API 도 원본 내용을 싣는다).
|
|
1475
|
+
*
|
|
1476
|
+
* 블록 하나라도 읽지 못하면 던진다 — 빠진 채로 노트를 쓰면 다음 push 가 그 블록을 Notion 에서 지운다.
|
|
1477
|
+
*/
|
|
1478
|
+
getBlockTree(blockId: string): Promise<BlockTreeNode[]>;
|
|
1479
|
+
/**
|
|
1480
|
+
* 자식 블록을 덧붙인다. `options.after` 를 주면 그 블록 **바로 뒤**에 삽입한다
|
|
1481
|
+
* (기본은 맨 끝). 자리표시자를 실제 미디어 블록으로 제자리 교체할 때 필요하다.
|
|
1482
|
+
*
|
|
1483
|
+
* 실측(2025-09-03): position 삽입 응답의 `results` 는 방금 넣은 블록뿐 아니라
|
|
1484
|
+
* 삽입 지점 이후의 형제까지 함께 돌려준다. 앞에서부터 batch 길이만큼이 새 블록이므로
|
|
1485
|
+
* 배치를 이어 붙일 때는 그 마지막 id 를 다음 기준점으로 삼는다.
|
|
1486
|
+
*/
|
|
1487
|
+
appendChildren(blockId: string, children: unknown[], options?: {
|
|
1488
|
+
readonly after?: string;
|
|
1489
|
+
}): Promise<string[]>;
|
|
1490
|
+
/**
|
|
1491
|
+
* {@link appendChildren} 과 같되 새로 만든 블록을 통째로 돌려준다. 파일을 붙인 미디어 블록은
|
|
1492
|
+
* 응답에 Notion 이 저장한 파일의 URL 이 실려 온다 — 다시 조회하지 않고 그 파일을 알아 둔다(S-05).
|
|
1493
|
+
*/
|
|
1494
|
+
appendChildBlocks(blockId: string, children: unknown[], options?: {
|
|
1495
|
+
readonly after?: string;
|
|
1496
|
+
}): Promise<(BlockObjectResponse | PartialBlockObjectResponse)[]>;
|
|
1497
|
+
/**
|
|
1498
|
+
* 깊이 제한 없는 블록 트리를 맨 끝에 덧붙인다 — 요청 한도(깊이 두 단계 · 100 개 · 1000 블록)를 넘는
|
|
1499
|
+
* 자식은 부모를 만든 뒤 그 아래에 마저 붙인다({@link shapeAppendRequests}). 더 깊은 블록의 id 는
|
|
1500
|
+
* 응답에 없어 부모의 자식 목록을 읽어 찾는다.
|
|
1501
|
+
*
|
|
1502
|
+
* 중간에 실패하면 그때까지 만든 블록은 남는다 — 호출측이 실패로 알린다.
|
|
1503
|
+
*/
|
|
1504
|
+
appendBlockTree(parentId: string, blocks: readonly BlockRequest[]): Promise<void>;
|
|
823
1505
|
deleteBlock(blockId: string): Promise<void>;
|
|
1506
|
+
/**
|
|
1507
|
+
* 코드 블록의 글을 바꾼다 — Markdown API 로 보낼 수 없는 코드(코드 속 ``` 줄, S-22)를 블록으로
|
|
1508
|
+
* 채운다. 긴 코드는 rich text 여럿에 나눠 담는다. 같은 글로 다시 보내도 결과가 같아 모호한 실패는
|
|
1509
|
+
* 다시 보낸다.
|
|
1510
|
+
*/
|
|
1511
|
+
updateCodeBlockText(blockId: string, code: string, language: LanguageRequest): Promise<void>;
|
|
1512
|
+
/**
|
|
1513
|
+
* 페이지 코드 블록의 글 — 컨테이너 안까지. 받은 markdown 만으로 코드 범위를 가를 수 없을 때(코드 속
|
|
1514
|
+
* ``` 줄, S-22) 블록의 글로 맞춘다.
|
|
1515
|
+
*/
|
|
1516
|
+
getCodeBlockTexts(pageId: string): Promise<string[]>;
|
|
824
1517
|
/** single_part 업로드 상한 (Notion API 규격 20MB) */
|
|
825
1518
|
private static readonly SINGLE_PART_MAX_BYTES;
|
|
826
1519
|
/** multi_part 파트 크기 — 규격상 마지막 파트 제외 5~20MB, 10MB 고정 사용 */
|
|
@@ -840,13 +1533,17 @@ declare class NotionClient {
|
|
|
840
1533
|
results: PageObjectResponse[];
|
|
841
1534
|
nextCursor: string | null;
|
|
842
1535
|
}>;
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
1536
|
+
/**
|
|
1537
|
+
* `since` 뒤에 고친 페이지 — 최근 것부터. DB 행도 온다. 누가 마지막으로 고쳤는지도 싣는다: 수정
|
|
1538
|
+
* 시각은 분 단위라 같은 분 안의 편집은 편집자로만 가를 수 있다(N-05).
|
|
1539
|
+
*/
|
|
1540
|
+
searchRecentPages(since: string, signal?: AbortLike): Promise<RecentPage[]>;
|
|
1541
|
+
/**
|
|
1542
|
+
* `since` 뒤에 속성(스키마) · 제목이 바뀐 data source — 최근 것부터. 속성을 더하거나 이름을 바꾸면 행은
|
|
1543
|
+
* 그대로라도 그 DB 의 `.base` 와 행의 frontmatter 가 바뀐다.
|
|
1544
|
+
*/
|
|
1545
|
+
searchRecentDataSources(since: string, signal?: AbortLike): Promise<RecentDataSource[]>;
|
|
847
1546
|
getChildPages(parentId: string): Promise<PageObjectResponse[]>;
|
|
848
|
-
/** 페이지가 휴지통(in_trash)이거나 보관(archived) 상태인지 판정한다. */
|
|
849
|
-
private static isTrashedOrArchived;
|
|
850
1547
|
getChildDatabaseIds(parentId: string): Promise<string[]>;
|
|
851
1548
|
/**
|
|
852
1549
|
* root 서브트리를 직접 BFS 순회해 하위 페이지를 모두 수집한다(중첩 깊이 무관·무손실).
|
|
@@ -854,13 +1551,17 @@ declare class NotionClient {
|
|
|
854
1551
|
*
|
|
855
1552
|
* @param opts.deadlineMs `Date.now()` 기준 마감 시각. 각 페이지 처리 전 초과를 검사해
|
|
856
1553
|
* 초과 시 {@link DiscoveryTooLargeError}를 던진다(대규모 서브트리 → search 폴백 유도).
|
|
1554
|
+
* **그때까지 찾은 페이지는 에러의 `partial` 에 실어 보낸다** — 버리면 호출측이 두
|
|
1555
|
+
* 경로를 경합시키게 되고, 어느 쪽이 이기는지를 벽시계가 정하게 된다(R12-A).
|
|
857
1556
|
* 미지정 시 무제한(기존 동작 유지 — 테스트 mock 경로는 영향 없음).
|
|
1557
|
+
* @param opts.signal 취소되면 다음 페이지로 가기 전에 `OperationAbortedError` 로 멈춘다.
|
|
858
1558
|
*/
|
|
859
1559
|
getChildPagesRecursive(parentId: string, opts?: {
|
|
860
1560
|
deadlineMs?: number;
|
|
1561
|
+
signal?: AbortLike;
|
|
861
1562
|
}): Promise<PageObjectResponse[]>;
|
|
862
1563
|
/** 워크스페이스에서 통합에 공유된 모든 페이지를 bulk search 로 열거한다(100/요청). */
|
|
863
|
-
searchAllPages(): Promise<PageObjectResponse[]>;
|
|
1564
|
+
searchAllPages(signal?: AbortLike): Promise<PageObjectResponse[]>;
|
|
864
1565
|
/**
|
|
865
1566
|
* root 하위 페이지를 **search 기반**으로 디스커버리한다(대규모 서브트리용 폴백).
|
|
866
1567
|
*
|
|
@@ -875,7 +1576,7 @@ declare class NotionClient {
|
|
|
875
1576
|
* - 휴지통/보관(in_trash/archived) 페이지 제외
|
|
876
1577
|
* - root 자신은 제외(직접 순회도 root 의 *자식*부터 수집)
|
|
877
1578
|
*/
|
|
878
|
-
getPagesUnderRootViaSearch(rootId: string): Promise<PageObjectResponse[]>;
|
|
1579
|
+
getPagesUnderRootViaSearch(rootId: string, signal?: AbortLike): Promise<PageObjectResponse[]>;
|
|
879
1580
|
listDatabaseViews(databaseId: string): Promise<ViewConfig[]>;
|
|
880
1581
|
getView(viewId: string): Promise<ViewConfig | null>;
|
|
881
1582
|
getDatabaseViewsConfig(databaseId: string): Promise<DatabaseViewsConfig>;
|
|
@@ -892,8 +1593,35 @@ declare class NotionClient {
|
|
|
892
1593
|
setWikilinkResolver(resolver: WikilinkResolver$1): void;
|
|
893
1594
|
extractProperties(page: PageObjectResponse): Record<string, unknown>;
|
|
894
1595
|
private lastRequestTime;
|
|
1596
|
+
/**
|
|
1597
|
+
* 429 이후 **클라이언트 전체**가 쉬어야 하는 시각(epoch ms).
|
|
1598
|
+
*
|
|
1599
|
+
* rate limit 은 요청 하나의 문제가 아니라 워크스페이스 전체에 걸린 신호다. 그러니
|
|
1600
|
+
* 429 를 맞은 요청만 기다리게 두면 나머지 슬롯이 곧바로 다시 두드려 429 를 재생산한다.
|
|
1601
|
+
* 슬롯을 붙잡아 남을 막는 대신, 다 같이 보는 이 게이트로 쉬게 한다.
|
|
1602
|
+
*/
|
|
1603
|
+
private cooldownUntil;
|
|
1604
|
+
/**
|
|
1605
|
+
* 요청 1건을 rate limit 슬롯 안에서 실행하고, 재시도 가능한 실패는 백오프 후 다시 시도한다.
|
|
1606
|
+
*
|
|
1607
|
+
* 백오프 대기는 **슬롯을 놓은 뒤** 한다. 예전 구현은 `sema.acquire()` 안에서 재시도
|
|
1608
|
+
* 전체(최대 60초 x 5회)를 돌려, 불운한 요청 하나가 동시성 한 칸을 최대 5분 점유했다.
|
|
1609
|
+
* 동시성 기본값이 3이므로 그런 요청 3건이면 클라이언트 전체가 멈춘다. 게다가 재시도는
|
|
1610
|
+
* 로그를 한 줄도 남기지 않아, 겉보기에는 죽은 프로세스와 구분되지 않았다 — 실제로
|
|
1611
|
+
* 268페이지 pull 이 진행 로그가 멈춘 채 수 분씩 정지하는 것으로 나타났다(R9).
|
|
1612
|
+
* 그래서 (1) 대기 중에는 슬롯을 반납하고, (2) 429 는 {@link cooldownUntil} 전역
|
|
1613
|
+
* 게이트로 함께 쉬고, (3) 모든 재시도를 경고로 남긴다.
|
|
1614
|
+
*
|
|
1615
|
+
* `idempotent: false` 는 두 번 적용되면 결과가 달라지는 쓰기다(페이지 생성 · 블록
|
|
1616
|
+
* 덧붙이기 · 부분 치환). 이런 요청은 서버가 «처리하지 않았다» 고 분명히 말한 실패만
|
|
1617
|
+
* 다시 보낸다. 타임아웃 · 연결 끊김 · 5xx 는 요청이 이미 적용됐을 수 있고 Notion 은
|
|
1618
|
+
* 멱등 키를 받지 않으므로, 다시 보내면 같은 페이지 · 같은 블록이 두 벌 생긴다.
|
|
1619
|
+
* 그런 실패는 호출측으로 올린다 — 페이지 생성은 WAL(pending_operations)이 부모에서
|
|
1620
|
+
* 제목으로 찾아 입양한다.
|
|
1621
|
+
*/
|
|
895
1622
|
private withRateLimit;
|
|
896
|
-
|
|
1623
|
+
/** 슬롯을 쥔 뒤의 발사 대기 — 전역 쿨다운(429)과 최소 요청 간격을 함께 지킨다. */
|
|
1624
|
+
private waitForTurn;
|
|
897
1625
|
}
|
|
898
1626
|
|
|
899
1627
|
interface RichTextSegment {
|
|
@@ -1000,17 +1728,188 @@ declare class NotionBlockBuilder {
|
|
|
1000
1728
|
static table(rows: string[][], hasColumnHeader?: boolean, hasRowHeader?: boolean): NotionBlock;
|
|
1001
1729
|
}
|
|
1002
1730
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1731
|
+
/**
|
|
1732
|
+
* DB 완결성 게이트 — "원격에 있는 행이 볼트에도 전부 있는가"를 대조한다.
|
|
1733
|
+
*
|
|
1734
|
+
* ## 왜 필요한가 (R11)
|
|
1735
|
+
*
|
|
1736
|
+
* 기존 게이트는 전부 **멱등성**(같은 입력에 같은 출력)을 본다 — 해시 불일치 0, repull
|
|
1737
|
+
* 바이트 동일, churn 0. 그런데 멱등성은 *체계적 미발견*을 구조적으로 잡지 못한다:
|
|
1738
|
+
* 디스커버리가 매번 **같은 행을 똑같이 놓치면** 두 번째 pull 도 첫 번째와 동일한
|
|
1739
|
+
* 결과를 내므로 churn 은 0 이고 해시도 전부 일치한다. 실제로 2026-07-17 pull 은 DB 행
|
|
1740
|
+
* 296개를 침묵 유실한 채 `audit-vault 결함 0 · 해시 불일치 0 · byte-identical repull`
|
|
1741
|
+
* 을 전부 통과했다. "안 바뀐다"와 "빠짐없다"는 다른 성질이고, 후자를 보는 게이트가
|
|
1742
|
+
* 없었던 것이다.
|
|
1743
|
+
*
|
|
1744
|
+
* 이 모듈은 후자만 본다. 원격 행 id 집합과 볼트가 추적 중인 db-row id 집합을 직접
|
|
1745
|
+
* 대조해, 어느 쪽에만 있는지를 센다. 카운트가 아니라 **집합**을 비교하는 이유는
|
|
1746
|
+
* 카운트만 보면 "1건 미발견 + 1건 잔재"가 상쇄돼 통과하기 때문이다.
|
|
1747
|
+
*
|
|
1748
|
+
* ## 범위와 한계
|
|
1749
|
+
*
|
|
1750
|
+
* 볼트가 **아는** database 에 대해 "그 DB 의 모든 행이 있는가"를 보장한다. database
|
|
1751
|
+
* 자체를 통째로 못 찾은 경우는 볼트에 흔적이 없어 여기서 알 수 없으므로, 호출부가
|
|
1752
|
+
* 설정/디스커버리가 아는 database id 를 {@link CompletenessOptions.databaseIds} 로
|
|
1753
|
+
* 넘겨 그 구멍까지 덮는다(행 0개로 대조되어 미발견이 그대로 드러난다).
|
|
1754
|
+
*
|
|
1755
|
+
* 원격 열거는 {@link CompletenessRemoteSource.queryAllDatabasePages} — 전 data source
|
|
1756
|
+
* 를 순회하는 SSOT — 하나만 쓴다. 여기서 별도 쿼리를 짜면 검증기가 피검증 대상과 다른
|
|
1757
|
+
* 열거 규칙을 갖게 돼, 게이트가 통과해도 실제 pull 은 놓치는 상황이 생긴다.
|
|
1758
|
+
*/
|
|
1759
|
+
/** 원격 행 열거 — {@link NotionClient} 가 그대로 만족한다(전 data source 순회 SSOT). */
|
|
1760
|
+
interface CompletenessRemoteSource {
|
|
1761
|
+
queryAllDatabasePages(databaseId: string, filter?: unknown): Promise<Array<{
|
|
1762
|
+
id: string;
|
|
1763
|
+
}>>;
|
|
1764
|
+
}
|
|
1765
|
+
/** 볼트가 추적 중인 레코드 열거 — {@link IStateDB} 가 그대로 만족한다. */
|
|
1766
|
+
interface CompletenessLocalSource {
|
|
1767
|
+
getAll(): ReadonlyArray<{
|
|
1768
|
+
readonly notionPageId: string | null;
|
|
1769
|
+
readonly notionParentId: string | null;
|
|
1770
|
+
readonly fileType: string;
|
|
1771
|
+
}>;
|
|
1772
|
+
}
|
|
1773
|
+
/** database 1개의 대조 결과. */
|
|
1774
|
+
interface DatabaseCompleteness {
|
|
1775
|
+
/** 대조 대상 database id(입력 표기 그대로). */
|
|
1776
|
+
readonly databaseId: string;
|
|
1777
|
+
/** 원격 행 수(전 data source 합·page id 디듀프 후). */
|
|
1778
|
+
readonly remoteRows: number;
|
|
1779
|
+
/** 볼트가 이 DB 소속으로 추적 중인 db-row 수. */
|
|
1780
|
+
readonly localRows: number;
|
|
1781
|
+
/** 원격에만 있는 행 id — **침묵 미발견**. 비어야 정상. */
|
|
1782
|
+
readonly missingIds: readonly string[];
|
|
1783
|
+
/** 볼트에만 있는 행 id — 원격 삭제 잔재/부모 오배치. 비어야 정상. */
|
|
1784
|
+
readonly extraIds: readonly string[];
|
|
1785
|
+
}
|
|
1786
|
+
/** 원격 열거가 실패한 database — 결과를 "이상 없음"으로 접지 않기 위해 따로 싣는다. */
|
|
1787
|
+
interface CompletenessFailure {
|
|
1788
|
+
readonly databaseId: string;
|
|
1789
|
+
readonly error: string;
|
|
1790
|
+
}
|
|
1791
|
+
/** {@link verifyDatabaseCompleteness} 결과. */
|
|
1792
|
+
interface CompletenessReport {
|
|
1793
|
+
/** database 별 대조 결과(입력 순서). */
|
|
1794
|
+
readonly databases: readonly DatabaseCompleteness[];
|
|
1795
|
+
/** 원격 열거 실패 목록. 하나라도 있으면 `complete` 는 false. */
|
|
1796
|
+
readonly failures: readonly CompletenessFailure[];
|
|
1797
|
+
/** 대조에 성공한 database 들의 원격 행 총합. */
|
|
1798
|
+
readonly remoteTotal: number;
|
|
1799
|
+
/** 같은 database 들의 볼트 db-row 총합. */
|
|
1800
|
+
readonly localTotal: number;
|
|
1801
|
+
/** 미발견·잔재·조회 실패가 모두 0. */
|
|
1802
|
+
readonly complete: boolean;
|
|
1803
|
+
}
|
|
1804
|
+
interface CompletenessOptions {
|
|
1805
|
+
/**
|
|
1806
|
+
* 볼트에 행이 하나도 없어도 반드시 대조할 database id(설정 `notion.databases[]` ·
|
|
1807
|
+
* 디스커버리 캐시 등). 통째로 미발견된 DB 를 드러내는 유일한 경로다.
|
|
1808
|
+
*/
|
|
1809
|
+
readonly databaseIds?: readonly string[];
|
|
1810
|
+
}
|
|
1811
|
+
/**
|
|
1812
|
+
* 원격 행 집합과 볼트 db-row 집합을 database 별로 대조한다.
|
|
1813
|
+
*
|
|
1814
|
+
* 대조 대상 = 볼트가 db-row 를 추적 중인 모든 database ∪
|
|
1815
|
+
* {@link CompletenessOptions.databaseIds}. 한 database 의 열거가 실패해도 나머지는
|
|
1816
|
+
* 계속 대조하고, 실패는 {@link CompletenessReport.failures} 로 비침묵 보고한다.
|
|
1817
|
+
*/
|
|
1818
|
+
declare function verifyDatabaseCompleteness(remote: CompletenessRemoteSource, local: CompletenessLocalSource, options?: CompletenessOptions): Promise<CompletenessReport>;
|
|
1819
|
+
/**
|
|
1820
|
+
* 원격 페이지 열거 — {@link NotionClient} 가 그대로 만족한다.
|
|
1821
|
+
*
|
|
1822
|
+
* DB 쪽과 달리 여기서는 **일부러 pull 과 다른 열거를 쓴다**. DB 행은 열거 방법이 하나뿐이라
|
|
1823
|
+
* 검증기가 다른 규칙을 쓰면 게이트가 통과해도 pull 은 놓치는 상황이 생기지만, 페이지는
|
|
1824
|
+
* 열거 경로가 둘(직접 순회 · search)이고 **둘이 같은 집합을 내지 않는다**는 게 애초의
|
|
1825
|
+
* 결함이다(R12-A). pull 이 어느 경로로 돌았든 독립된 두 번째 열거와 대조해야 의미가 있다.
|
|
1826
|
+
*/
|
|
1827
|
+
interface PageCompletenessRemoteSource {
|
|
1828
|
+
getPagesUnderRootViaSearch(rootId: string): Promise<Array<{
|
|
1829
|
+
id: string;
|
|
1830
|
+
}>>;
|
|
1831
|
+
}
|
|
1832
|
+
/** {@link verifyPageCompleteness} 결과. */
|
|
1833
|
+
interface PageCompletenessReport {
|
|
1834
|
+
/** 원격(search 열거) root 하위 페이지 수. */
|
|
1835
|
+
readonly remotePages: number;
|
|
1836
|
+
/** 볼트가 추적 중인 페이지 수(db-row 제외). */
|
|
1837
|
+
readonly vaultPages: number;
|
|
1838
|
+
/** 원격에만 있는 페이지 id — **침묵 미발견**. 비어야 정상. */
|
|
1839
|
+
readonly missingIds: readonly string[];
|
|
1840
|
+
/**
|
|
1841
|
+
* 볼트에만 있는 페이지 id — **정보성**(판정에 쓰지 않는다).
|
|
1842
|
+
*
|
|
1843
|
+
* DB 행과 달리 페이지는 볼트에만 있는 게 정상일 수 있다: 아직 push 하지 않은 로컬 노트,
|
|
1844
|
+
* root 페이지 자신(search 열거는 root 를 제외한다), search 색인이 아직 못 따라온 신규
|
|
1845
|
+
* 페이지. 이걸 실패로 접으면 게이트가 거짓 적색을 내며 신뢰를 잃는다.
|
|
1846
|
+
*/
|
|
1847
|
+
readonly localOnlyIds: readonly string[];
|
|
1848
|
+
/** 열거 실패 사유. 있으면 `complete` 는 false(실패를 통과로 접지 않는다). */
|
|
1849
|
+
readonly error?: string;
|
|
1850
|
+
/** 미발견 0 이고 열거도 성공. */
|
|
1851
|
+
readonly complete: boolean;
|
|
1852
|
+
}
|
|
1853
|
+
/** {@link SyncOrchestrator.verifyCompleteness} 결과 — DB 와 페이지 대조를 한 판정으로 묶는다. */
|
|
1854
|
+
interface VaultCompletenessReport {
|
|
1855
|
+
readonly databases: CompletenessReport;
|
|
1856
|
+
/** 페이지 모드에서만 채워진다. DB 모드에는 root 서브트리가 없어 `null`. */
|
|
1857
|
+
readonly pages: PageCompletenessReport | null;
|
|
1858
|
+
/** 두 대조가 모두 통과. */
|
|
1859
|
+
readonly complete: boolean;
|
|
1860
|
+
}
|
|
1861
|
+
/**
|
|
1862
|
+
* 원격 root 하위 페이지 집합과 볼트가 추적 중인 페이지 집합을 대조한다.
|
|
1863
|
+
*
|
|
1864
|
+
* 볼트 쪽은 `fileType !== "db-row"` 로 고른다 — 페이지형 타입을 열거하면
|
|
1865
|
+
* (`file`·`folder-note`·`folder-only`) 새 타입이 생겼을 때 한쪽만 낡아 조용히 게이트를
|
|
1866
|
+
* 빠져나간다. "행이 아닌 것은 전부 페이지"가 이 대조가 실제로 뜻하는 바다.
|
|
1867
|
+
*/
|
|
1868
|
+
declare function verifyPageCompleteness(remote: PageCompletenessRemoteSource, local: CompletenessLocalSource, rootPageId: string): Promise<PageCompletenessReport>;
|
|
1869
|
+
|
|
1870
|
+
/** 이름 변경 힌트 — 지금 경로 → 마지막 동기화 때 알던 경로. */
|
|
1871
|
+
interface RenameHints {
|
|
1872
|
+
readonly files: Readonly<Record<string, string>>;
|
|
1873
|
+
readonly folders: Readonly<Record<string, string>>;
|
|
1874
|
+
}
|
|
1875
|
+
type RenameKind = "file" | "folder";
|
|
1876
|
+
|
|
1877
|
+
interface FileInfo {
|
|
1878
|
+
readonly path: string;
|
|
1879
|
+
readonly content: string;
|
|
1880
|
+
readonly mtime: string;
|
|
1881
|
+
}
|
|
1882
|
+
/** 변경 감지 옵션 — 옮긴 노트를 찾는 데 쓴다(S-11). */
|
|
1883
|
+
interface LocalScanOptions {
|
|
1884
|
+
/** 플러그인이 적어 둔 이름 변경 힌트. */
|
|
1885
|
+
readonly hints?: RenameHints;
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1888
|
+
* 옮긴 것으로 짝지은 추적 노트. 상태 DB 는 아직 옛 경로(`record.obsidianPath`)를 적고 있다.
|
|
1889
|
+
*/
|
|
1890
|
+
interface LocalMoveAdoption {
|
|
1891
|
+
readonly record: SyncRecord;
|
|
1892
|
+
/** 파일의 지금 경로. */
|
|
1893
|
+
readonly to: string;
|
|
1894
|
+
/** 마지막으로 Notion 에 반영한 경로 — 앞서 입양해 둔 이동이면 그 WAL 의 옛 경로. */
|
|
1895
|
+
readonly origin: string;
|
|
1896
|
+
}
|
|
1897
|
+
interface LocalScan {
|
|
1898
|
+
/** 입양을 마친 것으로 보고 가른 변경 — 옮긴 노트는 새 경로의 `moved` 다. */
|
|
1899
|
+
readonly changes: LocalChange[];
|
|
1900
|
+
readonly adoptions: readonly LocalMoveAdoption[];
|
|
1901
|
+
}
|
|
1008
1902
|
declare class ChangeDetector {
|
|
1009
1903
|
private readonly stateDb;
|
|
1010
1904
|
constructor(stateDb: IStateDB);
|
|
1011
|
-
detectLocalChangesFast(stats: FileStatInfo[], readFile: (path: string) => Promise<string
|
|
1012
|
-
detectLocalChanges(currentFiles: FileInfo[]): LocalChange[];
|
|
1013
|
-
|
|
1905
|
+
detectLocalChangesFast(stats: FileStatInfo[], readFile: (path: string) => Promise<string>, options?: LocalScanOptions): Promise<LocalChange[]>;
|
|
1906
|
+
detectLocalChanges(currentFiles: FileInfo[], options?: LocalScanOptions): LocalChange[];
|
|
1907
|
+
/** 파일 크기 · 수정 시각이 지난 동기화와 같으면 내용을 읽지 않는다. */
|
|
1908
|
+
scanLocalChangesFast(stats: readonly FileStatInfo[], readFile: (path: string) => Promise<string>, options?: LocalScanOptions): Promise<LocalScan>;
|
|
1909
|
+
scanLocalChanges(currentFiles: readonly FileInfo[], options?: LocalScanOptions): LocalScan;
|
|
1910
|
+
/** 이름을 바꿔도 파일 크기 · 수정 시각은 그대로다 — 옮긴 노트는 그것으로 건너뛰지 않는다. */
|
|
1911
|
+
private movePending;
|
|
1912
|
+
private assemble;
|
|
1014
1913
|
}
|
|
1015
1914
|
|
|
1016
1915
|
interface NonMdFileInfo {
|
|
@@ -1039,12 +1938,34 @@ interface VaultFS {
|
|
|
1039
1938
|
}
|
|
1040
1939
|
|
|
1041
1940
|
type ResolutionChoice = "local" | "remote" | "merge" | "duplicate";
|
|
1941
|
+
/**
|
|
1942
|
+
* Notion 에서 지운 노트의 충돌인가 — 올리지 않은 로컬 편집이 남은 채 원격이 휴지통 · 보관 · 없음이다.
|
|
1943
|
+
* 풀 수 있는 것은 «로컬 유지»(Notion 에 새 페이지로 다시 만든다)와 «원격 유지»(볼트에서도 지운다)
|
|
1944
|
+
* 뿐이다 — 병합하거나 사본으로 남길 원격 본문이 없다.
|
|
1945
|
+
*/
|
|
1946
|
+
declare function isRemoteDeletion(conflict: Conflict): boolean;
|
|
1947
|
+
/** 이 충돌을 풀 수 있는 선택지 — 앱은 이것만 보여 준다. */
|
|
1948
|
+
declare function applicableChoices(conflict: Conflict): readonly ResolutionChoice[];
|
|
1949
|
+
/**
|
|
1950
|
+
* 전략이 이 충돌에 고르는 것. 원격에서 지운 노트는 복제할 원격이 없어 duplicate 도 «로컬 유지» —
|
|
1951
|
+
* 둘 다 잃는 것이 없다. manual 은 자동 병합인데 지운 원격과는 병합할 수 없어 던진다.
|
|
1952
|
+
*/
|
|
1953
|
+
declare function choiceForStrategy(conflict: Conflict, strategy: ConflictStrategy): ResolutionChoice;
|
|
1042
1954
|
interface ResolutionResult {
|
|
1043
1955
|
readonly path: string;
|
|
1044
1956
|
readonly choice: ResolutionChoice;
|
|
1045
1957
|
readonly success: boolean;
|
|
1046
1958
|
readonly mergeHadConflicts?: boolean;
|
|
1959
|
+
/** 풀지 못한 이유 — 일괄 해결(`resolveAllConflicts`)이 실패를 던지지 않고 싣는다. */
|
|
1960
|
+
readonly error?: string;
|
|
1047
1961
|
}
|
|
1962
|
+
/**
|
|
1963
|
+
* 충돌을 볼트 파일과 상태 DB 에서 푼다. **Notion 에는 올리지 않는다.**
|
|
1964
|
+
*
|
|
1965
|
+
* 앱은 `SyncOrchestrator.resolveConflict` 를 쓴다 — 이 클래스로 푼 뒤 고른 결과를 Notion 에 올리고,
|
|
1966
|
+
* 올리지 못하면 충돌로 되돌린다. 이 클래스만 부르면 지난 동기화 사본만 해결 결과로 바뀌어, 다음
|
|
1967
|
+
* pull 이 바뀐 원격으로 고른 로컬 · 병합 결과를 덮는다(N-06).
|
|
1968
|
+
*/
|
|
1048
1969
|
declare class ConflictResolver {
|
|
1049
1970
|
private readonly stateDb;
|
|
1050
1971
|
private readonly vaultFs;
|
|
@@ -1052,160 +1973,159 @@ declare class ConflictResolver {
|
|
|
1052
1973
|
resolve(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
|
|
1053
1974
|
resolveByStrategy(conflict: Conflict, strategy: ConflictStrategy): Promise<ResolutionResult>;
|
|
1054
1975
|
resolveAll(conflicts: Conflict[], strategy: ConflictStrategy): Promise<ResolutionResult[]>;
|
|
1976
|
+
/**
|
|
1977
|
+
* Notion 에서 지운 노트 — «로컬 유지» 는 추적만 놓아 파일을 새 노트로 만든다(다음 push 가 새 페이지로
|
|
1978
|
+
* 올린다. 앱은 오케스트레이터가 곧바로 올린다). «원격 유지» 는 볼트에서도 지운다.
|
|
1979
|
+
*/
|
|
1980
|
+
private resolveRemoteDeletion;
|
|
1055
1981
|
private resolveLocal;
|
|
1056
1982
|
private resolveRemote;
|
|
1057
1983
|
private resolveMerge;
|
|
1058
1984
|
private resolveDuplicate;
|
|
1985
|
+
/**
|
|
1986
|
+
* 충돌 미리보기 — 로컬(`-`)과 원격(`+`)의 줄 비교. 바뀐 곳과 앞뒤 세 줄을 `@@` 묶음으로 보인다.
|
|
1987
|
+
*
|
|
1988
|
+
* 예전에는 같은 번호의 줄끼리 견줬다 — 한쪽 앞에 한 줄만 더해도 그 뒤의 모든 줄이 바뀐 것으로 보여,
|
|
1989
|
+
* 무엇이 실제로 다른지 찾을 수 없었다.
|
|
1990
|
+
*/
|
|
1059
1991
|
generateDiff(conflict: Conflict): string;
|
|
1060
1992
|
}
|
|
1061
1993
|
|
|
1994
|
+
/**
|
|
1995
|
+
* 한 오케스트레이터의 작업은 한 번에 하나만 돈다(S-09).
|
|
1996
|
+
*
|
|
1997
|
+
* 오케스트레이터는 실행마다 상태를 쥔다 — 원격을 본 시각(N-05, `RunObservation`) · 이번 실행이 고른
|
|
1998
|
+
* 새 경로(`PagePuller` 의 `claimedPaths`) · 준비하지 못한 폴더(`FolderPlacement`). 두 실행이 겹치면
|
|
1999
|
+
* 뒤 실행이 앞 실행의 것을 덮는다. 앞 pull 이 원격을 본 시각이 뒤로 밀려 그 사이의 Notion 편집을 본
|
|
2000
|
+
* 것으로 적고(영영 받지 않는다), 두 실행이 같은 새 경로를 골라 서로의 파일을 덮는다. 플러그인의
|
|
2001
|
+
* 수동 · 자동 · 볼트 이벤트 sync 와 CLI `watch` 의 파일 변경 · 주기 sync 가 이렇게 겹쳤다.
|
|
2002
|
+
*
|
|
2003
|
+
* 겹친 요청은 기다리게 하지 않고 거절한다. 무엇을 할지는 부른 쪽마다 다르다 — 사용자가 누른
|
|
2004
|
+
* 것은 알리고, 자동 sync 는 건너뛰고, 파일 감시는 끝난 뒤 다시 돌린다. 여기서 줄을 세우면
|
|
2005
|
+
* 긴 sync(실볼트 pull 500초) 동안 주기 sync 가 쌓인다.
|
|
2006
|
+
*/
|
|
2007
|
+
/** 잠그는 작업 — 볼트 · 상태 DB · 원격을 바꾸거나 이번 실행의 기준을 정하는 것. */
|
|
2008
|
+
type GatedOperation = "push" | "pull" | "sync" | "status" | "fetch" | "resolve" | "discard";
|
|
2009
|
+
declare function operationLabel(operation: GatedOperation): string;
|
|
2010
|
+
declare class SyncBusyError extends Error {
|
|
2011
|
+
/** 이미 도는 작업. */
|
|
2012
|
+
readonly running: GatedOperation;
|
|
2013
|
+
/** 거절한 작업. */
|
|
2014
|
+
readonly requested: GatedOperation;
|
|
2015
|
+
constructor(
|
|
2016
|
+
/** 이미 도는 작업. */
|
|
2017
|
+
running: GatedOperation,
|
|
2018
|
+
/** 거절한 작업. */
|
|
2019
|
+
requested: GatedOperation);
|
|
2020
|
+
}
|
|
2021
|
+
|
|
1062
2022
|
declare class SyncOrchestrator {
|
|
1063
2023
|
private readonly config;
|
|
1064
2024
|
private readonly stateDb;
|
|
1065
2025
|
private readonly notionClient;
|
|
1066
2026
|
private readonly vaultFs;
|
|
1067
|
-
private readonly changeDetector;
|
|
1068
|
-
private readonly pipeline;
|
|
1069
|
-
private readonly blockConverter;
|
|
1070
|
-
private readonly imageHandler;
|
|
1071
2027
|
private readonly fileHandler;
|
|
1072
2028
|
private readonly databaseSyncer;
|
|
1073
|
-
|
|
1074
|
-
private readonly
|
|
1075
|
-
private
|
|
1076
|
-
private
|
|
1077
|
-
private
|
|
1078
|
-
private
|
|
1079
|
-
private
|
|
1080
|
-
private
|
|
2029
|
+
/** DB 행 push 용 — DB 마다 스키마를 읽은 매퍼. 실행마다 비운다(S-01). */
|
|
2030
|
+
private readonly rowSchemas;
|
|
2031
|
+
private readonly observation;
|
|
2032
|
+
private readonly placement;
|
|
2033
|
+
private readonly planner;
|
|
2034
|
+
private readonly detector;
|
|
2035
|
+
private readonly drift;
|
|
2036
|
+
private readonly discovery;
|
|
2037
|
+
private readonly puller;
|
|
2038
|
+
private readonly pullPlanner;
|
|
2039
|
+
private readonly pusher;
|
|
2040
|
+
private readonly conflictWorkflow;
|
|
2041
|
+
private readonly inspector;
|
|
2042
|
+
private readonly recovery;
|
|
2043
|
+
private readonly gate;
|
|
1081
2044
|
constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, customFetch?: typeof globalThis.fetch);
|
|
2045
|
+
/** 도는 작업 — 없으면 null(S-09). 부른 쪽이 겹칠 요청을 미리 거를 때 쓴다. */
|
|
2046
|
+
get runningOperation(): GatedOperation | null;
|
|
1082
2047
|
push(options?: PushOptions): Promise<PushResult>;
|
|
2048
|
+
private executePush;
|
|
2049
|
+
/** 볼트의 노트가 마지막으로 올린 내용 그대로인가 — 읽지 못하면(지움 · 옮김) 아니다. */
|
|
2050
|
+
private unchangedSincePush;
|
|
1083
2051
|
pull(options?: PullOptions): Promise<PullResult>;
|
|
2052
|
+
private executePull;
|
|
1084
2053
|
sync(options?: SyncOptions): Promise<SyncResult>;
|
|
1085
|
-
|
|
2054
|
+
private executeSync;
|
|
2055
|
+
/**
|
|
2056
|
+
* 원격까지 본다 — 이번 실행의 원격 기준을 정하므로 다른 작업과 겹치지 않는다. 원격을 훑는 데 분 단위가
|
|
2057
|
+
* 걸릴 수 있어 취소를 받는다. 취소하면 {@link OperationAbortedError} 를 던진다.
|
|
2058
|
+
*/
|
|
2059
|
+
status(options?: StatusOptions): Promise<StatusResult>;
|
|
2060
|
+
private executeStatus;
|
|
2061
|
+
/**
|
|
2062
|
+
* 로컬만 본다 — 원격 기준을 정하지 않아 도는 작업과 겹쳐도 된다. 변경은 push 가 쓰는 판정 그대로다
|
|
2063
|
+
* (옮긴 노트 · 폴더 포함). 상태 DB 는 바꾸지 않는다.
|
|
2064
|
+
*/
|
|
1086
2065
|
statusLocal(): Promise<StatusResult>;
|
|
2066
|
+
/**
|
|
2067
|
+
* 볼트에서 노트 · 폴더의 이름을 바꿨다는 기록 — 플러그인이 볼트의 rename 이벤트로 알린다(S-11).
|
|
2068
|
+
*
|
|
2069
|
+
* 다음 실행이 옮긴 노트를 추적 레코드와 짝지을 때 쓴다. 이름과 내용을 함께 바꾼 노트는 이
|
|
2070
|
+
* 기록 없이는 짝을 찾지 못해, 옛 페이지를 지우고 새 페이지를 만든다.
|
|
2071
|
+
*/
|
|
2072
|
+
recordLocalRename(from: string, to: string, kind: RenameKind): void;
|
|
2073
|
+
/** 볼트에서 지운 노트 · 폴더 — 그 자리를 새 경로로 적은 힌트를 버린다. */
|
|
2074
|
+
recordLocalDelete(path: string): void;
|
|
2075
|
+
/**
|
|
2076
|
+
* 볼트 완결성 검증 — 원격에 있는 것이 볼트에 빠짐없이 있는지 대조한다(R11-B · R12-C).
|
|
2077
|
+
*
|
|
2078
|
+
* 기존 게이트(해시 일치·repull 바이트 동일·churn 0)는 전부 **멱등성**을 본다. 매번
|
|
2079
|
+
* 같은 것을 놓치는 체계적 미발견은 그 게이트를 전부 통과한다 — 실제로 296개 DB 행이
|
|
2080
|
+
* 그렇게 침묵 유실됐다. 여기서만 "빠짐없다"를 본다.
|
|
2081
|
+
*
|
|
2082
|
+
* 대조 대상 database id 는 이 클래스가 조립한다: 설정에 적힌 것 + 디스커버리 캐시가
|
|
2083
|
+
* 아는 것 + DB 모드의 루트 DB. 캐시 표현(`discovered_dbs` 메타 키)은 `discovered-databases.ts`
|
|
2084
|
+
* 한 곳이 정하고 여기서 조립한다 — 호출부(CLI·E2E)가 그 표현을 각자 다시 해석하지 않게.
|
|
2085
|
+
*
|
|
2086
|
+
* 페이지(R12-C)는 pull 이 쓴 경로가 아니라 **search 열거**로 대조한다. 페이지는 열거
|
|
2087
|
+
* 경로가 둘이고 둘이 같은 집합을 내지 않는 게 애초의 결함이므로(R12-A), 독립된 두 번째
|
|
2088
|
+
* 열거와 맞대 봐야 의미가 있다.
|
|
2089
|
+
*/
|
|
2090
|
+
verifyCompleteness(): Promise<VaultCompletenessReport>;
|
|
1087
2091
|
fetch(): Promise<{
|
|
1088
2092
|
newPages: number;
|
|
1089
2093
|
deletedPages: number;
|
|
1090
2094
|
modifiedPages: number;
|
|
1091
2095
|
duration: number;
|
|
1092
2096
|
}>;
|
|
1093
|
-
private
|
|
1094
|
-
|
|
1095
|
-
private discoverChildDatabases;
|
|
1096
|
-
/**
|
|
1097
|
-
* 자동 발견된 child_database 1개를 DatabaseSyncer 설정으로 변환한다.
|
|
1098
|
-
* 블록 스캔/markdown 양쪽 발견 경로에서 공통으로 사용한다.
|
|
1099
|
-
* - localFolder: 부모 페이지의 obsidianPath 기준으로 폴더를 잡아 원본 중첩 구조를 보존
|
|
1100
|
-
* (예: 부모 "AI Engineer (1).md" + DB "Minirecord Project" → "AI Engineer (1)/Minirecord-Project")
|
|
1101
|
-
* - 제목 취득 실패 시 부모 폴더명 기반 안전 이름으로 폴백
|
|
1102
|
-
*/
|
|
1103
|
-
private buildDiscoveredDbConfig;
|
|
1104
|
-
/** 접근 불가 DB denylist 를 상태 메타에서 로드한다(하이픈 정규화). */
|
|
1105
|
-
private loadInaccessibleDbIds;
|
|
1106
|
-
/** linked view 컨테이너 → 원본 DB 매핑을 상태 메타에서 로드한다(nohyph → nohyph). */
|
|
1107
|
-
private loadLinkedDbMap;
|
|
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;
|
|
2097
|
+
private executeFetch;
|
|
2098
|
+
/** pull 이 쓴 노트의 Notion 링크를 볼트 링크로 푼다 — 푼 링크 수를 돌려준다. */
|
|
1119
2099
|
private resolveNotionLinks;
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
2100
|
+
/**
|
|
2101
|
+
* 해소 대상 충돌 목록 — 원격 본문을 pull 과 같은 파이프라인으로 렌더해 담는다. 렌더가 첨부를 볼트에
|
|
2102
|
+
* 내려받고 경로를 고르므로 다른 작업과 겹치지 않게 돌린다 — 도는 sync 와 같은 첨부 · 경로를 두고 다툰다.
|
|
2103
|
+
*/
|
|
2104
|
+
listConflicts(): Promise<Conflict[]>;
|
|
2105
|
+
/**
|
|
2106
|
+
* 추적 파일의 현재 원격 본문을 pull 과 같은 변환으로 렌더한다 — 표시 전용이라 첨부를 내려받지
|
|
2107
|
+
* 않는다. 추적되지 않았거나 원격 페이지가 없으면 null.
|
|
2108
|
+
*/
|
|
2109
|
+
renderRemoteSnapshot(path: string): Promise<string | null>;
|
|
2110
|
+
/** 로컬 변경 하나의 두 글 — 지난 동기화 때의 글과 지금 볼트의 글. 표시 전용이라 잠그지 않는다. */
|
|
2111
|
+
localChangeDiff(change: LocalChange): Promise<ChangeDiff>;
|
|
2112
|
+
/** 원격 변경 하나의 두 글 — 지난 동기화 때의 글과 Notion 의 지금 글. 표시 전용이라 잠그지 않는다. */
|
|
2113
|
+
remoteChangeDiff(change: RemoteChange): Promise<ChangeDiff>;
|
|
2114
|
+
/** 해소할 게 남지 않은 충돌 레코드를 `synced` 로 되돌리고, 되돌린 경로를 반환한다. */
|
|
2115
|
+
clearStaleConflicts(conflicts: readonly Conflict[]): string[];
|
|
1123
2116
|
/** 단일 충돌을 사용자가 고른 선택지(local/remote/merge/duplicate)로 해소 + Notion 전파. */
|
|
1124
2117
|
resolveConflict(conflict: Conflict, choice: ResolutionChoice): Promise<ResolutionResult>;
|
|
1125
2118
|
/** 단일 충돌을 전략(manual/local-first/remote-first/duplicate)으로 해소 + Notion 전파. */
|
|
1126
2119
|
resolveConflictByStrategy(conflict: Conflict, strategy: ConflictStrategy): Promise<ResolutionResult>;
|
|
1127
|
-
/** 여러 충돌을 동일 전략으로 일괄 해소 + Notion 전파. */
|
|
2120
|
+
/** 여러 충돌을 동일 전략으로 일괄 해소 + Notion 전파. 하나를 올리지 못해도 나머지를 푼다. */
|
|
1128
2121
|
resolveAllConflicts(conflicts: Conflict[], strategy: ConflictStrategy): Promise<ResolutionResult[]>;
|
|
1129
|
-
/** 충돌 미리보기용 통합 diff(원본 vs 로컬 vs 원격). 해소 없이 표시 전용. */
|
|
1130
|
-
generateConflictDiff(conflict: Conflict): string;
|
|
1131
2122
|
/**
|
|
1132
|
-
*
|
|
1133
|
-
*
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
*/
|
|
1139
|
-
private propagateResolution;
|
|
1140
|
-
private pushDelete;
|
|
1141
|
-
private pushMove;
|
|
1142
|
-
private detectRemoteChanges;
|
|
1143
|
-
/**
|
|
1144
|
-
* 증분 원격 변경 감지 — `since` 이후 수정된 페이지만 search 로 받아 created/modified 만
|
|
1145
|
-
* 만든다. **삭제는 의도적으로 감지하지 않는다**: search API 는 in_trash/archived 페이지를
|
|
1146
|
-
* 반환하지 않아(=사라진 것을 증분만으로는 구분 불가) 삭제 판정에는 전체 enumeration 이
|
|
1147
|
-
* 필수다. 따라서 호출부는 `deleteSync` 가 켜진 경우 이 fast-path 를 쓰지 않고
|
|
1148
|
-
* `detectRemoteChanges`(전체 스캔, 삭제 diff 포함)로 우회한다. (I10)
|
|
1149
|
-
*/
|
|
1150
|
-
private detectRemoteChangesIncremental;
|
|
1151
|
-
private isTrackedParent;
|
|
1152
|
-
private pullCreate;
|
|
1153
|
-
/**
|
|
1154
|
-
* 페이지가 폴더(자식 페이지·자식 DB 보유)인지 신뢰성 있게 판정한다.
|
|
1155
|
-
*
|
|
1156
|
-
* 결함(폴더노트 본문분리)의 근본 원인은 얕은 판정이었다 — 최상위 블록 첫 페이지에서
|
|
1157
|
-
* child_page 만 검사하면 callout·column·toggle 안에 중첩된 자식 페이지나 child_database 를
|
|
1158
|
-
* 놓쳐 폴더노트가 file 로 오분류되고, 본문이 폴더 밖 최상위로 밀려 resolveUniqueFilePath
|
|
1159
|
-
* 충돌(' (1).md')로 쪼개졌다.
|
|
1160
|
-
*
|
|
1161
|
-
* 1차: 발견 단계(서브트리 순회/증분)에서 전 페이지의 부모를 해소하며 만든
|
|
1162
|
-
* _childParentIds 집합으로 O(1) 판정(추가 API 호출 0, 처리 순서 무관) — 전체 pull 경로.
|
|
1163
|
-
* 폴백: 집합에 없을 때만(증분 pull 의 신규 폴더 등) fetchAllChildrenDeep 로 컨테이너를
|
|
1164
|
-
* 재귀 탐색해 child_page·child_database 를 직접 확인한다.
|
|
1165
|
-
*/
|
|
1166
|
-
private pageHasChildContainers;
|
|
1167
|
-
private pullUpdate;
|
|
1168
|
-
private pullDelete;
|
|
1169
|
-
private ensureFolderPage;
|
|
1170
|
-
private resolveNotionParent;
|
|
1171
|
-
private repairFolderRecords;
|
|
1172
|
-
private isFolderNote;
|
|
1173
|
-
private resolveParentPath;
|
|
1174
|
-
private pushCreatePage;
|
|
1175
|
-
private pageHasLiveChildren;
|
|
1176
|
-
private pushUpdatePage;
|
|
1177
|
-
private fetchPageMarkdown;
|
|
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;
|
|
1186
|
-
private resolveNotionIdWikilinks;
|
|
1187
|
-
private extractParentId;
|
|
1188
|
-
private resolveBlockToPageId;
|
|
1189
|
-
private cleanupInterruptedSync;
|
|
1190
|
-
/**
|
|
1191
|
-
* I12 — 중단된 push create 작업 재개.
|
|
1192
|
-
*
|
|
1193
|
-
* pending_operations 에 미완료(create·push) 항목이 있으면:
|
|
1194
|
-
* - state 에 notion_page_id 가 이미 있으면 → 생성·매핑까지는 끝났고 markCompleted 직전에
|
|
1195
|
-
* 중단된 것 → 완료 처리(잔여 본문/이미지는 다음 변경감지가 pushUpdate 로 마무리).
|
|
1196
|
-
* - notion_page_id 가 비어 있으면(Window A: 생성 적용됐으나 매핑 기록 전 중단) → 부모에서
|
|
1197
|
-
* 제목으로 child_page 를 검색해 고아 페이지를 입양(중복 생성 차단). 없으면 자리표시
|
|
1198
|
-
* 레코드를 제거(FK CASCADE 로 op 도 삭제)해 다음 push 가 새로 생성하게 한다.
|
|
1199
|
-
*
|
|
1200
|
-
* 한계(문서화): Notion 은 idempotency key 가 없어 "생성 요청 적용 직후 같은 호출 내 재시도"
|
|
1201
|
-
* 로 인한 중복은 완전히 차단하지 못한다. 429 는 적용 전 거절이라 안전하고, 프로세스 재시작
|
|
1202
|
-
* 후 재개는 본 검색-입양으로 중복을 막는다. DB 모드(부모가 database)는 child_page 검색이
|
|
1203
|
-
* 불가하므로 자리표시 제거 후 재생성으로 폴백한다.
|
|
1204
|
-
*/
|
|
1205
|
-
private recoverInterruptedPushOps;
|
|
1206
|
-
/** 부모 페이지의 직속 자식 중 제목이 일치하는(보관/휴지통 제외) child_page id 를 찾는다. */
|
|
1207
|
-
private findChildPageByTitle;
|
|
1208
|
-
private resolveUniqueFilePath;
|
|
2123
|
+
* 로컬 변경 하나를 지난 동기화 때의 글로 되돌린다 — Git 의 `restore` 와 같다. Notion 은 건드리지
|
|
2124
|
+
* 않는다. 되돌릴 원본이 없는 것은 이유와 함께 거절한다.
|
|
2125
|
+
*/
|
|
2126
|
+
discardLocalChange(path: string): Promise<void>;
|
|
2127
|
+
/** 충돌 미리보기용 줄 비교(로컬 vs 원격). 해소 없이 표시 전용. */
|
|
2128
|
+
generateConflictDiff(conflict: Conflict): string;
|
|
1209
2129
|
}
|
|
1210
2130
|
|
|
1211
2131
|
interface PathFilterConfig {
|
|
@@ -1237,18 +2157,48 @@ declare class NodeVaultFS implements VaultFS {
|
|
|
1237
2157
|
private walkDir;
|
|
1238
2158
|
}
|
|
1239
2159
|
|
|
2160
|
+
/**
|
|
2161
|
+
* push 가 노트 임베드를 올려 만든 미디어 블록 하나 — Notion 에 저장된 파일과 노트의 임베드를
|
|
2162
|
+
* 잇는다(S-05).
|
|
2163
|
+
*
|
|
2164
|
+
* 예전(R1)에는 올린 블록의 캡션에 임베드 대상을 그대로 적고, pull 이 그 캡션으로 임베드를
|
|
2165
|
+
* 되찾았다. 그래서 Notion 에서 보는 캡션이 `assets/사진.png|300` 같은 경로 · 크기 · 파일
|
|
2166
|
+
* 이름이었다. 이제 캡션에는 사람이 적은 설명만 싣고, 어느 임베드였는지는 여기 적어 둔다.
|
|
2167
|
+
*
|
|
2168
|
+
* 파일 id 로 잇는 이유: Notion 에서 같은 이름의 다른 파일로 바꾸면 id 가 바뀐다. 이름으로 이으면
|
|
2169
|
+
* 바뀐 파일을 옛 로컬 파일로 되돌려 Notion 의 편집을 잃는다.
|
|
2170
|
+
*/
|
|
2171
|
+
interface UploadedMedia {
|
|
2172
|
+
/** Notion 이 그 파일을 저장한 id(`notionFileIdOf`) — 블록 id · file_upload id 와 다르다. */
|
|
2173
|
+
readonly fileId: string;
|
|
2174
|
+
/** 노트에 적혀 있던 임베드 대상 그대로 — `경로|별칭`. */
|
|
2175
|
+
readonly target: string;
|
|
2176
|
+
/** 올린 볼트 파일(해석한 경로) — pull 이 아직 있는지 보고 되살린다. */
|
|
2177
|
+
readonly localPath: string;
|
|
2178
|
+
/**
|
|
2179
|
+
* 올린 · 받은 파일의 전체 sha256. 볼트의 그 파일이 아직 이 바이트면 본문 push 가 다시 올리지 않고
|
|
2180
|
+
* Notion 의 그 블록을 둔다(`ImageHandler.keepUploadedMedia`). 이 값을 적기 전에 적은 기록에는
|
|
2181
|
+
* 없다 — 그 미디어는 한 번 더 올리고 그때 적는다.
|
|
2182
|
+
*/
|
|
2183
|
+
readonly fileHash?: string;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1240
2186
|
/** 미디어(이미지/파일) 다운로드 운영 튜닝값. 미지정 시 기존 동작과 동일한 기본값 사용. */
|
|
1241
2187
|
interface MediaOptions {
|
|
1242
2188
|
readonly concurrency?: number;
|
|
1243
2189
|
readonly maxRetries?: number;
|
|
1244
2190
|
readonly retryBaseMs?: number;
|
|
1245
2191
|
readonly maxFileSizeBytes?: number;
|
|
2192
|
+
/** 다운로드 1회 시도의 시간 상한(ms). 기본 300초. 0 이하면 상한 없음(권장하지 않음). */
|
|
2193
|
+
readonly downloadTimeoutMs?: number;
|
|
1246
2194
|
}
|
|
1247
2195
|
interface ImageDownloadResult {
|
|
1248
2196
|
readonly originalUrl: string;
|
|
1249
2197
|
readonly localPath: string;
|
|
1250
2198
|
readonly hash: string;
|
|
1251
2199
|
readonly size: number;
|
|
2200
|
+
/** 받은 파일의 전체 sha256 — 받은 미디어 기록에 적어 push 가 그 파일이 그대로인지 본다. */
|
|
2201
|
+
readonly fileHash?: string;
|
|
1252
2202
|
}
|
|
1253
2203
|
interface ImageUploadResult {
|
|
1254
2204
|
readonly localPath: string;
|
|
@@ -1257,6 +2207,24 @@ interface ImageUploadResult {
|
|
|
1257
2207
|
readonly fileHash: string;
|
|
1258
2208
|
readonly fileSize: number;
|
|
1259
2209
|
}
|
|
2210
|
+
/** {@link ImageHandler.keepUploadedMedia} 의 결과. */
|
|
2211
|
+
interface KeptMedia {
|
|
2212
|
+
/** 보낼 본문 — 다시 올리지 않아도 되는 자리표시자를 Notion 의 그 파일로 바꿨다. */
|
|
2213
|
+
readonly markdown: string;
|
|
2214
|
+
/** 그대로 둔 미디어 — 본문을 보낸 뒤 올린 미디어 기록에 다시 적는다. */
|
|
2215
|
+
readonly kept: readonly UploadedMedia[];
|
|
2216
|
+
}
|
|
2217
|
+
/** 제자리 교체 결과 — 어떤 임베드 대상이 처리됐는지 호출자가 알아야 중복 업로드를 막는다. */
|
|
2218
|
+
interface MaterializeResult {
|
|
2219
|
+
readonly uploaded: ImageUploadResult[];
|
|
2220
|
+
/** 실제 블록으로 바뀐 임베드 대상(원본 문자열 + 별칭 제거형 둘 다 담는다). */
|
|
2221
|
+
readonly handledTargets: Set<string>;
|
|
2222
|
+
/**
|
|
2223
|
+
* 자리표시자를 찾아 본문을 고치려 했다 — 도중에 실패했어도 페이지가 바뀌었을 수 있다. 호출측은
|
|
2224
|
+
* 보낸 본문으로 만든 지문을 버리고 다시 받는다.
|
|
2225
|
+
*/
|
|
2226
|
+
readonly touched: boolean;
|
|
2227
|
+
}
|
|
1260
2228
|
declare class ImageHandler {
|
|
1261
2229
|
private readonly vaultFs;
|
|
1262
2230
|
private readonly attachmentFolder;
|
|
@@ -1267,11 +2235,47 @@ declare class ImageHandler {
|
|
|
1267
2235
|
private readonly maxRetries;
|
|
1268
2236
|
private readonly retryBaseMs;
|
|
1269
2237
|
private readonly maxFileSizeBytes;
|
|
2238
|
+
private readonly downloadTimeoutMs;
|
|
1270
2239
|
constructor(vaultFs: VaultFS, attachmentFolder?: string, notionClient?: NotionClient | undefined, customFetch?: typeof globalThis.fetch, options?: MediaOptions, stateDb?: IStateDB | undefined);
|
|
1271
2240
|
private get fetchFn();
|
|
2241
|
+
/** 상한 로직은 첨부 경로와 공유한다 — 한쪽만 빠뜨리지 않도록(utils/download-fetch). */
|
|
2242
|
+
private fetchForDownload;
|
|
1272
2243
|
downloadImage(url: string, pageTitle: string): Promise<ImageDownloadResult>;
|
|
1273
2244
|
private fetchImage;
|
|
2245
|
+
/**
|
|
2246
|
+
* 캡션이 push 가 적어 둔 원본 임베드 대상인지 판정한다(R1). 맞으면 그 문자열을
|
|
2247
|
+
* 그대로 돌려주고, 호출자는 사본을 내려받는 대신 `![[원본]]` 으로 되살린다.
|
|
2248
|
+
*
|
|
2249
|
+
* 사용자가 Notion 에서 손으로 쓴 캡션을 경로로 오인하면 깨진 위키링크가 되므로,
|
|
2250
|
+
* ① 파일명이 실제 업로드된 파일명과 같고 ② 볼트에 그 파일이 실재할 때만 인정한다.
|
|
2251
|
+
* 둘 중 하나라도 어긋나면 null 을 돌려 기존 다운로드 경로로 떨어진다.
|
|
2252
|
+
*
|
|
2253
|
+
* ②의 실재 확인은 push 와 **같은 후보 사다리**(노트 폴더 → 루트 → attachments/)를
|
|
2254
|
+
* 쓴다 — 위키링크는 파일명만 적는 게 관례라 볼트 루트만 보면 노트 옆 파일을 놓친다.
|
|
2255
|
+
*/
|
|
2256
|
+
private resolveOriginalTarget;
|
|
1274
2257
|
private persistImage;
|
|
2258
|
+
/**
|
|
2259
|
+
* push 가 올린 미디어를 노트에 적혀 있던 임베드로 되돌린다 — 아무것도 내려받지 않는다(S-05).
|
|
2260
|
+
*
|
|
2261
|
+
* push 는 임베드 하나를 제자리 블록 하나로 올리고, Notion 이 그 파일을 저장한 id 를 임베드
|
|
2262
|
+
* 대상과 함께 적어 둔다({@link rememberUploadedMedia}). 여기서는 받은 본문의 미디어 URL 에서
|
|
2263
|
+
* 그 id 를 읽어 대상을 되찾는다. 캡션에 경로를 적지 않아도 되고, Notion 에서 같은 이름의 다른
|
|
2264
|
+
* 파일로 바꾼 것(id 가 다르다)을 옛 로컬 파일로 되돌리지도 않는다.
|
|
2265
|
+
*
|
|
2266
|
+
* 적어 둔 것이 없으면 옛 push 처럼 캡션이 임베드 대상인지 본다(R1). 둘 다 아니면 그대로 두고,
|
|
2267
|
+
* 뒤따르는 {@link downloadAllImages} · {@link downloadAllFiles} 가 사본을 받는다.
|
|
2268
|
+
*
|
|
2269
|
+
* 내려받지 않으므로 표시 전용 렌더(diff)나 이미지 다운로드를 끈 설정에서도 부른다.
|
|
2270
|
+
*
|
|
2271
|
+
* @param pageId 본문을 받은 페이지 — 그 페이지에 올린 것만 되찾는다
|
|
2272
|
+
* @param notePath 노트 경로 — 옛 캡션의 파일을 push 와 같은 사다리로 찾는다
|
|
2273
|
+
*/
|
|
2274
|
+
restoreUploadedMedia(markdown: string, pageId?: string, notePath?: string): Promise<string>;
|
|
2275
|
+
/** 적어 둔 업로드면 되살릴 임베드 대상. 볼트에서 그 파일이 사라졌으면 null — 사본을 받는다. */
|
|
2276
|
+
private knownEmbedTarget;
|
|
2277
|
+
/** 미디어 URL 이 가리키는 파일 이름 — 서명 URL 은 경로 끝, 내부 참조는 `attachment:` 뒤. */
|
|
2278
|
+
private uploadedNameOf;
|
|
1275
2279
|
downloadAllImages(markdown: string, pageTitle: string, pageId?: string): Promise<{
|
|
1276
2280
|
content: string;
|
|
1277
2281
|
downloads: ImageDownloadResult[];
|
|
@@ -1279,13 +2283,99 @@ declare class ImageHandler {
|
|
|
1279
2283
|
uploadLocalImage(localPath: string, noteFolder?: string): Promise<ImageUploadResult>;
|
|
1280
2284
|
uploadAndAppendImages(pageId: string, images: ImageReference[], notePath?: string): Promise<ImageUploadResult[]>;
|
|
1281
2285
|
/**
|
|
1282
|
-
*
|
|
1283
|
-
*
|
|
1284
|
-
*
|
|
2286
|
+
* 본문을 보내기 전에, 다시 올리지 않아도 되는 미디어 자리표시자를 Notion 에 이미 있는 그 파일로
|
|
2287
|
+
* 바꾼다.
|
|
2288
|
+
*
|
|
2289
|
+
* 본문 push 는 자리표시자를 심어 본문을 통째로 바꾸고, 바꾼 뒤 자리표시자를 하나씩 올려 블록으로
|
|
2290
|
+
* 바꾼다({@link materializeLocalMedia}). 그래서 글 한 줄만 고쳐도 그 페이지의 미디어를 모두 다시
|
|
2291
|
+
* 올렸다 — 업로드 요청이 미디어마다 셋이고, Notion 의 블록 · 파일이 새로 바뀌었다. 받은 본문의 미디어
|
|
2292
|
+
* URL 을 그대로 실어 본문을 바꾸면 Notion 은 그 블록을 그대로 둔다 — 블록 id · 파일이 같고 캡션만
|
|
2293
|
+
* 실은 글로 바뀐다. 자리를 옮겨도, 서명(쿼리)을 떼도 같다(2026-10-12 실측).
|
|
2294
|
+
*
|
|
2295
|
+
* 둘 다 맞는 자리표시자만 바꾼다 — 나머지는 예전처럼 올린다.
|
|
2296
|
+
* - 그 페이지의 미디어 기록에 같은 경로의 임베드가 있고, 받은 본문에 그 파일이 지금 있다
|
|
2297
|
+
* - 볼트의 그 파일이 적어 둔 지문과 같다 — 같은 이름에 다른 내용이면 올려야 한다
|
|
2298
|
+
*
|
|
2299
|
+
* 기록이 없거나(이 기능 이전에 올린 미디어) 본문을 읽지 못하면 아무것도 바꾸지 않는다 — 다시 올릴
|
|
2300
|
+
* 뿐 잃는 것은 없다.
|
|
2301
|
+
*
|
|
2302
|
+
* @param markdown 보낼 본문(Notion markdown) — 자리표시자가 줄 단독으로 있다
|
|
2303
|
+
*/
|
|
2304
|
+
keepUploadedMedia(pageId: string, markdown: string, notePath?: string): Promise<KeptMedia>;
|
|
2305
|
+
/**
|
|
2306
|
+
* 자리표시자 자리에 실을 Notion 미디어 한 줄. 종류 · 캡션 · URL 을 그대로 실을 수 없거나 볼트의 파일이
|
|
2307
|
+
* 올린 것과 다르면 null — 그 자리표시자는 다시 올린다.
|
|
2308
|
+
*/
|
|
2309
|
+
private keptMediaLine;
|
|
2310
|
+
/**
|
|
2311
|
+
* 그대로 둔 미디어에 실을 캡션 — 임베드의 설명. 캡션 없는 파일을 받으면 올라간 이름을 별칭으로
|
|
2312
|
+
* 적으므로({@link fileNameOf}), Notion 에 캡션이 없고 별칭이 그 이름이면 캡션을 비운 채 둔다 — 실으면
|
|
2313
|
+
* 받은 파일마다 이름이 캡션으로 붙는다.
|
|
2314
|
+
*/
|
|
2315
|
+
private keptCaptionOf;
|
|
2316
|
+
/**
|
|
2317
|
+
* push 가 심어 둔 자리표시자 quote 를 **제자리에서** 실제 image/file 블록으로 바꾼다(R1).
|
|
2318
|
+
*
|
|
2319
|
+
* 그 전에는 임베드 하나가 Notion 에서 두 조각으로 갈라졌다 — 본문 자리에는
|
|
2320
|
+
* `> 📎 foo.png` + `%% im-nobsidian:local-image:… %%` 리터럴 마커가, 실제 이미지는
|
|
2321
|
+
* 페이지 맨 끝에 따로. 실측: 볼트 175개 파일에서 마커 978건이 Notion 에 그대로
|
|
2322
|
+
* 텍스트로 새고 있었고 그중 964건(98.6%)이 이 미디어 쌍이었다. 사용자가 Notion 에서
|
|
2323
|
+
* 보는 화면이 깨질 뿐 아니라 이미지가 본문 흐름에서 이탈한다.
|
|
2324
|
+
*
|
|
2325
|
+
* 캡션에는 사람이 적은 설명만 단다({@link captionOfEmbed}). 어느 임베드였는지는 Notion 이
|
|
2326
|
+
* 그 파일을 저장한 id 와 함께 적어 두고({@link rememberUploadedMedia}), pull 이 그것으로
|
|
2327
|
+
* 임베드를 되찾는다(S-05). 예전에는 캡션에 임베드 대상을 그대로 적어 Notion 에 경로 · 크기 ·
|
|
2328
|
+
* 파일 이름이 보였다.
|
|
2329
|
+
*
|
|
2330
|
+
* 볼트에서 파일을 못 찾으면 자리표시자를 그대로 둔다 — 마커가 남아야 pull 이
|
|
2331
|
+
* 임베드를 복원할 수 있으므로, 반쯤 지우는 것보다 안전하다.
|
|
2332
|
+
*/
|
|
2333
|
+
materializeLocalMedia(pageId: string, pushedMarkdown: string, notePath?: string, kept?: readonly UploadedMedia[]): Promise<MaterializeResult>;
|
|
2334
|
+
/**
|
|
2335
|
+
* 방금 만든 미디어 블록의 파일을 Notion 이 저장한 id. 만든 응답에 파일 URL 이 실려 오고,
|
|
2336
|
+
* 없으면 블록을 한 번 조회한다. 끝내 모르면 null — 던지지 않는다(블록은 이미 제자리에 있다).
|
|
2337
|
+
*/
|
|
2338
|
+
private storedFileIdOf;
|
|
2339
|
+
/**
|
|
2340
|
+
* 페이지 블록 트리를 훑어 자리표시자를 부모와 함께 모은다. 콜아웃·토글·목록 안에
|
|
2341
|
+
* 들어간 임베드도 제자리 교체하려면 부모 id 가 필요하다(append 의 after_block 은
|
|
2342
|
+
* 같은 부모의 자식만 기준으로 삼는다).
|
|
2343
|
+
*/
|
|
2344
|
+
private collectPlaceholders;
|
|
2345
|
+
/**
|
|
2346
|
+
* 업로드분을 file_registry 에 남긴다. 노트 임베드는 여기서 페이지에 붙으므로,
|
|
2347
|
+
* 이후 FileHandler.pushAllFiles 가 같은 첨부를 폴더 페이지에 standalone 으로
|
|
2348
|
+
* 재업로드하는 중복을 공유 레지스트리로 차단한다.
|
|
2349
|
+
*/
|
|
2350
|
+
private registerUploads;
|
|
2351
|
+
/**
|
|
2352
|
+
* 올린 미디어를 임베드와 함께 적어 둔다(S-05). 적지 못해도 push 는 깨뜨리지 않는다 — 블록은
|
|
2353
|
+
* 이미 제자리에 있고, 다음 pull 이 그 파일을 알아보지 못해 사본을 받을 뿐이다.
|
|
2354
|
+
*/
|
|
2355
|
+
private rememberUploads;
|
|
2356
|
+
/**
|
|
2357
|
+
* 받은 미디어를 그 페이지의 미디어 기록에 더한다 — 그 노트를 고쳐 push 할 때 Notion 의 원래 블록을
|
|
2358
|
+
* 둔다({@link keepUploadedMedia}). 적지 못해도 pull 은 깨뜨리지 않는다 — 그 노트를 push 하면 예전처럼
|
|
2359
|
+
* 다시 올릴 뿐이다.
|
|
2360
|
+
*/
|
|
2361
|
+
private rememberReceived;
|
|
2362
|
+
/**
|
|
2363
|
+
* 임베드 경로가 가리킬 수 있는 볼트 실제 파일 후보를 우선순위대로 만든다(F23).
|
|
2364
|
+
* Obsidian 위키링크 임베드는 노트 옆 파일을 파일명만으로 참조하는 게 관례라
|
|
2365
|
+
* 노트 폴더 → 볼트 루트 → attachments/ 순.
|
|
2366
|
+
*
|
|
2367
|
+
* push(업로드)와 pull(원본 복원)이 **같은 사다리**를 봐야 왕복이 닫힌다. 예전에는
|
|
2368
|
+
* pull 쪽이 볼트 루트만 확인해서, `__e2e_probe__/노트.md` 안의 `![[t-img.png]]` 는
|
|
2369
|
+
* 올라갈 때는 노트 폴더에서 잘 찾아 올리고도 내려올 때는 원본을 못 찾아 사본을
|
|
2370
|
+
* 새로 받았다(실측) — 왕복할 때마다 첨부가 한 벌씩 늘어난다.
|
|
2371
|
+
*/
|
|
2372
|
+
private vaultCandidates;
|
|
2373
|
+
/**
|
|
2374
|
+
* 임베드 경로를 볼트 실제 파일로 해석해 읽는다(F23).
|
|
2375
|
+
* 폴백은 조용히 진행하고, 전부 실패했을 때만 마지막 오류를 던진다.
|
|
1285
2376
|
*/
|
|
1286
2377
|
private readImageFromVault;
|
|
1287
|
-
|
|
1288
|
-
downloadAllFiles(markdown: string, pageTitle: string): Promise<{
|
|
2378
|
+
downloadAllFiles(markdown: string, pageTitle: string, pageId?: string): Promise<{
|
|
1289
2379
|
content: string;
|
|
1290
2380
|
downloads: ImageDownloadResult[];
|
|
1291
2381
|
}>;
|
|
@@ -1307,7 +2397,25 @@ declare class ImageHandler {
|
|
|
1307
2397
|
private guessFileExtension;
|
|
1308
2398
|
}
|
|
1309
2399
|
|
|
2400
|
+
/**
|
|
2401
|
+
* 확장자 → MIME / Notion 블록타입 매핑의 단일 출처.
|
|
2402
|
+
*
|
|
2403
|
+
* 예전에는 ImageHandler 가 이미지 확장자만 담은 축소판 테이블을 따로 들고 있었다.
|
|
2404
|
+
* 그 결과 노트에 박힌 `![[문서.txt]]` 같은 임베드를 올릴 때 `application/octet-stream`
|
|
2405
|
+
* 으로 요청해 Notion File Upload API 가 400 으로 거절했다(실측). 업로드 경로가 둘인데
|
|
2406
|
+
* 테이블이 둘이면 한쪽만 고쳐지므로 여기로 합친다.
|
|
2407
|
+
*/
|
|
1310
2408
|
type NotionBlockType = "image" | "pdf" | "video" | "audio" | "file";
|
|
2409
|
+
|
|
2410
|
+
/** 첨부 다운로드 운영 튜닝값. 미지정 시 기존 동작과 동일한 기본값 사용. */
|
|
2411
|
+
interface FileHandlerOptions {
|
|
2412
|
+
/** 다운로드에 쓸 fetch 구현(테스트 주입용). 기본 전역 fetch. */
|
|
2413
|
+
readonly fetch?: typeof globalThis.fetch;
|
|
2414
|
+
/** 다운로드 1회 시도의 시간 상한(ms). 기본 300초. 0 이하면 상한 없음(권장하지 않음). */
|
|
2415
|
+
readonly downloadTimeoutMs?: number;
|
|
2416
|
+
/** 첨부 1건 업로드 전체의 시간 상한(ms). 기본 30분. 0 이하면 상한 없음. */
|
|
2417
|
+
readonly itemTimeoutMs?: number;
|
|
2418
|
+
}
|
|
1311
2419
|
interface FileUploadResult {
|
|
1312
2420
|
readonly localPath: string;
|
|
1313
2421
|
readonly fileUploadId: string;
|
|
@@ -1324,13 +2432,37 @@ declare class FileHandler {
|
|
|
1324
2432
|
private readonly notionClient;
|
|
1325
2433
|
private readonly stateDb;
|
|
1326
2434
|
private readonly sema;
|
|
1327
|
-
|
|
2435
|
+
private readonly customFetch?;
|
|
2436
|
+
private readonly downloadTimeoutMs;
|
|
2437
|
+
private readonly itemTimeoutMs;
|
|
2438
|
+
constructor(vaultFs: VaultFS, notionClient: NotionClient, stateDb: IStateDB, concurrency?: number, options?: FileHandlerOptions);
|
|
2439
|
+
private get fetchFn();
|
|
1328
2440
|
pushFilesForFolder(folderPageId: string, folderPath: string): Promise<FileUploadResult[]>;
|
|
1329
2441
|
/**
|
|
1330
|
-
*
|
|
1331
|
-
*
|
|
1332
|
-
*
|
|
1333
|
-
*
|
|
2442
|
+
* 첨부 1건을 폴더 페이지에 올린다 — 두 push 경로(`pushFilesForFolder` · `pushAllFiles`)의
|
|
2443
|
+
* 공통 단위. 변경 없음 판정·동시성 슬롯·시간 상한·실패 격리를 모두 여기 한 곳에 둔다.
|
|
2444
|
+
*
|
|
2445
|
+
* 한 곳에 모은 이유는 R9a 에서 실제로 데인 적이 있어서다. 같은 다운로드 로직이
|
|
2446
|
+
* image/file 핸들러에 두 벌 존재한 탓에 한쪽에만 상한이 걸린 채로 남았고, 상한 없는
|
|
2447
|
+
* 쪽이 세마포어를 쥔 채 영원히 매달렸다. 여기도 같은 코드가 두 루프에 나뉘어 있었다.
|
|
2448
|
+
*
|
|
2449
|
+
* 상한(R9f): 업로드는 호출 1건마다 상한이 있어도(SDK 30초) 멀티파트는 파트 수만큼
|
|
2450
|
+
* 호출이 늘어나는 **합성 경로**라 전체로는 여전히 무한대다. 상한을 넘긴 첨부는 경고만
|
|
2451
|
+
* 남기고 나머지 첨부는 계속 올린다 — 첨부 하나가 push 전체를 멎게 하지 않는다.
|
|
2452
|
+
*
|
|
2453
|
+
* @returns 업로드 결과. 변경 없음(스킵)이거나 실패했으면 null.
|
|
2454
|
+
*/
|
|
2455
|
+
private pushSingleFile;
|
|
2456
|
+
/**
|
|
2457
|
+
* 업로드 후보 목록 — 도구 내부 산출물(`.base`/`.notion.json`)은 아예 빼고, Notion 이 받지 않는
|
|
2458
|
+
* 형식은 따로 모은다.
|
|
2459
|
+
*
|
|
2460
|
+
* 내부 산출물: node 측 VaultFS 는 과거 `.base` 를 워커에서 빼는 식으로 우회했지만, Obsidian
|
|
2461
|
+
* 어댑터는 모든 파일을 반환하므로 플러그인 push 가 사이드카를 Notion 첨부로 오염시켰다.
|
|
2462
|
+
* 소비자인 여기서 일괄 차단해 구현체 간 동작을 통일한다.
|
|
2463
|
+
*
|
|
2464
|
+
* 받지 않는 형식(S-14): 올리면 업로드를 만들 때 400 으로 거절된다. 등록되지 않으니 push 할
|
|
2465
|
+
* 때마다 같은 요청을 다시 보내 같은 이유로 실패했다(실볼트 `.ipynb` 22개).
|
|
1334
2466
|
*/
|
|
1335
2467
|
private listUploadableFiles;
|
|
1336
2468
|
pushAllFiles(): Promise<FileUploadResult[]>;
|
|
@@ -1355,19 +2487,120 @@ declare class ConversionPipeline {
|
|
|
1355
2487
|
convertToMarkdown(content: string, context: ConversionContext, metadata?: ProcessorMetadata): string;
|
|
1356
2488
|
}
|
|
1357
2489
|
|
|
2490
|
+
interface DbBaseFile {
|
|
2491
|
+
readonly basePath: string;
|
|
2492
|
+
readonly title: string;
|
|
2493
|
+
}
|
|
2494
|
+
/**
|
|
2495
|
+
* DB 마다 실제로 기록한 `.base` 경로와 DB 제목 — 인라인 DB placeholder 를 `.base` 임베드로 바꿀 때
|
|
2496
|
+
* 쓴다(F22). 폴더명(하이픈 새니타이즈)과 `.base` 파일명(sanitizeFileName: 공백 · 점 보존)은 규칙이
|
|
2497
|
+
* 달라, 폴더로 추측한 경로는 깨진 임베드가 된다(E2E 실측 91/158건).
|
|
2498
|
+
*
|
|
2499
|
+
* 예전에는 이번 실행이 기록한 것만 메모리에 두었다 — pull 마다 모든 DB 의 `.base` 를 다시 썼기
|
|
2500
|
+
* 때문이다. 이제는 바뀐 DB 만 조회하므로(ADR-027) 이번에 조회하지 않은 DB 의 것도 알아야 한다. 그래서
|
|
2501
|
+
* 상태 DB 에 남긴다. 값이 바뀔 때만 쓴다 — 전체 대조는 DB 마다 한 번씩 부른다.
|
|
2502
|
+
*/
|
|
2503
|
+
declare class DbBaseFiles {
|
|
2504
|
+
private readonly stateDb;
|
|
2505
|
+
private entries;
|
|
2506
|
+
constructor(stateDb: Pick<IStateDB, "getMeta" | "setMeta">);
|
|
2507
|
+
get(databaseId: string): DbBaseFile | undefined;
|
|
2508
|
+
set(databaseId: string, info: DbBaseFile): void;
|
|
2509
|
+
/** 상태 메타에서 한 번 읽는다. 없거나 깨졌으면 비어 있다 — 이번 실행이 기록하는 것부터 다시 쌓는다. */
|
|
2510
|
+
private load;
|
|
2511
|
+
}
|
|
2512
|
+
|
|
2513
|
+
/**
|
|
2514
|
+
* 원격을 얼마나 훑을지 — 전체 대조인가, 바뀐 것만인가 (ADR-027).
|
|
2515
|
+
*
|
|
2516
|
+
* 전체 대조는 루트 아래 페이지를 모두 순회하고 동기화하는 DB 를 모두 조회한다. 원격에서 지운 것 ·
|
|
2517
|
+
* 범위 밖으로 옮긴 것은 이것으로만 보인다 — search 는 휴지통을 돌려주지 않는다. 대신 비싸다: 실볼트
|
|
2518
|
+
* (발견 DB 166개)에서 원격이 그대로인데도 재pull 이 523.5초 걸렸다(실측 2026-09-28). 예전에는
|
|
2519
|
+
* `deleteSync` 가 켜져 있으면 pull · 상태 확인마다 전체 대조를 했고, 플러그인은 늘 켠다.
|
|
2520
|
+
*
|
|
2521
|
+
* 그래서 전체 대조는 주기(`sync.fullReconcileInterval`)마다만 하고, 그 사이에는 지난 pull 뒤에 바뀐
|
|
2522
|
+
* 페이지 · 행 · DB 스키마만 search 로 찾아 그 DB 만 조회한다. 원격 삭제는 다음 전체 대조가 반영한다.
|
|
2523
|
+
*/
|
|
2524
|
+
|
|
2525
|
+
/**
|
|
2526
|
+
* 이번 pull 이 조회할 DB. `all` — 전체 대조. `changed` — 바뀐 것이 보인 DB 만(id 는
|
|
2527
|
+
* {@link compactNotionId}). 새로 발견한 DB 는 어느 쪽이든 조회한다 — 받은 적이 없다.
|
|
2528
|
+
*/
|
|
2529
|
+
type DatabaseSelection = {
|
|
2530
|
+
readonly kind: "all";
|
|
2531
|
+
} | {
|
|
2532
|
+
readonly kind: "changed";
|
|
2533
|
+
readonly ids: ReadonlySet<string>;
|
|
2534
|
+
};
|
|
2535
|
+
/**
|
|
2536
|
+
* DB 마다 이번 pull 이 어떻게 끝났나 — 다음 pull 이 바뀐 것이 없어도 다시 조회할 DB
|
|
2537
|
+
* ({@link PENDING_DATABASES_META_KEY})를 가른다.
|
|
2538
|
+
*
|
|
2539
|
+
* 조회하다 실패했거나 · 받지 못한 행이 있거나 · 취소로 닿지 못한 DB 는 대기로 남는다. 받았거나 · 이제
|
|
2540
|
+
* 없어서 뺀 DB 는 대기에서 빠진다. 이번에 조회하지 않은 DB 의 대기는 그대로 남는다.
|
|
2541
|
+
*/
|
|
2542
|
+
declare class DatabasePullLedger {
|
|
2543
|
+
private readonly selection;
|
|
2544
|
+
private readonly pending;
|
|
2545
|
+
private readonly settled;
|
|
2546
|
+
private readonly unfinished;
|
|
2547
|
+
private skippedCount;
|
|
2548
|
+
/** 이제 동기화하는 DB — 알면 대기를 이것으로 좁힌다. */
|
|
2549
|
+
private syncing;
|
|
2550
|
+
constructor(selection: DatabaseSelection, pending: ReadonlySet<string>);
|
|
2551
|
+
/** 이 DB 를 조회하나 — 새로 발견한 DB 는 부른 쪽이 따로 조회한다(받은 적이 없다). */
|
|
2552
|
+
selects(databaseId: string): boolean;
|
|
2553
|
+
/** 바뀐 것이 보이지 않아 조회하지 않았다. */
|
|
2554
|
+
skip(): void;
|
|
2555
|
+
/** 받았거나 · 이제 없어서 뺐다 — 다시 조회할 것이 없다. */
|
|
2556
|
+
settle(databaseId: string): void;
|
|
2557
|
+
/** 조회하다 실패했거나 · 받지 못한 행이 있거나 · 취소로 닿지 못했다 — 다음 pull 이 다시 조회한다. */
|
|
2558
|
+
retry(databaseId: string): void;
|
|
2559
|
+
get skipped(): number;
|
|
2560
|
+
/**
|
|
2561
|
+
* 이제 동기화하는 DB 를 알린다 — 대기를 이것으로 좁힌다. 설정에서 뺐거나 접근 불가로 뺀 DB 를 영영
|
|
2562
|
+
* 들고 있지 않는다. 알리지 않으면(발견이 멈춤 등) 좁히지 않는다 — 모르는 것을 버리지 않는다.
|
|
2563
|
+
*/
|
|
2564
|
+
keepOnly(databaseIds: Iterable<string>): void;
|
|
2565
|
+
/** 다음 pull 의 대기 — 지난 대기에서 받은 것을 빼고 이번에 받지 못한 것을 더한다. */
|
|
2566
|
+
nextPending(): Set<string>;
|
|
2567
|
+
}
|
|
2568
|
+
|
|
2569
|
+
/** 한 번의 동기화가 원격을 보는 기준 — 판정 · 기록에 함께 쓴다. */
|
|
2570
|
+
interface ObservationContext {
|
|
2571
|
+
/**
|
|
2572
|
+
* 이번 동기화가 시작한 시각. 원격을 «본» 시각으로 적는다 — 실제로 본 시각보다 이르거나 같아서
|
|
2573
|
+
* 가라앉았다고 서둘러 보지 않는다. 동기화 밖이면 null(가라앉았다고 보지 않는다).
|
|
2574
|
+
*/
|
|
2575
|
+
readonly seenAt: string | null;
|
|
2576
|
+
/** 이 토큰의 봇 id. 받지 못했으면 null — 봇 규칙 없이 가른다. */
|
|
2577
|
+
readonly botUserId: string | null;
|
|
2578
|
+
}
|
|
2579
|
+
|
|
1358
2580
|
interface DatabaseSyncResult {
|
|
1359
2581
|
created: number;
|
|
1360
2582
|
updated: number;
|
|
1361
|
-
/**
|
|
2583
|
+
/**
|
|
2584
|
+
* Notion 에서 지워져 볼트에서도 지운 행 수(deleteSync). 행은 DB 조회로만 보여, 행의 삭제는 전체
|
|
2585
|
+
* 대조가 아니라 여기서 가른다(S-12).
|
|
2586
|
+
*/
|
|
2587
|
+
deleted: number;
|
|
2588
|
+
/** Pull 시 로컬·리모트 동시 수정으로 발생한 충돌. */
|
|
1362
2589
|
conflicts: Conflict[];
|
|
1363
2590
|
failed: FailedOperation[];
|
|
1364
2591
|
/**
|
|
1365
2592
|
* 이번 pull 에서 실제로 디스크에 기록한 db-row 파일 경로(SSOT).
|
|
1366
2593
|
* 오케스트레이터의 링크 해소(resolveNotionLinks) 대상은 반드시 이 목록으로 정한다 —
|
|
1367
2594
|
* 과거의 `getByStatus("synced").slice(-N)` 휴리스틱은 ORDER BY 가 없어 증분 pull 에서
|
|
1368
|
-
* 엉뚱한 행을 골라 정작 바뀐 행의 relation UUID 를 영영 해소하지 못했다(M3).
|
|
2595
|
+
* 엉뚱한 행을 골라 정작 바뀐 행의 relation UUID 를 영영 해소하지 못했다(M3).
|
|
1369
2596
|
*/
|
|
1370
2597
|
writtenPaths: string[];
|
|
2598
|
+
/**
|
|
2599
|
+
* R13: 볼트에서 사라졌기에 되살린 행 수. `updated` 와 섞지 않는다 — 리모트는 그대로이고
|
|
2600
|
+
* 바뀐 것은 로컬의 부재이므로, 합쳐 세면 dry-run 과 멱등성 게이트(churn)가 "원격이
|
|
2601
|
+
* 바뀌었다"고 거짓말한다. 페이지 경로가 이미 같은 이유로 `restored` 를 분리해 보고한다.
|
|
2602
|
+
*/
|
|
2603
|
+
restored: number;
|
|
1371
2604
|
/**
|
|
1372
2605
|
* F25: 이 config 가 linked view 컨테이너로 판정된 경우 data source 원본 database id.
|
|
1373
2606
|
* 행 처리는 원본 config 에 양보하고 건너뛰었으므로, 호출처(디스커버리 캐시)는 이 config
|
|
@@ -1375,6 +2608,22 @@ interface DatabaseSyncResult {
|
|
|
1375
2608
|
*/
|
|
1376
2609
|
linkedOriginalDbId?: string;
|
|
1377
2610
|
}
|
|
2611
|
+
/** dry-run 이 센 행 하나 — 경로와 할 일. `restore` 는 볼트에서 사라진 행을 되살리는 것이다. */
|
|
2612
|
+
interface PlannedRow {
|
|
2613
|
+
readonly pageId: string;
|
|
2614
|
+
readonly path: string;
|
|
2615
|
+
readonly operation: "create" | "update" | "delete";
|
|
2616
|
+
readonly restore?: boolean;
|
|
2617
|
+
}
|
|
2618
|
+
/**
|
|
2619
|
+
* 행을 받는 진행 — 진행 표시가 페이지와 행을 한 목록 · 한 수로 보이게 한다. 행은 DB 를 조회해야 몇
|
|
2620
|
+
* 개인지 알아, 받기 전에 이 DB 에서 받을 행 수를 `planned` 로 알리고 행 하나를 마칠 때마다 `done` 을
|
|
2621
|
+
* 부른다. 지운 행은 조회에 없어 미리 셀 수 없다 — 지울 때 `planned(1)` 뒤에 `done` 을 부른다.
|
|
2622
|
+
*/
|
|
2623
|
+
interface RowProgress {
|
|
2624
|
+
planned(count: number): void;
|
|
2625
|
+
done(item: ProgressItem): void;
|
|
2626
|
+
}
|
|
1378
2627
|
declare class DatabaseSyncer {
|
|
1379
2628
|
private readonly config;
|
|
1380
2629
|
private readonly stateDb;
|
|
@@ -1382,23 +2631,33 @@ declare class DatabaseSyncer {
|
|
|
1382
2631
|
private readonly vaultFs;
|
|
1383
2632
|
private readonly pipeline;
|
|
1384
2633
|
private readonly imageHandler;
|
|
2634
|
+
/**
|
|
2635
|
+
* 이번 동기화가 원격을 보는 기준(N-05) — 오케스트레이터가 실행마다 정한다. 따로 쓰면(시험)
|
|
2636
|
+
* 아무것도 가라앉았다고 보지 않는다 — 수정 시각이 같은 행도 받아서 견준다.
|
|
2637
|
+
*/
|
|
2638
|
+
private readonly observation;
|
|
1385
2639
|
private readonly propertyMapper;
|
|
1386
2640
|
private readonly baseFileGenerator;
|
|
1387
2641
|
private readonly sidecarGenerator;
|
|
1388
2642
|
/**
|
|
1389
|
-
*
|
|
1390
|
-
*
|
|
1391
|
-
* (sanitizeFileName: 공백·점 보존)은 규칙이 달라 localFolder 로 추측한 경로는 깨진
|
|
1392
|
-
* 임베드가 된다. generateBaseFile 은 매 pull 모든 DB 에 대해 실행되므로 pull 종료
|
|
1393
|
-
* 시점에는 성공한 DB 전체가 채워져 있다.
|
|
2643
|
+
* 실제로 기록한 .base 경로/DB 제목 — placeholder 임베드 재작성(F22)의 SSOT. 이번 pull 이 조회하지
|
|
2644
|
+
* 않은 DB 의 것도 남아 있다({@link DbBaseFiles}).
|
|
1394
2645
|
*/
|
|
1395
|
-
readonly baseFileInfo:
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
2646
|
+
readonly baseFileInfo: DbBaseFiles;
|
|
2647
|
+
constructor(config: Config, stateDb: IStateDB, notionClient: NotionClient, vaultFs: VaultFS, pipeline: ConversionPipeline, imageHandler: ImageHandler,
|
|
2648
|
+
/**
|
|
2649
|
+
* 이번 동기화가 원격을 보는 기준(N-05) — 오케스트레이터가 실행마다 정한다. 따로 쓰면(시험)
|
|
2650
|
+
* 아무것도 가라앉았다고 보지 않는다 — 수정 시각이 같은 행도 받아서 견준다.
|
|
2651
|
+
*/
|
|
2652
|
+
observation?: () => ObservationContext);
|
|
2653
|
+
pullAll(opts?: {
|
|
2654
|
+
readonly paths?: readonly string[];
|
|
2655
|
+
readonly progress?: RowProgress;
|
|
2656
|
+
/** 조회할 DB 와 DB 마다의 결과(ADR-027). 없으면 모든 DB 를 조회한다. */
|
|
2657
|
+
readonly ledger?: DatabasePullLedger;
|
|
2658
|
+
/** 취소하면 다음 DB 를 조회하지 않는다 — 닿지 못한 DB 는 다음 pull 이 조회한다. */
|
|
2659
|
+
readonly signal?: AbortLike;
|
|
2660
|
+
}): Promise<DatabaseSyncResult>;
|
|
1402
2661
|
pullDatabase(dbConfig: DatabaseSyncConfig, opts?: {
|
|
1403
2662
|
/**
|
|
1404
2663
|
* F25: database id(하이픈 유무 무관) → 그 DB 행이 사는 로컬 폴더. 디스커버리 캐시가
|
|
@@ -1406,8 +2665,100 @@ declare class DatabaseSyncer {
|
|
|
1406
2665
|
* 경우에만 행 소유를 양보한다 — 원본이 미발견이면 이 컨테이너가 유일한 접근 통로다.
|
|
1407
2666
|
*/
|
|
1408
2667
|
resolveDbFolder?: (databaseId: string) => string | null;
|
|
2668
|
+
/**
|
|
2669
|
+
* 이 범위의 행만 받고 지운다(`pull --path` · 변경 패널의 항목별 받기). 없으면 모든 행.
|
|
2670
|
+
* 새 행은 DB 폴더 전체가 범위일 때만 만든다 — 받기 전에는 어느 경로에 쓸지 모른다.
|
|
2671
|
+
*/
|
|
2672
|
+
paths?: readonly string[];
|
|
2673
|
+
/** 받은 행 · 지운 행을 하나씩 알린다. 예전에는 pull 진행 표시에 행이 하나도 보이지 않았다. */
|
|
2674
|
+
progress?: RowProgress;
|
|
1409
2675
|
}): Promise<DatabaseSyncResult>;
|
|
2676
|
+
/**
|
|
2677
|
+
* 받을 행을 세기만 한다(dry-run) — 볼트 · 상태 DB · Notion 을 바꾸지 않는다. 조회 · 범위 · 받을
|
|
2678
|
+
* 행 판정 · 삭제 확인은 {@link pullDatabase} 와 같다.
|
|
2679
|
+
*
|
|
2680
|
+
* 행 본문은 읽지 않는다. 그래서 같은 분 안의 편집(«확인 안 됨»)은 고침으로 세고, 받아 보고 같으면
|
|
2681
|
+
* 쓰지 않을 행 · local-first 로 지킬 행 · 충돌로 남길 행도 고침으로 센다. 새 행의 경로는 pull 과 같은
|
|
2682
|
+
* 차례(ID 순)로 고른다 — 이번에 만들 행끼리 이름이 겹치면 뒤 차례에 id 가 붙는다.
|
|
2683
|
+
*/
|
|
2684
|
+
planDatabase(dbConfig: DatabaseSyncConfig, opts?: {
|
|
2685
|
+
resolveDbFolder?: (databaseId: string) => string | null;
|
|
2686
|
+
paths?: readonly string[];
|
|
2687
|
+
}): Promise<PlannedRow[]>;
|
|
2688
|
+
/**
|
|
2689
|
+
* 이 DB 에서 볼 행을 조회한다. 범위(`paths`)가 이 DB 폴더에 닿지 않으면 조회하지 않는다 — 예전에는
|
|
2690
|
+
* 범위와 상관없이 모든 행을 만들고 고쳤다.
|
|
2691
|
+
*
|
|
2692
|
+
* F25: 행 parent 로 data source 의 원본 database 를 검증한다(추가 API 0회). 신 모델에선
|
|
2693
|
+
* linked view 컨테이너도 data_sources 가 채워져 retrieve 만으론 원본과 구분되지 않아
|
|
2694
|
+
* 캐시에 오등록될 수 있는데, 그대로 두면 같은 행 집합을 여러 config 가 각자 자기 폴더로
|
|
2695
|
+
* 릴레이 재배치해 원격 무변경에도 매 pull 재작성이 쌓인다(실측 66건/pull, 행당 최대 4중).
|
|
2696
|
+
* 컨테이너로 판정되면 행은 원본에 양보하고 .base 만 원본 폴더 필터로 재지향해
|
|
2697
|
+
* "같은 데이터의 다른 뷰"라는 Notion 의미를 보존한다.
|
|
2698
|
+
*
|
|
2699
|
+
* DB 는 한 번만 받는다(`meta`) — 행 조회 · 스키마 · `.base` 제목이 이것을 같이 읽는다. 예전에는 같은 DB 를
|
|
2700
|
+
* 세 번 넘게 다시 받아 DB 하나에 요청 6~8회가 들었다(ADR-027).
|
|
2701
|
+
*/
|
|
2702
|
+
private queryRows;
|
|
2703
|
+
/**
|
|
2704
|
+
* 조회한 행 하나에 할 일 — 건너뜀 · 되살림 · 받음. 범위가 행 몇 개면 그 행만 받고 새 행은
|
|
2705
|
+
* 만들지 않는다(어느 경로에 쓸지 받기 전에는 모른다).
|
|
2706
|
+
*
|
|
2707
|
+
* 리모트가 지난번에 본 그대로면 건너뜀. 수정 시각이 같아도 같은 분 안의 편집일 수 있으면
|
|
2708
|
+
* («확인 안 됨», N-05) 받아서 견준다 — 같으면 파일을 쓰지 않는다. 단 두 가지 예외가 있다.
|
|
2709
|
+
* · 레코드 경로가 현재 DB 폴더 직속이 아니면(F24 동명 DB 폴더 분리 후 옛 공유 폴더 잔류)
|
|
2710
|
+
* 원격 무변경이어도 재처리해 현 폴더로 재배치한다.
|
|
2711
|
+
* · 로컬 파일이 사라졌으면 되살린다(R13). 이 확인이 없으면 pullDatabasePage 안의 복원
|
|
2712
|
+
* 판정(localExists → resolvePullConflict)에 영영 도달하지 못해 지운 행이 조용히 영구
|
|
2713
|
+
* 소실된다. 페이지 경로는 R0 으로 이미 마감한 계약인데 행 경로만 빠져 있었다 —
|
|
2714
|
+
* 오케스트레이터의 detectMissingLocalFiles 가 "행은 여기서 이미 같은 판정을 거친다"는
|
|
2715
|
+
* (틀린) 전제로 db-row 를 제외해 두었기에 양쪽 어디에도 복원 경로가 없는 상태였다.
|
|
2716
|
+
* deleteSync 가 켜져 있으면 되살리지 않는다 — 지운 것은 Notion 에서도 지우라는 뜻이고, sync 는
|
|
2717
|
+
* pull 을 먼저 돌려 되살리면 뒤이은 push 가 지울 것을 잃는다(detectMissingLocalFiles 와 같다).
|
|
2718
|
+
* · 예전 버전이 적은 날짜 모양이 남은 행은 그 줄만 새 모양으로 고쳐 쓴다({@link outdatedDateForm},
|
|
2719
|
+
* F-08).
|
|
2720
|
+
*/
|
|
2721
|
+
private rowAction;
|
|
2722
|
+
/**
|
|
2723
|
+
* 원격이 그대로인 행이지만 예전 버전이 적은 날짜 모양(`…Z` · `…+09:00` · `{start, end}`)이 남아 새
|
|
2724
|
+
* 모양으로 고쳐 쓸 행인가(F-08, {@link PropertyMapper.outdatedDateKeys}). 고쳐 쓰면 사본도 새 모양이
|
|
2725
|
+
* 되어 다음 pull 은 건너뛴다.
|
|
2726
|
+
*
|
|
2727
|
+
* 지난 동기화 뒤 로컬을 고친 행은 고르지 않는다 — 로컬 편집을 지키느라 쓰지 않으니 pull 마다 고르기만
|
|
2728
|
+
* 한다. 그 편집을 올린 뒤의 pull 이 고른다. 로컬 파일을 읽지 못하면 고르지 않는다.
|
|
2729
|
+
*/
|
|
2730
|
+
private outdatedDateForm;
|
|
2731
|
+
/**
|
|
2732
|
+
* 예전 버전이 적은 날짜 모양이 남은 행에서 그 키들만 새 모양으로 고쳐 쓴다(F-08) — 원격이 그대로라 본문 ·
|
|
2733
|
+
* 첨부는 다시 받지 않는다. 본문은 한 글자도 바꾸지 않고, 다른 키의 줄은 그대로 둔다(키 단위로 나눠 읽을
|
|
2734
|
+
* 수 없는 frontmatter 는 값만 지킨다 — {@link mergeRowFrontmatter}).
|
|
2735
|
+
*
|
|
2736
|
+
* 예전에는 행을 통째로 다시 받았다. 행마다 본문을 읽어 요청이 늘고, 다시 그릴 때마다 달라지는 본문(빈
|
|
2737
|
+
* 블록 · 첨부 이름)까지 바뀌었다 — 실볼트 사본에서 다시 받은 149행 중 99행.
|
|
2738
|
+
*
|
|
2739
|
+
* 고른 뒤 로컬을 고쳤으면 쓰지 않는다(`skipped`) — 이어지는 push 가 올린다.
|
|
2740
|
+
*/
|
|
2741
|
+
private rewriteDateForms;
|
|
2742
|
+
/** 이 DB 의 추적 행 가운데 이번 조회에 없던 것 — 지워졌는지는 아직 모른다. */
|
|
2743
|
+
private unlistedRows;
|
|
2744
|
+
/**
|
|
2745
|
+
* 조회에 없던 이 DB 의 추적 행 가운데 Notion 에서 정말 지워진 것만 볼트에서 지운다(deleteSync, S-12).
|
|
2746
|
+
*
|
|
2747
|
+
* 행은 DB 조회로만 보인다 — 전체 대조(페이지 순회)는 행을 보지 못해 행의 삭제는 여기서 가른다.
|
|
2748
|
+
* 조회에 없다는 것만으로 지우지 않고 행마다 원격에 묻는다({@link remotePresence}): 살아 있는 행은
|
|
2749
|
+
* 다른 DB 로 옮겨졌을 수 있다(그 DB 의 pull 이 옮겨 받는다). 묻지 못한 행은 이번에는 둔다 — 다음
|
|
2750
|
+
* pull 이 다시 묻는다.
|
|
2751
|
+
*
|
|
2752
|
+
* 조회 필터(`pullFilter`)가 있으면 가르지 않는다 — 조회에 없는 행이 필터 밖인지 지워진 것인지
|
|
2753
|
+
* 모르고, 필터 밖으로 나간 행을 매번 물으면 요청이 끝없이 는다.
|
|
2754
|
+
*
|
|
2755
|
+
* 올리지 않은 로컬 편집이 있는 행은 페이지와 같은 규칙을 따른다({@link applyRemoteDeletion}) —
|
|
2756
|
+
* 지우지 않고 충돌로 남기거나 파일을 둔다.
|
|
2757
|
+
*/
|
|
2758
|
+
private pullDeletedRows;
|
|
1410
2759
|
private pullDatabaseViews;
|
|
2760
|
+
/** 행들 가운데 한 번이라도 기간인 날짜 속성 → 그 짝 키. */
|
|
2761
|
+
private rangeEndsOf;
|
|
1411
2762
|
private generateBaseFile;
|
|
1412
2763
|
/**
|
|
1413
2764
|
* DB 제목이 바뀌면 `.base`/`.notion.json` 파일명(safeName)이 바뀌어 옛 이름의 산출물이
|
|
@@ -1430,107 +2781,58 @@ declare class DatabaseSyncer {
|
|
|
1430
2781
|
* (extractValue 는 URL 만 남긴다). 사용자가 넣은 진짜 외부 URL 은 그대로 둔다.
|
|
1431
2782
|
*/
|
|
1432
2783
|
private localizeFileProperties;
|
|
2784
|
+
/**
|
|
2785
|
+
* 행 하나를 pull 이 쓰는 모양 그대로 렌더한다 — 파일은 쓰지 않는다.
|
|
2786
|
+
*
|
|
2787
|
+
* pull(pullDatabasePage)과 충돌 비교 · 원격 선택 해소 · 원격 미리보기(오케스트레이터의
|
|
2788
|
+
* renderRemotePage)가 같은 렌더를 봐야 한다. 행을 페이지처럼 렌더하면 커버 · 아이콘 ·
|
|
2789
|
+
* 첨부 속성이 빠지고, 본문 첫머리의 옛 속성 블록 값이 실제 속성을 덮는다(S-02).
|
|
2790
|
+
*
|
|
2791
|
+
* 본문을 읽지 못하면 던진다 — 행은 실패로 남고 레코드가 그대로라 다음 pull 이 다시
|
|
2792
|
+
* 받는다. 예전에는 빈 본문으로 넘어가, 로컬이 그대로인 행은 본문이 지워진 채 «동기화
|
|
2793
|
+
* 완료» 로 기록됐다(S-10). 읽지 못한 것은 비어 있는 것이 아니다.
|
|
2794
|
+
*
|
|
2795
|
+
* @param options.downloadMedia false 면 첨부를 내려받지 않는다(표시 전용). 비교를 보려다
|
|
2796
|
+
* 볼트에 파일이 생기면 안 된다 — 그 대가로 아직 내려받지 않은 첨부는 원격 URL 로 남는다.
|
|
2797
|
+
* @param options.localPath 로컬 노트가 지금 있는 자리 — 코드 펜스 표기를 되살리는 근거(S-20).
|
|
2798
|
+
* 다른 폴더로 옮기기 전이면 옛 경로다. 기본은 `filePath`.
|
|
2799
|
+
*/
|
|
2800
|
+
renderRow(page: PageObjectResponse, filePath: string, options?: {
|
|
2801
|
+
downloadMedia?: boolean;
|
|
2802
|
+
localPath?: string;
|
|
2803
|
+
}): Promise<{
|
|
2804
|
+
content: string;
|
|
2805
|
+
title: string;
|
|
2806
|
+
properties: Record<string, unknown>;
|
|
2807
|
+
/** 받은 원격 본문의 지문(`remoteBodyFingerprint`). */
|
|
2808
|
+
bodyFingerprint: string;
|
|
2809
|
+
}>;
|
|
2810
|
+
/**
|
|
2811
|
+
* 행의 본문 밖이 지난 동기화 사본 그대로인가(N-05) — 제목과 보낼 수 있는 속성, 그리고
|
|
2812
|
+
* {@link renderRow} 로 받는 행이면 아이콘 · 커버. push 는 원격을 덮어써도 되는지, pull 은 받을
|
|
2813
|
+
* 것이 있는지를 이 규칙으로 가른다. 읽기 전용 속성(수식 · 롤업 · 수정 시각)은 보지 않는다 —
|
|
2814
|
+
* 로컬에서 고칠 수 없고 push 가 보내지 않는다.
|
|
2815
|
+
*
|
|
2816
|
+
* @param mapper 그 행의 DB 스키마를 읽은 매퍼.
|
|
2817
|
+
* @param base 지난 동기화 시점의 frontmatter.
|
|
2818
|
+
* @param path 지난 동기화 시점의 경로 — 파일 이름을 따르던 제목을 가른다.
|
|
2819
|
+
* @param decorated 아이콘 · 커버도 견주나 — {@link renderRow} 로 받는 행만 그것을 노트에 적는다.
|
|
2820
|
+
*/
|
|
2821
|
+
rowOutsideBodyUnchanged(mapper: PropertyMapper, page: PageObjectResponse, base: Readonly<Record<string, unknown>>, path: string, decorated: boolean): boolean;
|
|
2822
|
+
/**
|
|
2823
|
+
* 원격 행의 아이콘 · 커버가 `frontmatter` 에 적힌 그대로인가 — {@link renderRow} 가 적는 모양으로
|
|
2824
|
+
* 견준다(N-05). Notion 에 올린 파일은 읽을 때마다 주소가 바뀌고(서명) 커버는 볼트로 내려받아
|
|
2825
|
+
* 적으므로, 있는지만 견준다.
|
|
2826
|
+
*/
|
|
2827
|
+
private decorationsUnchanged;
|
|
2828
|
+
/** 레코드가 지난번에 본 원격 행과 지금 행을 견준다 — 이번 동기화의 기준으로(N-05). */
|
|
2829
|
+
private remoteVerdict;
|
|
2830
|
+
/**
|
|
2831
|
+
* 행 경로 후보의 주인 — 상태 DB 의 레코드, 없으면 볼트에 이미 있는 파일(주인 모름). 추적하지 않는 로컬 파일
|
|
2832
|
+
* (아직 push 하지 않은 노트)을 새 행이 덮어쓰면 그 글이 사라졌다. 일반 페이지 받기(`claimFilePath`)처럼 피한다.
|
|
2833
|
+
*/
|
|
2834
|
+
private rowPathOwners;
|
|
1433
2835
|
private pullDatabasePage;
|
|
1434
|
-
private pushDatabase;
|
|
1435
|
-
/**
|
|
1436
|
-
* rename 감지를 위한 고아 레코드 인덱스(내용 해시 → 레코드)를 만든다.
|
|
1437
|
-
* 고아 = 해당 DB 소속 db-row 레코드 중 로컬 파일이 더 이상 존재하지 않는(livePaths 에 없는) 것.
|
|
1438
|
-
* 같은 해시가 여러 고아에 걸리면 첫 항목을 유지한다(희박한 케이스).
|
|
1439
|
-
*/
|
|
1440
|
-
private buildOrphanHashIndex;
|
|
1441
|
-
}
|
|
1442
|
-
|
|
1443
|
-
declare class BlockConverter {
|
|
1444
|
-
private n2m;
|
|
1445
|
-
private client;
|
|
1446
|
-
initNotionToMd(client: Client): void;
|
|
1447
|
-
private registerCustomTransformers;
|
|
1448
|
-
/**
|
|
1449
|
-
* `numbered_list_item` 을 `1.` 마크다운으로 내보내 **번호 목록 타입을 보존**한다(I2 블록 라운드트립).
|
|
1450
|
-
*
|
|
1451
|
-
* notion-to-md 기본 렌더는 `md.bullet(text, block.numbered_list_item.number)` 를 쓰는데
|
|
1452
|
-
* Notion API 는 list item 에 `.number` 를 채워주지 않아 `undefined` → 불릿(`- `)으로 **강등**된다.
|
|
1453
|
-
* 그러면 블록 폴백 경로(notionBlocksToMarkdown)로 pull 한 뒤 다시 push 하면 numbered_list_item 이
|
|
1454
|
-
* bulleted_list_item 으로 **타입 유실**된다(기본 markdown-api 경로는 영향 없음).
|
|
1455
|
-
*
|
|
1456
|
-
* 항상 `1.` 로 내보내면 마크다운이 시각적으로 자동 재번호하므로 표시는 동일하고, martian 이 다시
|
|
1457
|
-
* numbered_list_item 으로 파싱해 타입이 보존된다. 커스텀 트랜스포머 등록은 notion-to-md 의 기본
|
|
1458
|
-
* 자식 처리를 끄므로(라이브러리 설계), 자식은 toggle/callout 과 동일하게 직접 fetch·들여쓰기한다.
|
|
1459
|
-
*/
|
|
1460
|
-
private registerNumberedListTransformer;
|
|
1461
|
-
private registerToggleTransformer;
|
|
1462
|
-
private registerToggleHeadingTransformers;
|
|
1463
|
-
private registerColumnTransformer;
|
|
1464
|
-
private registerSyncedBlockTransformer;
|
|
1465
|
-
private registerBookmarkTransformer;
|
|
1466
|
-
private registerVideoTransformer;
|
|
1467
|
-
private registerAudioTransformer;
|
|
1468
|
-
private registerFileTransformer;
|
|
1469
|
-
private registerChildPageTransformer;
|
|
1470
|
-
private registerChildDatabaseTransformer;
|
|
1471
|
-
private registerLinkToPageTransformer;
|
|
1472
|
-
private registerTocTransformer;
|
|
1473
|
-
private registerBreadcrumbTransformer;
|
|
1474
|
-
private registerLinkPreviewTransformer;
|
|
1475
|
-
private registerPdfTransformer;
|
|
1476
|
-
private registerEmbedTransformer;
|
|
1477
|
-
private registerCalloutTransformer;
|
|
1478
|
-
markdownToNotionBlocks(markdown: string): unknown[];
|
|
1479
|
-
private toggleContents;
|
|
1480
|
-
private columnContents;
|
|
1481
|
-
private toggleCounter;
|
|
1482
|
-
private columnCounter;
|
|
1483
|
-
private preProcessMarkdown;
|
|
1484
|
-
/**
|
|
1485
|
-
* compact underline/color 보존마커를 평문으로 강등 — block(martian) 폴백 경로 전용.
|
|
1486
|
-
*
|
|
1487
|
-
* 무손실 underline/color 복원은 기본 경로인 Markdown API({@link obsidianToNotionEnhanced})가
|
|
1488
|
-
* 전담한다. martian 은 이 서식을 표현할 수 없으므로, Markdown API 가 실패해 이 폴백을 탈 때는
|
|
1489
|
-
* 마커 텍스트가 본문에 리터럴로 새지 않도록 텍스트만 남기고 마커를 제거한다(서식 degrade).
|
|
1490
|
-
*/
|
|
1491
|
-
private stripInlineAnnotationMarkers;
|
|
1492
|
-
private replaceDividers;
|
|
1493
|
-
private extractToggleBlocks;
|
|
1494
|
-
private extractColumnBlocks;
|
|
1495
|
-
private postProcessBlocks;
|
|
1496
|
-
private convertTogglePlaceholder;
|
|
1497
|
-
private convertColumnPlaceholder;
|
|
1498
|
-
private convertDividerPlaceholder;
|
|
1499
|
-
/**
|
|
1500
|
-
* 단일 라인 보존 마커(목차·breadcrumb)를 원래 블록으로 복원한다.
|
|
1501
|
-
* pull 시 마크다운 표현이 없는 블록을 `%%im-nobsidian:toc%%` /
|
|
1502
|
-
* `%%im-nobsidian:breadcrumb%%` 마커 문단으로 남겨두므로, push 시 이 문단을
|
|
1503
|
-
* 감지해 table_of_contents / breadcrumb 블록으로 되돌린다. 그러지 않으면
|
|
1504
|
-
* 마커 텍스트가 일반 문단으로 Notion 에 기록되는 잠재 손실이 된다.
|
|
1505
|
-
*/
|
|
1506
|
-
private convertSingleLineMarker;
|
|
1507
|
-
private convertImageToVideoOrEmbed;
|
|
1508
|
-
/**
|
|
1509
|
-
* `@tryfabric/martian` 의 blockquote 표현을 **평탄화**한다(I2/I3 무손실 push).
|
|
1510
|
-
*
|
|
1511
|
-
* martian 의 parseBlockquote 는 `notion.blockquote([], children)` 로 본문을 quote 의
|
|
1512
|
-
* **자식 paragraph** 로 내려보내고 quote.rich_text 를 비운다(라이브러리 설계). 그 결과
|
|
1513
|
-
* `> 인용문` 을 push 하면 Notion 에 "빈 인용 줄 + 들여쓴 자식 문단"이 만들어지고, 다시
|
|
1514
|
-
* pull 하면 `> `(빈 줄) 아래에 자식 문단이 매달려 **원문과 구조가 갈라진다**(라운드트립 손실).
|
|
1515
|
-
* 또한 quote.rich_text[0] 이 항상 비어 callout 이모지 감지({@link convertQuoteToCallout})도
|
|
1516
|
-
* 무력화됐다.
|
|
1517
|
-
*
|
|
1518
|
-
* 선두의 연속된 paragraph 자식을 quote.rich_text 로 끌어올리고(문단 사이는 줄바꿈 rich_text
|
|
1519
|
-
* 로 연결 — Notion 네이티브 멀티라인 인용 표현), paragraph 가 아닌 자식(중첩 리스트 등)은
|
|
1520
|
-
* children 으로 남겨 구조를 보존한다. 이미 rich_text 에 본문이 있거나 끌어올릴 paragraph 가
|
|
1521
|
-
* 없으면 원본을 그대로 반환한다(안전 no-op).
|
|
1522
|
-
*/
|
|
1523
|
-
private flattenMartianBlockquote;
|
|
1524
|
-
private convertQuoteToCallout;
|
|
1525
|
-
notionBlocksToMarkdown(pageId: string): Promise<string>;
|
|
1526
|
-
/**
|
|
1527
|
-
* **in-memory** 블록 배열을 마크다운으로 변환한다(페이지 fetch 없음). notion-to-md 의
|
|
1528
|
-
* blocksToMarkdown 을 직접 호출하므로, `has_children:false` 인 평면 블록은 Notion 클라이언트를
|
|
1529
|
-
* 전혀 건드리지 않는다 → 고정 픽스처로 **오프라인 결정론** 블록 라운드트립(I2)을 잠글 수 있는
|
|
1530
|
-
* 정식 테스트 시임. children 이 있는 블록(table 행·callout 자식 등)은 클라이언트를 호출하므로
|
|
1531
|
-
* 호출 측이 평면 블록만 넣어야 오프라인이 보장된다.
|
|
1532
|
-
*/
|
|
1533
|
-
notionBlockArrayToMarkdown(blocks: Array<Record<string, unknown>>): Promise<string>;
|
|
1534
2836
|
}
|
|
1535
2837
|
|
|
1536
2838
|
declare class FrontmatterExtractor implements Processor {
|
|
@@ -1560,6 +2862,28 @@ declare class InlineDBParser implements Processor {
|
|
|
1560
2862
|
process(input: ProcessorInput): ProcessorOutput;
|
|
1561
2863
|
}
|
|
1562
2864
|
|
|
2865
|
+
/**
|
|
2866
|
+
* 블록 수식(`$$…$$`)의 두 `$$` 를 저마다 제 줄에 세운다 — Notion 은 `$$` 가 줄에 혼자 있을 때만 블록
|
|
2867
|
+
* 수식으로 읽는다. 한 줄로 쓴 `$$ x^2 $$` · 식이 `$$` 줄에 붙은 `$$ a` … `b $$` 는 빈 인라인 수식과
|
|
2868
|
+
* 글이 된다(실측).
|
|
2869
|
+
*
|
|
2870
|
+
* 콜아웃 · 목록 안의 수식은 새 줄에도 줄머리(`> ` · 들여쓰기)를 단다. 예전에는 닫는 `$$` 를 줄머리
|
|
2871
|
+
* 없이 세워 콜아웃 밖으로 꺼냈고, Notion 은 그 `$$` 부터 뒤 문단을 수식으로 삼켰다(실측).
|
|
2872
|
+
* 글 사이의 `$$x$$` 는 둔다 — Notion 은 인라인 수식으로 읽는데, 줄을 가르면 빈 수식과 글이 된다.
|
|
2873
|
+
*
|
|
2874
|
+
* 코드 펜스 · 인라인 코드 안의 `$` 는 코드다. 셸의 `$$`(PID) · `$HOME` 을 수식으로 보면 Notion 의
|
|
2875
|
+
* 코드가 바뀌고, 코드 속 `$$` 가 코드 밖 수식의 `$$` 와 짝지어져 그 사이 본문까지 옮겨졌다(S-21).
|
|
2876
|
+
*
|
|
2877
|
+
* 블록 수식으로 서지 못하는 `$$` 는 Notion 이 빈 인라인 수식으로 읽는다(2026-10-21 실측) — 짝이 없는 `$$` 는
|
|
2878
|
+
* Obsidian 이 글로 그리는데 Notion 에는 빈 수식 상자가 생겼다. 그래서 줄 안에서 짝이 없는 `$$` 는 `\$\$`(두 쪽 모두
|
|
2879
|
+
* 글 `$$`)로, 글과 한 줄인 `$$ x $$` 는 안쪽 공백을 떼어 `$$x$$`(Notion 의 인라인 수식)로 보낸다
|
|
2880
|
+
* ({@link settleLineDollars}). 제 줄의 `$$` 는 블록 수식의 경계라 둔다.
|
|
2881
|
+
*
|
|
2882
|
+
* 인라인 수식(`$…$`)의 안쪽은 건드리지 않는다. 예전에는 안쪽 공백을 잘라 `$ x $` 가 수식이 되고
|
|
2883
|
+
* `$5 and $10` 이 `$5 and$10` 으로 바뀌었다. Obsidian 과 Notion 은 여는 `$` 뒤 · 닫는 `$` 앞이
|
|
2884
|
+
* 공백이거나 닫는 `$` 뒤가 숫자면 수식으로 읽지 않는다(Obsidian 1.13.7 의 토크나이저 · Notion 실측).
|
|
2885
|
+
* 다만 Notion 은 글자에 붙은 수식(`w$^9$가`)도 글로 읽는다 — 그런 수식만 `` $`식`$ `` 로 보낸다({@link quoteGluedMath}).
|
|
2886
|
+
*/
|
|
1563
2887
|
declare class MathNormalizer implements Processor {
|
|
1564
2888
|
readonly name = "MathNormalizer";
|
|
1565
2889
|
readonly order = 50;
|
|
@@ -1570,6 +2894,10 @@ declare class EmbedResolver implements Processor {
|
|
|
1570
2894
|
readonly name = "EmbedResolver";
|
|
1571
2895
|
readonly order = 60;
|
|
1572
2896
|
process(input: ProcessorInput): ProcessorOutput;
|
|
2897
|
+
/** `![[대상]]` — 첨부는 자리표시자로(push), 노트 임베드는 원문 그대로. */
|
|
2898
|
+
private resolveEmbeds;
|
|
2899
|
+
/** `` — 동영상 URL 은 임베드 마커로, 외부 이미지는 목록에 모은다. */
|
|
2900
|
+
private resolveMarkdownImages;
|
|
1573
2901
|
}
|
|
1574
2902
|
|
|
1575
2903
|
declare class PreserveMarkerCollector implements Processor {
|
|
@@ -1584,6 +2912,20 @@ declare class UnsupportedBlockStripper implements Processor {
|
|
|
1584
2912
|
process(input: ProcessorInput): ProcessorOutput;
|
|
1585
2913
|
}
|
|
1586
2914
|
|
|
2915
|
+
/**
|
|
2916
|
+
* 보내기 직전에 코드 밖의 이어진 빈 줄을 한 줄로 줄인다 — Notion 마크다운 가져오기는 남는 빈 줄마다 빈
|
|
2917
|
+
* 블록을 만든다(실측: 주석 한 줄을 지운 자리의 빈 줄 셋이 빈 블록 셋). 받으면 그 빈 블록이 `<br>` 줄이 된다.
|
|
2918
|
+
*
|
|
2919
|
+
* 노트의 빈 줄과, 앞 전처리가 지운 주석 · 지원하지 않는 블록이 남긴 빈 줄을 함께 줄인다. 그래서 내용을
|
|
2920
|
+
* 지우거나 끼우는 전처리 뒤, 보존 마커 수집(70) 앞에 돈다. 예전에는 지원하지 않는 블록 제거(5)가 줄였다 —
|
|
2921
|
+
* 주석 제거(11)보다 먼저라 주석 자리가 빈 블록이 됐고, 코드 속 빈 줄까지 줄였다(S-27).
|
|
2922
|
+
*/
|
|
2923
|
+
declare class BlankLineCollapser implements Processor {
|
|
2924
|
+
readonly name = "BlankLineCollapser";
|
|
2925
|
+
readonly order = 65;
|
|
2926
|
+
process(input: ProcessorInput): ProcessorOutput;
|
|
2927
|
+
}
|
|
2928
|
+
|
|
1587
2929
|
declare class PropertiesTableInjector implements Processor {
|
|
1588
2930
|
readonly name = "PropertiesTableInjector";
|
|
1589
2931
|
readonly order = 15;
|
|
@@ -1591,7 +2933,7 @@ declare class PropertiesTableInjector implements Processor {
|
|
|
1591
2933
|
}
|
|
1592
2934
|
|
|
1593
2935
|
/**
|
|
1594
|
-
* 인라인 annotation(underline·color) 무손실 보존기 — push 전처리(order
|
|
2936
|
+
* 인라인 annotation(underline·color) 무손실 보존기 — push 전처리(order 13).
|
|
1595
2937
|
*
|
|
1596
2938
|
* 과거에는 underline/color 표기를 **제거**(strip)했으나, 그러면 Notion 으로 push 할 때
|
|
1597
2939
|
* 서식이 영구 소실됐다(I3 손실점). Notion 의 Enhanced Markdown 은 underline/color 를
|
|
@@ -1610,7 +2952,7 @@ declare class PropertiesTableInjector implements Processor {
|
|
|
1610
2952
|
*/
|
|
1611
2953
|
declare class InlineAnnotationPreserver implements Processor {
|
|
1612
2954
|
readonly name = "InlineAnnotationPreserver";
|
|
1613
|
-
readonly order =
|
|
2955
|
+
readonly order = 13;
|
|
1614
2956
|
process(input: ProcessorInput): ProcessorOutput;
|
|
1615
2957
|
}
|
|
1616
2958
|
|
|
@@ -1676,6 +3018,13 @@ declare class PropertiesTableRestorer implements Processor {
|
|
|
1676
3018
|
readonly name = "PropertiesTableRestorer";
|
|
1677
3019
|
readonly order = 5;
|
|
1678
3020
|
process(input: ProcessorInput): ProcessorOutput;
|
|
3021
|
+
/**
|
|
3022
|
+
* DB 행 본문 첫머리의 속성 블록은 예전 push 가 행을 페이지로 밀며 잘못 끼운 것이다(S-02).
|
|
3023
|
+
* 행의 속성은 Notion 속성이 정본이다 — 블록 값으로 덮으면 Notion 에서 고친 속성이 옛 값으로
|
|
3024
|
+
* 보인다. 그래서 값은 버리고 블록만 걷어 낸다. Notion 쪽 블록은 이 행의 본문을 다음에
|
|
3025
|
+
* push 할 때 사라진다.
|
|
3026
|
+
*/
|
|
3027
|
+
private dropFromRow;
|
|
1679
3028
|
private restoreFromYaml;
|
|
1680
3029
|
private restoreFromTable;
|
|
1681
3030
|
}
|
|
@@ -1766,13 +3115,19 @@ declare class FileWatcher {
|
|
|
1766
3115
|
stop(): Promise<void>;
|
|
1767
3116
|
}
|
|
1768
3117
|
|
|
3118
|
+
/** 한 번의 sync 가 무엇을 보나 — 바뀐 파일만(파일 변경) · 볼트 전체(주기 sync). */
|
|
3119
|
+
type WatchSyncScope = "changes" | "full";
|
|
1769
3120
|
interface WatchSyncOptions {
|
|
1770
3121
|
readonly debounceMs?: number;
|
|
1771
|
-
readonly onSyncStart?: () => void;
|
|
1772
|
-
readonly onSyncComplete?: (result: SyncResult) => void;
|
|
1773
|
-
|
|
3122
|
+
readonly onSyncStart?: (scope: WatchSyncScope) => void;
|
|
3123
|
+
readonly onSyncComplete?: (result: SyncResult, scope: WatchSyncScope) => void;
|
|
3124
|
+
/** 감시를 멈춰 도중에 취소한 sync — `result` 는 그때까지 처리한 것이다. */
|
|
3125
|
+
readonly onSyncCancelled?: (result: SyncResult, scope: WatchSyncScope) => void;
|
|
3126
|
+
readonly onSyncError?: (error: Error, scope: WatchSyncScope) => void;
|
|
1774
3127
|
readonly onFileChange?: (event: WatchEvent, path: string) => void;
|
|
1775
3128
|
}
|
|
3129
|
+
/** 마지막 변경 뒤 동기화까지 기다리는 시간 — CLI `watch` 와 플러그인 자동 동기화가 함께 쓴다. */
|
|
3130
|
+
declare const DEFAULT_WATCH_DEBOUNCE_MS = 2000;
|
|
1776
3131
|
declare class WatchSyncService {
|
|
1777
3132
|
private readonly rootPath;
|
|
1778
3133
|
private readonly orchestrator;
|
|
@@ -1783,10 +3138,26 @@ declare class WatchSyncService {
|
|
|
1783
3138
|
private pendingChanges;
|
|
1784
3139
|
private syncing;
|
|
1785
3140
|
private syncQueued;
|
|
3141
|
+
private fullSyncQueued;
|
|
1786
3142
|
private running;
|
|
3143
|
+
private inFlight;
|
|
3144
|
+
private stopping;
|
|
1787
3145
|
constructor(rootPath: string, orchestrator: SyncOrchestrator, options?: WatchSyncOptions);
|
|
1788
3146
|
start(): void;
|
|
3147
|
+
/**
|
|
3148
|
+
* 감시를 멈춘다. 도는 sync 는 취소하고 끝나기를 기다린다 — 예전에는 기다리지 않아, 부른 쪽이
|
|
3149
|
+
* sync 도중에 상태 DB 를 닫았다(S-09).
|
|
3150
|
+
*/
|
|
1789
3151
|
stop(): Promise<void>;
|
|
3152
|
+
private halt;
|
|
3153
|
+
/**
|
|
3154
|
+
* 볼트 전체 sync 를 요청한다 — CLI `watch --interval` 의 주기 sync(S-09).
|
|
3155
|
+
*
|
|
3156
|
+
* 파일 변경 sync 와 같은 줄에 선다. 예전에는 주기 sync 가 이 서비스 밖에서 오케스트레이터를
|
|
3157
|
+
* 불러, 도는 중에 들어온 파일 변경 sync · 다음 주기 sync 와 겹쳤다. 도는 sync 가 있으면 끝난 뒤
|
|
3158
|
+
* 한 번만 돈다 — 요청이 쌓여도 한 번이다.
|
|
3159
|
+
*/
|
|
3160
|
+
requestFullSync(): void;
|
|
1790
3161
|
isRunning(): boolean;
|
|
1791
3162
|
isSyncing(): boolean;
|
|
1792
3163
|
getPendingCount(): number;
|
|
@@ -1794,6 +3165,7 @@ declare class WatchSyncService {
|
|
|
1794
3165
|
private scheduleSync;
|
|
1795
3166
|
private clearDebounce;
|
|
1796
3167
|
private executeSync;
|
|
3168
|
+
private runBatch;
|
|
1797
3169
|
}
|
|
1798
3170
|
|
|
1799
3171
|
interface DBEntry {
|
|
@@ -1841,6 +3213,7 @@ interface PropertyOption {
|
|
|
1841
3213
|
declare class ViewDataProvider {
|
|
1842
3214
|
private readonly vaultFs;
|
|
1843
3215
|
constructor(vaultFs: VaultFS);
|
|
3216
|
+
/** 파일이 없으면 빈 객체. 있는데 읽지 못하면 던진다 — 「보기 설정이 없다」 와 「읽지 못했다」 는 다른 말이다. */
|
|
1844
3217
|
loadAllViewConfigs(): Promise<Record<string, DatabaseViewsConfig>>;
|
|
1845
3218
|
getViewConfigs(databaseId: string): Promise<DatabaseViewsConfig | null>;
|
|
1846
3219
|
collectEntries(folderPath: string): Promise<DBEntry[]>;
|
|
@@ -1858,37 +3231,45 @@ declare class EntryEditor {
|
|
|
1858
3231
|
updateDate(entryPath: string, dateProperty: string, newDate: string): Promise<void>;
|
|
1859
3232
|
}
|
|
1860
3233
|
|
|
1861
|
-
interface BasePropertySchema {
|
|
1862
|
-
readonly id: string;
|
|
1863
|
-
readonly type: string;
|
|
1864
|
-
readonly options?: Array<{
|
|
1865
|
-
name: string;
|
|
1866
|
-
color?: string;
|
|
1867
|
-
}>;
|
|
1868
|
-
readonly groups?: Array<{
|
|
1869
|
-
name: string;
|
|
1870
|
-
color?: string;
|
|
1871
|
-
optionIds?: string[];
|
|
1872
|
-
}>;
|
|
1873
|
-
}
|
|
1874
3234
|
interface BaseFileOptions {
|
|
1875
3235
|
readonly databaseId: string;
|
|
1876
3236
|
readonly databaseName: string;
|
|
1877
3237
|
readonly schema: Record<string, BasePropertySchema>;
|
|
1878
3238
|
readonly viewsConfig: DatabaseViewsConfig;
|
|
1879
3239
|
readonly folderPath: string;
|
|
3240
|
+
/**
|
|
3241
|
+
* 날짜 속성 이름 → 기간의 끝을 적는 짝 키(`<이름>_end`). 행 노트는 기간을 두 키로 적으므로 표에 그 속성이
|
|
3242
|
+
* 보이면 끝 열도 바로 뒤에 둔다(B13). 받은 행 가운데 기간이 있는 속성만 — 기간을 쓰지 않는 날짜 속성에
|
|
3243
|
+
* 늘 빈 열을 두지 않는다.
|
|
3244
|
+
*/
|
|
3245
|
+
readonly rangeEnds?: ReadonlyMap<string, string>;
|
|
1880
3246
|
}
|
|
1881
3247
|
type BasesViewType = "table" | "cards" | "list";
|
|
3248
|
+
/** Notion 뷰 하나를 Bases 로 옮길 때의 판정. */
|
|
3249
|
+
interface BasesViewMapping {
|
|
3250
|
+
/** Bases 뷰 타입. `null` = 대응이 없어 `.base` 에 실을 수 없음. */
|
|
3251
|
+
readonly type: BasesViewType | null;
|
|
3252
|
+
/**
|
|
3253
|
+
* 값이 있으면 **정확한 대응이 아니라 격하**라는 뜻(사람용 사유).
|
|
3254
|
+
* 뷰 자체는 살아남지만 원래 뷰의 표현 일부(달력 격자·타임라인 축)는 재현되지 않는다.
|
|
3255
|
+
*/
|
|
3256
|
+
readonly degradeReason?: string;
|
|
3257
|
+
}
|
|
1882
3258
|
/**
|
|
1883
|
-
* Notion 뷰 타입 → Obsidian Bases 뷰
|
|
3259
|
+
* Notion 뷰 타입 → Obsidian Bases 뷰 매핑(SSOT).
|
|
1884
3260
|
*
|
|
1885
|
-
* `null` = Bases 에 대응 뷰가 없어 `.base` 로 표현 불가. 이 경우 해당 뷰는 `.base` 에서
|
|
1886
|
-
* 누락되므로, 사이드카(`<db>.notion.json`)가 원본 뷰 설정을 무손실 보존한다.
|
|
1887
3261
|
* 뷰는 현재 pull-authoritative — Notion API 가 2026-03-19 뷰 생성/수정/삭제를 열었지만
|
|
1888
3262
|
* 본 프로젝트는 아직 뷰 write-back 을 구현하지 않았다(로드맵 항목). push 로 되돌릴 수
|
|
1889
3263
|
* 없는 동안 "조용한 유실 금지"는 곧 "보존 + 정직한 degrade 리포트"를 의미한다.
|
|
3264
|
+
*
|
|
3265
|
+
* 달력·타임라인은 예전에 `null` 이었다 — 즉 `.base` 에서 **통째로 사라졌다**. 사이드카에는
|
|
3266
|
+
* 남지만 Obsidian 을 여는 사람 눈에는 그 뷰가 애초에 없었던 것처럼 보인다. 격자·축은
|
|
3267
|
+
* 못 살려도 "날짜 순으로 늘어놓은 목록"이라는 본래 의도는 표로 남길 수 있으므로,
|
|
3268
|
+
* 버리는 대신 격하해 살리고 사유를 사이드카에 적는다.
|
|
1890
3269
|
*/
|
|
1891
|
-
declare const NOTION_TO_BASES_VIEW: Record<string,
|
|
3270
|
+
declare const NOTION_TO_BASES_VIEW: Record<string, BasesViewMapping>;
|
|
3271
|
+
/** Notion 뷰 타입의 Bases 매핑. 미상 타입은 표현 불가로 본다. */
|
|
3272
|
+
declare function basesViewMappingOf(notionType: string): BasesViewMapping;
|
|
1892
3273
|
/** Notion 뷰 타입의 Bases 대응 타입을 돌려준다. 미지원/미상 타입은 `null`. */
|
|
1893
3274
|
declare function basesViewTypeOf(notionType: string): BasesViewType | null;
|
|
1894
3275
|
declare class BaseFileGenerator {
|
|
@@ -1898,6 +3279,19 @@ declare class BaseFileGenerator {
|
|
|
1898
3279
|
private writeFormulas;
|
|
1899
3280
|
private writePropertyDisplayNames;
|
|
1900
3281
|
private writeViews;
|
|
3282
|
+
/** `filters:` 키와 그 값을 쓴다. 단일 식이면 한 줄, 그룹이면 블록. */
|
|
3283
|
+
private writeFilterValue;
|
|
3284
|
+
/** 필터 노드를 시퀀스 항목(`- …`)으로 쓴다. 그룹이면 재귀적으로 중첩한다. */
|
|
3285
|
+
private writeFilterItem;
|
|
3286
|
+
/**
|
|
3287
|
+
* 필터 식을 YAML 스칼라로 쓴다.
|
|
3288
|
+
*
|
|
3289
|
+
* 식은 대개 `note["상태"] == "진행 중"` 처럼 큰따옴표를 품는다. 평문 스칼라는 큰따옴표를
|
|
3290
|
+
* 안에 담을 수 있으므로(선두만 아니면 된다) 기본은 평문으로 두어 기존 `file.ext == "md"`
|
|
3291
|
+
* 라인과 모양을 맞춘다. 다만 `: `·` #`·선두 지시문자는 평문에서 의미가 달라지므로
|
|
3292
|
+
* 그때만 작은따옴표로 감싼다(속성명에 콜론이나 해시가 들어갈 수 있다).
|
|
3293
|
+
*/
|
|
3294
|
+
private yamlExpr;
|
|
1901
3295
|
/**
|
|
1902
3296
|
* 뷰 이름을 Bases 요구사항대로 유일화한다(in-place). 동명 충돌 시 ' 2',' 3'… 를 붙이되
|
|
1903
3297
|
* 이미 존재하는 이름과 다시 충돌하지 않을 때까지 증가시킨다.
|
|
@@ -1912,11 +3306,72 @@ declare class BaseFileGenerator {
|
|
|
1912
3306
|
*/
|
|
1913
3307
|
private propRef;
|
|
1914
3308
|
private resolvePropertyName;
|
|
1915
|
-
private decodeId;
|
|
1916
3309
|
private yamlKey;
|
|
1917
3310
|
private yamlValue;
|
|
1918
3311
|
}
|
|
1919
3312
|
|
|
3313
|
+
/**
|
|
3314
|
+
* 속성 id → 프론트매터 키 해소.
|
|
3315
|
+
*
|
|
3316
|
+
* Notion `views.retrieve` 는 RAW 속성 id(예: `[jiM`)를 주지만 `databases.retrieve` 스키마는
|
|
3317
|
+
* URL-인코딩된 id(예: `%5BjiM`)를 준다. 디코드 후 비교하지 않으면 title 외 모든 속성
|
|
3318
|
+
* (cover/order/sort/groupBy/filter)이 매칭에 실패해 조용히 사라진다.
|
|
3319
|
+
*
|
|
3320
|
+
* `.base` 생성기와 사이드카 생성기가 같은 규칙을 써야 하므로 여기 한 곳에 둔다.
|
|
3321
|
+
*/
|
|
3322
|
+
declare function resolvePropertyName(propertyId: string, schema: Record<string, BasePropertySchema>): string | undefined;
|
|
3323
|
+
|
|
3324
|
+
/**
|
|
3325
|
+
* Notion 뷰 필터식 → Obsidian Bases 필터식 번역기.
|
|
3326
|
+
*
|
|
3327
|
+
* ## 왜 필요한가
|
|
3328
|
+
*
|
|
3329
|
+
* Notion 뷰의 절반 가까이는 필터가 걸려 있다. 그런데 `.base` 는 폴더 필터만 내보냈고,
|
|
3330
|
+
* 사이드카는 "Notion 필터식은 .base 로 표현되지 않음"이라 적어 두고 끝냈다. 그래서
|
|
3331
|
+
* '시작전, 진행중' 이라는 이름의 뷰가 Obsidian 에서는 **완료된 행까지 전부** 보여 줬다 —
|
|
3332
|
+
* 이름과 내용이 어긋나는, 사용자가 오해하기 딱 좋은 상태였다. Bases 는 뷰마다 `filters:`
|
|
3333
|
+
* 를 지원하므로(공식 문서 `views[].filters`) 표현 가능한 조건은 실제로 옮길 수 있다.
|
|
3334
|
+
*
|
|
3335
|
+
* ## 부분 번역의 안전 규칙 — 이 파일의 핵심
|
|
3336
|
+
*
|
|
3337
|
+
* Notion 필터 문법은 Bases 보다 넓다(rollup any/every/none, 상대 날짜, formula 등).
|
|
3338
|
+
* 못 옮기는 리프를 만났을 때 **어느 방향으로 틀릴지**는 논리 위치에 따라 다르다:
|
|
3339
|
+
*
|
|
3340
|
+
* - `and` 안에서 리프 하나를 버리면 조건이 느슨해진다 → 결과는 **상위집합**.
|
|
3341
|
+
* Notion 보다 행이 더 보인다. 눈에 보이는 과다 표시이고, 데이터는 잃지 않는다.
|
|
3342
|
+
* - `or` 안에서 가지 하나를 버리면 조건이 조여진다 → 결과는 **부분집합**.
|
|
3343
|
+
* Notion 에 있던 행이 Obsidian 에서 **조용히 사라진다**. 이건 유실이다.
|
|
3344
|
+
* - `not` 은 안쪽을 버리면 부정의 의미 자체가 뒤집힌다.
|
|
3345
|
+
*
|
|
3346
|
+
* 그래서 `and` 는 번역 가능한 리프만 남기고, `or`/`not` 은 하나라도 못 옮기면 그룹
|
|
3347
|
+
* 전체를 포기한다. 포기한 것은 전부 {@link TranslationResult.untranslated} 로 보고돼
|
|
3348
|
+
* 사이드카 degrade 리포트에 실린다 — 조용히 넘어가는 경로는 없다.
|
|
3349
|
+
*/
|
|
3350
|
+
/** Bases 필터 노드 — 문자열 표현식이거나 and/or/not 그룹. */
|
|
3351
|
+
type BasesFilterNode = string | {
|
|
3352
|
+
readonly and: BasesFilterNode[];
|
|
3353
|
+
} | {
|
|
3354
|
+
readonly or: BasesFilterNode[];
|
|
3355
|
+
} | {
|
|
3356
|
+
readonly not: BasesFilterNode[];
|
|
3357
|
+
};
|
|
3358
|
+
interface TranslationResult {
|
|
3359
|
+
/** 번역된 필터. 옮길 수 있는 조건이 하나도 없으면 null. */
|
|
3360
|
+
readonly node: BasesFilterNode | null;
|
|
3361
|
+
/** 옮기지 못한 조건의 사람용 설명(중복 제거·정렬됨). */
|
|
3362
|
+
readonly untranslated: string[];
|
|
3363
|
+
}
|
|
3364
|
+
/** 속성 id/이름 → 프론트매터 키. 스키마에서 못 찾으면 undefined. */
|
|
3365
|
+
type PropertyNameResolver = (idOrName: string) => string | undefined;
|
|
3366
|
+
/**
|
|
3367
|
+
* Notion 필터 객체를 Bases 필터 노드로 옮긴다.
|
|
3368
|
+
*
|
|
3369
|
+
* @param filter Notion `views.retrieve` 가 준 필터 객체(`unknown` — 문법이 계속 늘어난다).
|
|
3370
|
+
* @param schema DB 속성 스키마. 속성 id 해소와 status 그룹 전개에 쓴다.
|
|
3371
|
+
* @param resolveName 속성 id(또는 이름) → 프론트매터 키 해소기.
|
|
3372
|
+
*/
|
|
3373
|
+
declare function translateNotionFilter(filter: unknown, schema: Record<string, BasePropertySchema>, resolveName: PropertyNameResolver): TranslationResult;
|
|
3374
|
+
|
|
1920
3375
|
/**
|
|
1921
3376
|
* DB 사이드카(`<db>.notion.json`) — `.base` 로 표현할 수 없는 Notion DB 메타데이터를
|
|
1922
3377
|
* 무손실 보존하는 단일 파일.
|
|
@@ -1968,7 +3423,12 @@ interface SidecarView {
|
|
|
1968
3423
|
}
|
|
1969
3424
|
interface DegradeNote {
|
|
1970
3425
|
readonly view: string;
|
|
1971
|
-
|
|
3426
|
+
/**
|
|
3427
|
+
* `view-unrepresentable` = 뷰가 `.base` 에서 통째로 누락.
|
|
3428
|
+
* `view-degraded` = 뷰는 남지만 다른 타입으로 격하(달력→표 등).
|
|
3429
|
+
* `setting-dropped` = 뷰는 그대로지만 설정 하나가 빠짐.
|
|
3430
|
+
*/
|
|
3431
|
+
readonly kind: "view-unrepresentable" | "view-degraded" | "setting-dropped";
|
|
1972
3432
|
readonly detail: string;
|
|
1973
3433
|
}
|
|
1974
3434
|
declare class SidecarGenerator {
|
|
@@ -1994,6 +3454,15 @@ declare class SidecarGenerator {
|
|
|
1994
3454
|
* 미표현 뷰는 이미 view-unrepresentable 로 통째 보고됨).
|
|
1995
3455
|
*/
|
|
1996
3456
|
private extractConfig;
|
|
3457
|
+
/**
|
|
3458
|
+
* 필터 손실을 **실제로 못 옮긴 것만** 보고한다.
|
|
3459
|
+
*
|
|
3460
|
+
* 예전엔 필터가 있기만 하면 "Notion 필터식은 .base 로 표현되지 않음"을 무조건 찍었다.
|
|
3461
|
+
* 이제 `.base` 가 뷰별 `filters:` 를 내보내므로 그 문구는 대부분의 경우 거짓이다.
|
|
3462
|
+
* 거짓 degrade 는 진짜 degrade 를 묻어 버리므로, `.base` 와 **같은 번역기**를 돌려
|
|
3463
|
+
* 남은 것만 적는다(두 산출물이 어긋날 여지를 없앤다).
|
|
3464
|
+
*/
|
|
3465
|
+
private noteFilterDegrade;
|
|
1997
3466
|
}
|
|
1998
3467
|
|
|
1999
3468
|
declare function sortEntries(entries: DBEntry[], sorts: NonNullable<ViewConfig["sorts"]>): DBEntry[];
|
|
@@ -2016,6 +3485,8 @@ interface FilterCondition {
|
|
|
2016
3485
|
}
|
|
2017
3486
|
declare function filterEntries(entries: DBEntry[], searchQuery?: string, filters?: FilterCondition[]): DBEntry[];
|
|
2018
3487
|
|
|
3488
|
+
/** Notion 이 쓰는 색 이름 전부(`default` 포함) — 색을 다루는 다른 곳은 이 목록을 읽는다. */
|
|
3489
|
+
declare const NOTION_COLOR_NAMES: readonly NotionColor[];
|
|
2019
3490
|
declare function getNotionColor(color: NotionColor | string): string;
|
|
2020
3491
|
declare function getNotionBgColor(color: NotionColor | string): string;
|
|
2021
3492
|
declare function generateColorCSS(): string;
|
|
@@ -2097,21 +3568,131 @@ declare function summarizeFidelity(perFile: ReadonlyArray<{
|
|
|
2097
3568
|
classification: FidelityClassification;
|
|
2098
3569
|
}>): FidelitySummary;
|
|
2099
3570
|
|
|
3571
|
+
/** 한 줄만 보고 판정할 수 있는 렌더 결함. */
|
|
3572
|
+
interface RenderLineRule {
|
|
3573
|
+
readonly code: string;
|
|
3574
|
+
readonly label: string;
|
|
3575
|
+
readonly re: RegExp;
|
|
3576
|
+
}
|
|
3577
|
+
declare const RENDER_LINE_RULES: readonly RenderLineRule[];
|
|
3578
|
+
/** 발견된 렌더 결함 하나. */
|
|
3579
|
+
interface RenderFinding {
|
|
3580
|
+
readonly code: string;
|
|
3581
|
+
readonly label: string;
|
|
3582
|
+
readonly line: number;
|
|
3583
|
+
}
|
|
3584
|
+
/**
|
|
3585
|
+
* 표 구분행 — 가로 공백만 허용한다. `\s` 를 쓰면 **줄바꿈까지 삼킨다**.
|
|
3586
|
+
*
|
|
3587
|
+
* 그러면 빈 표 행(`| | | |`) 다음의 수평선(`---`)이 한 덩어리로 잡혀 구분행 하나가
|
|
3588
|
+
* 날조되고, 데이터 행이 하나 줄어든 것처럼 보인다.
|
|
3589
|
+
*/
|
|
3590
|
+
declare const SEPARATOR_BODY = "[ \\t:|-]*-{3,}";
|
|
3591
|
+
/** 볼트에 실제로 쓰인 마크다운을 훑어 렌더 결함을 모은다. */
|
|
3592
|
+
declare function lintRenderedMarkdown(markdown: string): RenderFinding[];
|
|
3593
|
+
|
|
2100
3594
|
declare function computeHash(content: string): string;
|
|
2101
3595
|
declare function computeBufferHash(buffer: Buffer): string;
|
|
2102
3596
|
|
|
2103
3597
|
declare function generateId(): string;
|
|
2104
3598
|
declare function normalizeNotionId(id: string): string;
|
|
3599
|
+
/**
|
|
3600
|
+
* 사용자가 넣은 페이지 — ID(대시 있거나 없거나) 또는 Notion 주소(브라우저 주소창 · 「링크 복사」). 주소창에
|
|
3601
|
+
* 보이는 것을 그대로 붙여 넣어도 되게 한다. 못 찾으면 null.
|
|
3602
|
+
*
|
|
3603
|
+
* 데이터베이스 보기에서 연 페이지 주소는 `…/<DB>?v=<보기>&p=<페이지>` 다 — 연 페이지는 `p` 이고, `v` 는 보기다.
|
|
3604
|
+
*/
|
|
3605
|
+
declare function notionIdFromInput(input: string): string | null;
|
|
2105
3606
|
declare function notionIdsEqual(a: string, b: string): boolean;
|
|
2106
3607
|
/**
|
|
2107
3608
|
* 멘션 위키링크용 정규 id — 하이픈 제거 + 소문자. 오케스트레이터의 멘션 해소 정규식
|
|
2108
|
-
* (`[[notion:<32hex>]]`)이 인식하는 유일한 형태다. 블록
|
|
3609
|
+
* (`[[notion:<32hex>]]`)이 인식하는 유일한 형태다. 블록 방식 받기(`blocks-to-nfm`)와
|
|
2109
3610
|
* markdown-api 경로(convertPageMentions)가 동일 형태로 수렴하도록 한다(rank20/I3).
|
|
2110
3611
|
*/
|
|
2111
3612
|
declare function compactNotionId(id: string): string;
|
|
2112
3613
|
|
|
2113
3614
|
declare function sanitizeFileName(name: string): string;
|
|
2114
3615
|
|
|
3616
|
+
/** 노트(마크다운 파일)의 경로 · 이름인가. */
|
|
3617
|
+
declare function isMarkdownPath(path: string): boolean;
|
|
3618
|
+
/** 노트 확장자를 뗀다 — 노트가 아니면 그대로. */
|
|
3619
|
+
declare function stripMarkdownExtension(path: string): string;
|
|
3620
|
+
/** 경로의 노트 이름 — 마지막 조각에서 노트 확장자를 뗀다(`폴더/노트.MD` → `노트`). */
|
|
3621
|
+
declare function noteNameFromPath(path: string): string;
|
|
3622
|
+
|
|
3623
|
+
/**
|
|
3624
|
+
* 시각을 시간대의 벽시계 시각으로, 벽시계 시각을 오프셋이 붙은 시각으로 바꾼다(F-08).
|
|
3625
|
+
*
|
|
3626
|
+
* Obsidian 의 날짜시각 속성은 시간대가 없는 `YYYY-MM-DDTHH:mm` 이다 — 초 · 밀리초 · `Z` · `+09:00` 이
|
|
3627
|
+
* 붙으면 글로 본다(2026-10-04 Obsidian 1.12.4 실측). Notion 은 오프셋 없는 시각을 UTC 로 읽는다(같은 날
|
|
3628
|
+
* 실측: `2026-10-01T10:00` 을 보내면 `2026-10-01T10:00:00.000+00:00`). 그래서 pull 은 시각을 시간대의
|
|
3629
|
+
* 벽시계 시각으로 적고, push 는 그 시간대의 오프셋을 붙여 보낸다 — 같은 순간이다.
|
|
3630
|
+
*/
|
|
3631
|
+
/**
|
|
3632
|
+
* 이 컴퓨터의 시간대(IANA 이름). 부를 때마다 읽는다 — 앱이 켜진 채 시간대가 바뀔 수 있다.
|
|
3633
|
+
*/
|
|
3634
|
+
declare function systemTimeZone(): string;
|
|
3635
|
+
|
|
3636
|
+
/**
|
|
3637
|
+
* `--path` 범위 판정 — 볼트 상대경로가 사용자가 지정한 범위 안에 있는지.
|
|
3638
|
+
*
|
|
3639
|
+
* 단순 `startsWith` 는 **경로 조각 경계를 보지 않아** 범위를 넘겨 짚는다.
|
|
3640
|
+
* `--path Notes` 가 `Notes-archive/비밀.md` · `Notes백업.md` 까지 끌어와, 사용자가
|
|
3641
|
+
* 의도한 폴더 밖 파일을 push/pull 대상에 올린다(범위 지정의 의미가 무너진다).
|
|
3642
|
+
*
|
|
3643
|
+
* 규칙은 셋뿐이다.
|
|
3644
|
+
* 1. 완전 일치 — `--path a/b.md` ↔ `a/b.md`
|
|
3645
|
+
* 2. 폴더 하위 — `--path a` ↔ `a/b.md` (경계 `/` 를 반드시 소비)
|
|
3646
|
+
* 3. 그 외는 불일치 — `--path a` ↔ `ab.md` ✗
|
|
3647
|
+
*
|
|
3648
|
+
* 사용자가 `--path a/` 처럼 슬래시를 붙여 쓰는 경우도 같은 범위로 본다.
|
|
3649
|
+
*/
|
|
3650
|
+
declare function matchesPathScope(path: string, scope: string): boolean;
|
|
3651
|
+
/** 범위 목록 중 하나라도 걸리면 true. 목록이 비어 있으면(범위 미지정) 항상 true. */
|
|
3652
|
+
declare function inAnyPathScope(path: string, scopes: readonly string[] | undefined): boolean;
|
|
3653
|
+
|
|
3654
|
+
/** 줄 비교의 한 줄. 번호는 1부터 — 더한 줄은 옛 번호가, 지운 줄은 새 번호가 없다. */
|
|
3655
|
+
interface DiffLine {
|
|
3656
|
+
readonly kind: "same" | "added" | "removed";
|
|
3657
|
+
readonly text: string;
|
|
3658
|
+
readonly oldNumber: number | null;
|
|
3659
|
+
readonly newNumber: number | null;
|
|
3660
|
+
/**
|
|
3661
|
+
* 글 끝에 줄바꿈이 없는 줄. 바뀐 줄에만 붙인다 — 끝 줄바꿈만 다른 두 줄이 같은 줄을 지우고 다시 쓴 것처럼
|
|
3662
|
+
* 보이지 않게. 같은 줄에 붙은 것은 양쪽 다 없다는 뜻이라 차이가 아니다.
|
|
3663
|
+
*/
|
|
3664
|
+
readonly noNewlineAtEnd?: true;
|
|
3665
|
+
}
|
|
3666
|
+
/** 바뀐 줄과 그 앞뒤의 같은 줄 묶음 — Git 의 `@@ -oldStart,oldLines +newStart,newLines @@`. */
|
|
3667
|
+
interface DiffHunk {
|
|
3668
|
+
/** 묶음의 첫 옛 줄 번호(1부터). 옛 줄이 없는 묶음(`oldLines` 0)도 1부터 센다. */
|
|
3669
|
+
readonly oldStart: number;
|
|
3670
|
+
readonly oldLines: number;
|
|
3671
|
+
readonly newStart: number;
|
|
3672
|
+
readonly newLines: number;
|
|
3673
|
+
readonly lines: readonly DiffLine[];
|
|
3674
|
+
}
|
|
3675
|
+
/** 줄 종류마다 앞에 붙이는 부호 — Git 의 통합 diff 와 같다. 줄 비교를 글로 보이는 곳은 모두 이것을 쓴다. */
|
|
3676
|
+
declare const DIFF_SIGN: Readonly<Record<DiffLine["kind"], string>>;
|
|
3677
|
+
interface LineDiffOptions {
|
|
3678
|
+
/** 바뀐 줄 앞뒤로 함께 보일 같은 줄 수. 기본 3 — Git 과 같다. */
|
|
3679
|
+
readonly context?: number;
|
|
3680
|
+
}
|
|
3681
|
+
/**
|
|
3682
|
+
* 두 글의 줄 비교 — 바뀐 곳과 그 앞뒤 몇 줄을 묶음으로 낸다. 같은 글이면 빈 배열.
|
|
3683
|
+
*
|
|
3684
|
+
* 예전의 충돌 비교는 같은 번호의 줄끼리 견줬다 — 한쪽 앞에 한 줄만 더해도 그 뒤의 모든 줄이 바뀐 것으로
|
|
3685
|
+
* 보였다. 여기는 가장 긴 공통 줄을 맞춰 실제로 더하고 지운 줄만 가른다.
|
|
3686
|
+
*/
|
|
3687
|
+
declare function lineDiff(before: string, after: string, options?: LineDiffOptions): DiffHunk[];
|
|
3688
|
+
/**
|
|
3689
|
+
* 통합 diff 글의 줄(Git 의 `diff -u` 와 같은 모양) — 옛 글 · 새 글 이름 두 줄, 묶음마다 `@@` 머리와 부호를
|
|
3690
|
+
* 붙인 줄. 같은 글이면 이름 두 줄뿐이다. 끝 줄바꿈이 없다는 표시는 Git 과 달리 바뀐 줄 뒤에만 적는다
|
|
3691
|
+
* (`DiffLine.noNewlineAtEnd`). 글로 보이는 곳(CLI `diff` · `resolve`)이 모두 이것을 쓴다 — 저마다 만들면
|
|
3692
|
+
* 앞뒤 줄 수 · 큰 글 상한이 곳마다 달라진다.
|
|
3693
|
+
*/
|
|
3694
|
+
declare function formatUnifiedDiff(hunks: readonly DiffHunk[], oldName: string, newName: string): string[];
|
|
3695
|
+
|
|
2115
3696
|
interface Logger {
|
|
2116
3697
|
warn(message: string, ...args: unknown[]): void;
|
|
2117
3698
|
error(message: string, ...args: unknown[]): void;
|
|
@@ -2169,4 +3750,77 @@ declare function isInternalPath(path: string): boolean;
|
|
|
2169
3750
|
/** 모든 보존 마커가 공유하는 브랜드 토큰. */
|
|
2170
3751
|
declare const MARKER_BRAND = "im-nobsidian";
|
|
2171
3752
|
|
|
2172
|
-
|
|
3753
|
+
/**
|
|
3754
|
+
* 디자인 마커 읽기 — 볼트 노트 한 줄에 실린 마커가 «무엇을 어디에» 칠하는지 (U-01).
|
|
3755
|
+
*
|
|
3756
|
+
* pull 은 Obsidian 에 문법이 없는 Notion 서식(글자색 · 밑줄 · 멘션 · 블록 색 · 콜아웃/토글 스타일 ·
|
|
3757
|
+
* 표 속성)을 `%%…%%` 마커로 싣는다(ADR-008 · F-01 · F-04). 마커는 Obsidian 주석이라 읽기 보기에서는
|
|
3758
|
+
* 지워지고, 편집 화면(Live Preview)에서는 글자로 보인다 — 서식은 어느 쪽에서도 보이지 않는다.
|
|
3759
|
+
* 플러그인 렌더러가 이 결과로 칠한다. 마커 모양의 주인이 이 패키지(`constants/markers.ts`)라 읽기도
|
|
3760
|
+
* 여기 둔다 — 모양을 바꾸면 이 파일과 그 시험이 같이 깨진다.
|
|
3761
|
+
*/
|
|
3762
|
+
/** Notion 색 하나 — `red` · `red_bg` · `red_background` 를 같은 값으로 읽는다. */
|
|
3763
|
+
interface DesignColor {
|
|
3764
|
+
readonly name: Exclude<NotionColor, "default">;
|
|
3765
|
+
readonly background: boolean;
|
|
3766
|
+
}
|
|
3767
|
+
/** 글 구간에 걸리는 서식. */
|
|
3768
|
+
type InlineStyle = {
|
|
3769
|
+
readonly kind: "color";
|
|
3770
|
+
readonly color: DesignColor;
|
|
3771
|
+
} | {
|
|
3772
|
+
readonly kind: "underline";
|
|
3773
|
+
} | {
|
|
3774
|
+
readonly kind: "mention";
|
|
3775
|
+
};
|
|
3776
|
+
/** 줄 안의 글자 위치 `[from, to)`. */
|
|
3777
|
+
interface LineRange {
|
|
3778
|
+
readonly from: number;
|
|
3779
|
+
readonly to: number;
|
|
3780
|
+
}
|
|
3781
|
+
interface StyledRange extends LineRange {
|
|
3782
|
+
readonly style: InlineStyle;
|
|
3783
|
+
}
|
|
3784
|
+
/** 마커를 걷어 낸 글 조각과 그 조각에 걸린 서식 — 마커가 없는 글도 조각이 된다(서식 없음). */
|
|
3785
|
+
interface DesignSegment extends LineRange {
|
|
3786
|
+
readonly text: string;
|
|
3787
|
+
readonly styles: readonly InlineStyle[];
|
|
3788
|
+
}
|
|
3789
|
+
/**
|
|
3790
|
+
* 콜아웃 · 토글 머리 줄의 스타일. `icon` 이 null 이면 Notion 에서 아이콘이 없는 콜아웃이다
|
|
3791
|
+
* (마커에 icon 이 없다). 토글은 아이콘을 싣지 않는다(undefined).
|
|
3792
|
+
*/
|
|
3793
|
+
interface CalloutDesign {
|
|
3794
|
+
readonly color?: DesignColor;
|
|
3795
|
+
readonly icon?: string | null;
|
|
3796
|
+
}
|
|
3797
|
+
/** 표 한 줄(행)의 속성 — 셀 번호는 0 부터, 파이프 사이 칸 차례다. */
|
|
3798
|
+
interface TableLineDesign {
|
|
3799
|
+
readonly headerRow?: boolean;
|
|
3800
|
+
readonly headerColumn?: boolean;
|
|
3801
|
+
readonly rowColor?: DesignColor;
|
|
3802
|
+
/** 셀 번호 → 셀 색. */
|
|
3803
|
+
readonly cellColors: ReadonlyMap<number, DesignColor>;
|
|
3804
|
+
/** 셀 번호 → 열 색(머리 행의 `table-col` 마커가 그 열 전체에 건다). */
|
|
3805
|
+
readonly columnColors: ReadonlyMap<number, DesignColor>;
|
|
3806
|
+
}
|
|
3807
|
+
interface DesignLine {
|
|
3808
|
+
/** 줄에 있는 우리 마커 전부 — 편집 화면이 숨긴다. 짝이 맞은 닫는 토큰(`%%/color%%` 등)도 든다. */
|
|
3809
|
+
readonly markers: readonly LineRange[];
|
|
3810
|
+
/** 짝 마커 사이의 글 — 편집 화면이 칠한다. */
|
|
3811
|
+
readonly styled: readonly StyledRange[];
|
|
3812
|
+
/** 마커를 걷어 낸 차례대로의 글 조각 — 읽기 보기가 그려진 글과 맞춰 칠한다. */
|
|
3813
|
+
readonly segments: readonly DesignSegment[];
|
|
3814
|
+
readonly blockColor?: DesignColor;
|
|
3815
|
+
/** 콜아웃 · 토글 머리 줄의 스타일. */
|
|
3816
|
+
readonly callout?: CalloutDesign;
|
|
3817
|
+
readonly table?: TableLineDesign;
|
|
3818
|
+
}
|
|
3819
|
+
/** 이 글에 우리 마커가 하나라도 있나 — 렌더러가 마커 없는 블록을 건너뛰는 데 쓴다. */
|
|
3820
|
+
declare function hasDesignMarker(text: string): boolean;
|
|
3821
|
+
/** Notion 색 토큰을 읽는다. `default` · 모르는 색은 칠할 것이 없다(undefined). */
|
|
3822
|
+
declare function parseDesignColor(token: string | undefined): DesignColor | undefined;
|
|
3823
|
+
/** 한 줄을 읽는다. 마커가 없는 줄은 글 조각 하나뿐인 결과를 돌려준다. */
|
|
3824
|
+
declare function readDesignLine(line: string): DesignLine;
|
|
3825
|
+
|
|
3826
|
+
export { BaseFileGenerator, type BaseFileOptions, type BasePropertyOption, type BasePropertySchema, type BaseStatusGroup, type BaseSyncOptions, type BasesFilterNode, type BasesViewMapping, type BasesViewType, BlankLineCollapser, type BookmarkTarget, CONFIG_FILE, type CalendarEntry, type CalloutDesign, CalloutRestorer, CalloutTransformer, ChangeDetector, type ChangeDiff, 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, DEFAULT_TITLE_PROPERTY, DEFAULT_WATCH_DEBOUNCE_MS, DIFF_SIGN, type DatabaseCompleteness, type DatabaseSyncConfig, type DatabaseSyncResult, DatabaseSyncer, type DatabaseViewsConfig, type DeferredCode, type DegradeNote, type DesignColor, type DesignLine, type DesignSegment, type DiffHunk, type DiffLine, 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, type FolderMoveChange, FrontmatterExtractor, FrontmatterGenerator, type FullScanReason, GITIGNORE_ENTRY, type GatedOperation, type GroupByConfig, type GroupedEntries, IGNORE_FILE, INTERNAL_DIR, INTERNAL_DIR_GLOB, type IStateDB, type ImageDownloadResult, ImageHandler, type ImageReference, InlineAnnotationPreserver, InlineDBParser, type InlineStyle, type LineDiffOptions, type LineRange, type LocalChange, LocalImageRestorer, type LocalMoveAdoption, type LocalScan, type LocalScanOptions, type Logger, MARKER_BRAND, type MappingResult, type MappingType, MathNormalizer, MentionToWikilink, type MergeConflictRegion, type MergeResult, NOTION_COLOR_NAMES, NOTION_TOKEN_PREFIX, 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, PendingWalError, type PlannedRow, 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 RemoteObservation, type RemoteScanInfo, type RenameHints, type RenameKind, type RenderFinding, type RenderLineRule, type ResolutionChoice, type ResolutionResult, type RichTextSegment, type RowProgress, SAVED_STATE_TABLES_QUERY, SEPARATOR_BODY, STATE_DB_FILE, STATE_DB_MIGRATIONS, STATE_DB_PATH, SavedStateDbError, SidecarGenerator, type SidecarProperty, type SidecarView, StateDB, StateDbLockedError, type StateDbMigration, type StateDbTool, StateDbUnavailableError, StateLock, type StateLockClaim, type StateLockOwner, type StatusOptions, type StatusResult, type StyledRange, SyncBusyError, type SyncDirection, type SyncOptions, SyncOrchestrator, type SyncRecord, type SyncResult, type SyncStatus, type TableLineDesign, type TranslationResult, TreeMapper, UnsupportedBlockStripper, type UpsertSyncRecord, type VaultCompletenessReport, type VaultFS, type ViewConfig, ViewDataProvider, type ViewPropertyConfig, type ViewRenderData, type ViewType, WIKILINK_LOOKUPS, type WatchSyncOptions, type WatchSyncScope, WatchSyncService, type WikilinkEntry, type WikilinkLookup, WikilinkResolver, applicableChoices, assertNoPendingWal, basesViewMappingOf, basesViewTypeOf, choiceForStrategy, classifyBodyFidelity, compactNotionId, computeBufferHash, computeHash, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, pendingWalBytes, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness };
|