@im-nobsidian/core 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -511,6 +511,20 @@ interface PullResult {
511
511
  readonly linkCount: number;
512
512
  /** 원격을 얼마나 훑었나. 원격을 보지 않은 실행(받기 방향이 아님 · 취소)에는 없다. */
513
513
  readonly remoteScan?: RemoteScanInfo;
514
+ /** Notion 에서 옮긴 페이지를 볼트에서 옮긴 것(ADR-039) — dry-run 은 옮길 것. 없으면 옮긴 것이 없다. */
515
+ readonly moved?: readonly RemoteMove[];
516
+ /** Notion 에서 옮겼지만 볼트에서 옮기지 않은 것과 그 까닭. */
517
+ readonly moveSkipped?: readonly RemoteMoveSkip[];
518
+ }
519
+ /** Notion 에서 옮긴 페이지를 볼트에서 옮긴 것 하나 — 노트 하나거나 폴더 하나다. */
520
+ interface RemoteMove {
521
+ readonly from: string;
522
+ readonly to: string;
523
+ }
524
+ /** 옮기지 않고 둔 것과 그 까닭 — 볼트에서 직접 옮기면 다음 push 가 Notion 을 맞춘다. */
525
+ interface RemoteMoveSkip {
526
+ readonly path: string;
527
+ readonly reason: string;
514
528
  }
515
529
  interface SyncResult {
516
530
  readonly push: PushResult;
@@ -2133,6 +2147,8 @@ interface VaultFS {
2133
2147
  * 이 볼트(연결)가 맡은 경로인가 — 볼트 안의 다른 연결 폴더(ADR-038)는 false. 없으면 전부 맡는다.
2134
2148
  */
2135
2149
  ownsPath?(path: string): boolean;
2150
+ /** 이 폴더 안에 다른 연결 폴더가 있나 — 폴더를 통째로 옮기기 전에 묻는다. 없으면 없다. */
2151
+ holdsOtherConnection?(folder: string): boolean;
2136
2152
  /**
2137
2153
  * 설정(`paths.include` · `paths.exclude`)이나 `.im-nobsidian-ignore` 가 동기화에서 뺀 경로인가 (이슈 #8).
2138
2154
  * 뺀 노트는 목록에 없다 — 추적하던 노트라도 「볼트에서 지움」 으로 보지 않고, pull 이 받거나 되살리지도
@@ -2455,6 +2471,7 @@ declare class SyncOrchestrator {
2455
2471
  private readonly placement;
2456
2472
  private readonly databaseCreator;
2457
2473
  private readonly planner;
2474
+ private readonly remoteMoves;
2458
2475
  private readonly detector;
2459
2476
  private readonly drift;
2460
2477
  private readonly discovery;
@@ -2483,6 +2500,8 @@ declare class SyncOrchestrator {
2483
2500
  /** 볼트의 노트가 마지막으로 올린 내용 그대로인가 — 읽지 못하면(지움 · 옮김) 아니다. */
2484
2501
  private unchangedSincePush;
2485
2502
  pull(options?: PullOptions): Promise<PullResult>;
2503
+ /** 감지가 본, Notion 에서 옮긴 페이지를 볼트에서 옮긴다 — 경로를 좁힌 pull 은 그 범위의 것만. */
2504
+ private followRemoteMoves;
2486
2505
  private executePull;
2487
2506
  sync(options?: SyncOptions): Promise<SyncResult>;
2488
2507
  private executeSync;
@@ -4101,6 +4120,7 @@ declare class ScopedVaultFS implements VaultFS {
4101
4120
  */
4102
4121
  constructor(base: VaultFS, folder: string, connections: readonly string[]);
4103
4122
  ownsPath(path: string): boolean;
4123
+ holdsOtherConnection(folder: string): boolean;
4104
4124
  excludesPath(path: string, kind?: "file" | "folder"): boolean;
4105
4125
  /** 이 연결의 경로 → 볼트 경로. */
4106
4126
  private toBase;
@@ -4283,4 +4303,4 @@ declare function readColumnLayout(lines: readonly string[]): (ColumnPlace | unde
4283
4303
  /** 열 이름표 — `열 2/3 · 50%`. 비율이 없으면 `열 2/3`. */
4284
4304
  declare function columnLabel(place: ColumnPlace): string;
4285
4305
 
4286
- 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 ColumnPlace, type CompletenessFailure, type CompletenessLocalSource, type CompletenessOptions, type CompletenessRemoteSource, type CompletenessReport, type Config, ConfigManager, ConfigSchema, type Conflict, ConflictResolver, type ConflictStrategy, ConnectionBoundaryError, ConnectionScopeError, 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 FolderDatabaseReport, 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, type InferredProperty, 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, OBSIDIAN_CONFIG_DIR, type OperationType, type PageCompletenessRemoteSource, type PageCompletenessReport, type PageCover, type PageCreationPlan, type PageIcon, PathFilter, 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, ScopedVaultFS, 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, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
4306
+ 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 ColumnPlace, type CompletenessFailure, type CompletenessLocalSource, type CompletenessOptions, type CompletenessRemoteSource, type CompletenessReport, type Config, ConfigManager, ConfigSchema, type Conflict, ConflictResolver, type ConflictStrategy, ConnectionBoundaryError, ConnectionScopeError, 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 FolderDatabaseReport, 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, type InferredProperty, 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, OBSIDIAN_CONFIG_DIR, type OperationType, type PageCompletenessRemoteSource, type PageCompletenessReport, type PageCover, type PageCreationPlan, type PageIcon, PathFilter, 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 RemoteMove, type RemoteMoveSkip, 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, ScopedVaultFS, 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, columnLabel, compactNotionId, computeBufferHash, computeHash, configDirFrom, connectionFolderOf, createDefaultPipeline, extractCalendarEntries, filterByMonth, filterEntries, findConnectionFolders, formatUnifiedDiff, generateColorCSS, generateId, getLogger, getNotionBgColor, getNotionColor, getVisibleProperties, groupEntries, hasColumnLayout, hasDesignMarker, inAnyPathScope, isInternalPath, isMarkdownPath, isRemoteDeletion, lineDiff, lintRenderedMarkdown, matchesPathScope, normalizeNotionId, normalizeVaultPath, noteNameFromPath, notionIdFromInput, notionIdsEqual, operationLabel, parseDesignColor, pendingWalBytes, readColumnLayout, readDesignLine, resolvePropertyName, sanitizeFileName, setLogger, sortEntries, stateDbLockPath, stateDbWalPath, stripMarkdownExtension, summarizeFidelity, systemTimeZone, threeWayMerge, toVaultPath, translateNotionFilter, verifyDatabaseCompleteness, verifyPageCompleteness, wikilinkLookupsIn };
package/dist/index.js CHANGED
@@ -4656,6 +4656,15 @@ function folderNoteOf(folder) {
4656
4656
  function isFolderRecord(record) {
4657
4657
  return (record.fileType === "folder-note" || record.fileType === "folder-only") && !isMarkdownPath(record.obsidianPath);
4658
4658
  }
4659
+ function childFolderOf(record, lookup) {
4660
+ if (isFolderRecord(record) && !lookup.databaseAt(record.obsidianPath)) {
4661
+ return record.obsidianPath;
4662
+ }
4663
+ if (record.fileType === "folder-note" || record.fileType === "folder-only") {
4664
+ return parentFolderOf(record.obsidianPath);
4665
+ }
4666
+ return stripMarkdownExtension(record.obsidianPath);
4667
+ }
4659
4668
  function parentFolderOf(path) {
4660
4669
  const slash = path.lastIndexOf("/");
4661
4670
  return slash < 0 ? "" : path.slice(0, slash);
@@ -5217,6 +5226,7 @@ var FrontmatterExtractor = class {
5217
5226
  };
5218
5227
 
5219
5228
  // src/utils/md-regions.ts
5229
+ var LIST_MARKER_SOURCE = String.raw`(?:[-*+]|\d{1,9}[.)])`;
5220
5230
  var BLANK_LINE_RE = /\n[ \t]*\n/;
5221
5231
  function computeAnchor(content, idx) {
5222
5232
  const before = content.slice(Math.max(0, idx - 32), idx);
@@ -5255,7 +5265,9 @@ function closingFence(line, open) {
5255
5265
  }
5256
5266
  return { lead: m[1], bar: m[2] };
5257
5267
  }
5258
- var QUOTED_FENCE_OPEN_RE = /^((?:[\t ]*>)*)([\t ]*)((?:[-*+]|\d{1,9}[.)])[\t ]+)?(`{3,}|~{3,})(.*)\r?$/;
5268
+ var QUOTED_FENCE_OPEN_RE = new RegExp(
5269
+ String.raw`^((?:[\t ]*>)*)([\t ]*)(${LIST_MARKER_SOURCE}[\t ]+)?(\`{3,}|~{3,})(.*)\r?$`
5270
+ );
5259
5271
  var QUOTE_MARK_RE = /^[\t ]*>/;
5260
5272
  var LEADING_SPACE_RE = /^[\t ]*/;
5261
5273
  function stripQuoteMarks(line, depth2) {
@@ -5352,7 +5364,7 @@ function codeLineMask(content) {
5352
5364
  }
5353
5365
  return inCode;
5354
5366
  }
5355
- var TOP_LIST_ITEM_RE = /^ {0,3}(?:[-*+]|\d{1,9}[.)])(?:[\t ]|$)/;
5367
+ var TOP_LIST_ITEM_RE = new RegExp(String.raw`^ {0,3}${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
5356
5368
  var INDENTED_CODE_WIDTH = 4;
5357
5369
  function indentedCodeLineMask(content) {
5358
5370
  const lines = content.split("\n");
@@ -5683,7 +5695,9 @@ function withoutPrefix(line, prefix) {
5683
5695
  return null;
5684
5696
  }
5685
5697
  var DOUBLE_DOLLAR_RE = /(?<![\\$])\$\$(?!\$)/g;
5686
- var OWN_LINE_DOLLARS_RE = /^(?:[ \t]*>)*[ \t]*(?:\[![^\]]*\][+-]?[ \t]*|(?:[-*+]|\d+[.)])[ \t]+)?\$\$[ \t]*$/;
5698
+ var OWN_LINE_DOLLARS_RE = new RegExp(
5699
+ String.raw`^(?:[ \t]*>)*[ \t]*(?:\[![^\]]*\][+-]?[ \t]*|${LIST_MARKER_SOURCE}[ \t]+)?\$\$[ \t]*$`
5700
+ );
5687
5701
  function settleDollars(content, final) {
5688
5702
  return mapOutsideCode(
5689
5703
  content,
@@ -6208,7 +6222,7 @@ function widenAmbiguousFences(lines, blocks) {
6208
6222
  }
6209
6223
  return widened;
6210
6224
  }
6211
- var LIST_ITEM_LINE_RE = /^[\t ]*(?:[-*+]|\d{1,9}[.)])(?:[\t ]|$)/;
6225
+ var LIST_ITEM_LINE_RE = new RegExp(String.raw`^[\t ]*${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
6212
6226
  function alignNestedCodeBodies(text) {
6213
6227
  const lines = text.split("\n");
6214
6228
  const blocks = scanContainerBlocks(lines);
@@ -8052,7 +8066,7 @@ function clampCalloutIndent(content) {
8052
8066
  }
8053
8067
  return out.join("\n");
8054
8068
  }
8055
- var BODY_INDENT_RE = /^\t+(?![ \t]*(?:[-*+]|\d+[.)])(?:\s|$))/;
8069
+ var BODY_INDENT_RE = new RegExp(String.raw`^\t+(?![ \t]*${LIST_MARKER_SOURCE}(?:\s|$))`);
8056
8070
  function clampBodyIndent(line) {
8057
8071
  return line.replace(BODY_INDENT_RE, CLAMPED_INDENT);
8058
8072
  }
@@ -8253,10 +8267,10 @@ function stripQuotes(s) {
8253
8267
 
8254
8268
  // src/converter/lazy-continuation.ts
8255
8269
  var QUOTE_PREFIX_RE2 = /^[\t >]*>/;
8256
- var LIST_ITEM_RE = /^(?:[-*+]|\d+[.)])(?:[\t ]|$)/;
8270
+ var LIST_ITEM_RE = new RegExp(String.raw`^${LIST_MARKER_SOURCE}(?:[\t ]|$)`);
8257
8271
  var NOT_LAZY_RE = /^(?:#{1,6}(?:[\t ]|$)|>|\[!|(?:[-*_][\t ]*){3,}$|\||<br\s*\/?>[\t ]*$|%%)/;
8258
8272
  var NOT_PARAGRAPH_RE = /^(?:#{1,6}(?:[\t ]|$)|(?:[-*_][\t ]*){3,}$|\||<br\s*\/?>[\t ]*$|\$\$|%%)/;
8259
- var LIST_PARAGRAPH_RE = /^(?:[-*+]|\d+[.)])[\t ]+\S/;
8273
+ var LIST_PARAGRAPH_RE = new RegExp(String.raw`^${LIST_MARKER_SOURCE}[\t ]+\S`);
8260
8274
  var FENCE_LINE_RE2 = /^(?:`{3,}|~{3,})/;
8261
8275
  function continuesListParagraph(previous, line) {
8262
8276
  const prev = splitQuoted(previous);
@@ -8327,7 +8341,7 @@ var BlockSpacer = class {
8327
8341
  };
8328
8342
  }
8329
8343
  };
8330
- var LIST_ITEM_RE2 = /^[ \t]*(?:[-*+]|\d+[.)])(?:\s|$)/;
8344
+ var LIST_ITEM_RE2 = new RegExp(String.raw`^[ \t]*${LIST_MARKER_SOURCE}(?:\s|$)`);
8331
8345
  var QUOTE_RE = /^[ \t]*>/;
8332
8346
  var CALLOUT_START_RE = /^[ \t]*>\s*\[![^\]]*\]/;
8333
8347
  var TABLE_ROW_RE = /^[ \t]*\|/;
@@ -8702,7 +8716,7 @@ var RENDER_LINE_RULES = [
8702
8716
  var SEPARATOR_BODY = "[ \\t:|-]*-{3,}";
8703
8717
  var SEPARATOR_RE = new RegExp(`^[\\t ]*>?[\\t ]*\\|${SEPARATOR_BODY}[ \\t:|-]*\\|`);
8704
8718
  var DEEP_FENCE_RE = /^ {4,}```/;
8705
- var LIST_ITEM_RE3 = /^([\t ]*)([-*+]|\d{1,9}[.)])(?:[\t ]|$)/;
8719
+ var LIST_ITEM_RE3 = new RegExp(String.raw`^([\t ]*)(${LIST_MARKER_SOURCE})(?:[\t ]|$)`);
8706
8720
  var CODE_INDENT = 4;
8707
8721
  var FOOTNOTE_CONTENT_WIDTH = 4;
8708
8722
  var ATX_HEADING_RE = /^ {0,3}#{1,6}(?:[\t ]|$)/;
@@ -10209,7 +10223,7 @@ function pipeTablesToNfmTables(content) {
10209
10223
  // src/converter/empty-block.ts
10210
10224
  var EMPTY_BLOCK_LINE_RE = new RegExp(`^(${CONTAINER_PREFIX_SOURCE})<empty-block\\/>[\\t ]*$`);
10211
10225
  var BREAK_LINE_RE = new RegExp(`^(${CONTAINER_PREFIX_SOURCE})<br\\s*\\/?>[\\t ]*$`);
10212
- var LIST_ITEM_RE4 = /^(?:[-*+]|\d+[.)])(?:\s|$)/;
10226
+ var LIST_ITEM_RE4 = new RegExp(String.raw`^${LIST_MARKER_SOURCE}(?:\s|$)`);
10213
10227
  function mapProseLines(content, map) {
10214
10228
  const lines = content.split("\n");
10215
10229
  const kinds = classifyContainerLines(lines.map((line) => splitContainerPrefix(line).body));
@@ -11196,7 +11210,7 @@ function unescapeAngleBrackets(content) {
11196
11210
  (segment) => segment.split("\n").map(unescapeLine).join("\n")
11197
11211
  );
11198
11212
  }
11199
- var BLOCK_PREFIX_RE = /^[ \t]*(?:(?:>|[-*+]|\d+[.)])[ \t]*)*$/;
11213
+ var BLOCK_PREFIX_RE = new RegExp(String.raw`^[ \t]*(?:(?:>|${LIST_MARKER_SOURCE})[ \t]*)*$`);
11200
11214
  var TAG_START_RE = /[A-Za-z/!?]/;
11201
11215
  function unescapeLine(line) {
11202
11216
  if (!line.includes("\\<") && !line.includes("\\>")) return line;
@@ -15209,16 +15223,16 @@ var PageDecorations = class {
15209
15223
  *
15210
15224
  * @param baseName 내려받은 파일 이름의 앞부분 — 보통 노트 이름
15211
15225
  */
15212
- async pulled(page, baseName2, download) {
15226
+ async pulled(page, baseName3, download) {
15213
15227
  const out = {};
15214
15228
  const cover = this.notionClient.extractCover(page);
15215
15229
  if (cover) {
15216
- const local = download ? await this.imageHandler.localizeNotionFileUrl(cover.url, `${baseName2}-cover`) : null;
15230
+ const local = download ? await this.imageHandler.localizeNotionFileUrl(cover.url, `${baseName3}-cover`) : null;
15217
15231
  out.cover = local ? `[[${local}]]` : cover.url;
15218
15232
  }
15219
15233
  const icon = this.notionClient.extractIcon(page);
15220
15234
  if (icon) {
15221
- const local = download && icon.type === "file" ? await this.imageHandler.localizeNotionFileUrl(icon.value, `${baseName2}-icon`) : null;
15235
+ const local = download && icon.type === "file" ? await this.imageHandler.localizeNotionFileUrl(icon.value, `${baseName3}-icon`) : null;
15222
15236
  out.icon = local ? `[[${local}]]` : icon.value;
15223
15237
  }
15224
15238
  return out;
@@ -16718,6 +16732,10 @@ var ScopedVaultFS = class {
16718
16732
  const joined = normalizeVaultPath(this.folder === "" ? path : `${this.folder}/${path}`);
16719
16733
  return joined !== null && matchesPathScope(joined, this.folder) && connectionFolderOf(joined, this.nested) === null;
16720
16734
  }
16735
+ holdsOtherConnection(folder) {
16736
+ const joined = normalizeVaultPath(this.folder === "" ? folder : `${this.folder}/${folder}`);
16737
+ return joined !== null && this.nested.some((c) => matchesPathScope(c, joined));
16738
+ }
16721
16739
  excludesPath(path, kind) {
16722
16740
  const joined = normalizeVaultPath(this.folder === "" ? path : `${this.folder}/${path}`);
16723
16741
  return joined !== null && (this.base.excludesPath?.(joined, kind) ?? false);
@@ -18587,6 +18605,199 @@ var LocalPlanner = class {
18587
18605
  }
18588
18606
  };
18589
18607
 
18608
+ // src/sync/remote-moves.ts
18609
+ var MOVABLE_TYPES = /* @__PURE__ */ new Set([
18610
+ "file",
18611
+ "folder-note",
18612
+ "folder-only"
18613
+ ]);
18614
+ function movedInNotion(record, parentId) {
18615
+ return MOVABLE_TYPES.has(record.fileType) && record.notionParentId !== null && !notionIdsEqual(record.notionParentId, parentId);
18616
+ }
18617
+ var RemoteMoveFollower = class {
18618
+ constructor(config, stateDb, vaultFs, folderLookup) {
18619
+ this.config = config;
18620
+ this.stateDb = stateDb;
18621
+ this.vaultFs = vaultFs;
18622
+ this.folderLookup = folderLookup;
18623
+ }
18624
+ config;
18625
+ stateDb;
18626
+ vaultFs;
18627
+ folderLookup;
18628
+ /**
18629
+ * @param moved 페이지 id → Notion 의 새 부모 페이지 id.
18630
+ * @param options.dryRun 옮길 자리만 정하고 쓰지 않는다.
18631
+ */
18632
+ async follow(moved, options = {}) {
18633
+ const result = { moved: [], skipped: [] };
18634
+ for (const [pageId, parentId] of moved) {
18635
+ const record = this.stateDb.getByNotionId(pageId);
18636
+ if (!record || !movedInNotion(record, parentId)) continue;
18637
+ if (this.vaultFs.excludesPath?.(record.obsidianPath)) continue;
18638
+ let outcome;
18639
+ try {
18640
+ outcome = await this.followOne(record, parentId, options.dryRun === true);
18641
+ } catch (error) {
18642
+ outcome = {
18643
+ path: record.obsidianPath,
18644
+ reason: `\uC62E\uAE30\uC9C0 \uBABB\uD568: ${error instanceof Error ? error.message : String(error)}`
18645
+ };
18646
+ }
18647
+ if ("reason" in outcome) {
18648
+ result.skipped.push(outcome);
18649
+ getLogger().warn(
18650
+ `[Im-Nobsidian] Notion \uC5D0\uC11C \uC62E\uAE34 \uD398\uC774\uC9C0\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE30\uC9C0 \uC54A\uC74C (${outcome.path}): ${outcome.reason}`
18651
+ );
18652
+ } else if (outcome.moves.length > 0) {
18653
+ result.moved.push(...outcome.moves);
18654
+ }
18655
+ }
18656
+ return result;
18657
+ }
18658
+ async followOne(record, parentId, dryRun) {
18659
+ const skip = (reason) => ({ path: record.obsidianPath, reason });
18660
+ if (this.stateDb.getIncompleteOpByState(record.id, "move")) {
18661
+ return skip(
18662
+ "\uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE34 \uAC83\uC744 \uC544\uC9C1 \uC62C\uB9AC\uC9C0 \uC54A\uC558\uB2E4 \u2014 \uB2E4\uC74C push \uAC00 Notion \uC744 \uBCFC\uD2B8\uC758 \uC790\uB9AC\uB85C \uC62E\uAE34\uB2E4"
18663
+ );
18664
+ }
18665
+ const folder = this.parentFolder(parentId);
18666
+ if (folder === null) return skip("Notion \uC758 \uC0C8 \uBD80\uBAA8\uAC00 \uBCFC\uD2B8\uC5D0 \uC5C6\uB2E4");
18667
+ const units = this.unitsOf(record);
18668
+ const moves = units.map((from) => ({ from, to: joinPath(folder, baseName2(from)) }));
18669
+ if (moves.every((move) => move.from === move.to)) {
18670
+ if (!dryRun) this.stateDb.setNotionParentId(record.id, parentId);
18671
+ return { moves: [] };
18672
+ }
18673
+ for (const move of moves) {
18674
+ const blocked = await this.blocked(move);
18675
+ if (blocked) return skip(blocked);
18676
+ }
18677
+ if (dryRun) return { moves };
18678
+ const done = [];
18679
+ try {
18680
+ for (const move of moves) {
18681
+ await this.vaultFs.moveFile(move.from, move.to);
18682
+ done.push(move);
18683
+ }
18684
+ } catch (error) {
18685
+ for (const move of done.reverse())
18686
+ await this.vaultFs.moveFile(move.to, move.from).catch(() => {
18687
+ });
18688
+ throw error;
18689
+ }
18690
+ this.stateDb.transaction(() => {
18691
+ for (const move of moves) this.rewritePaths(move);
18692
+ this.stateDb.setNotionParentId(record.id, parentId);
18693
+ });
18694
+ for (const move of moves) {
18695
+ getLogger().info(
18696
+ `[Im-Nobsidian] Notion \uC5D0\uC11C \uC62E\uAE34 \uD398\uC774\uC9C0\uB97C \uBCFC\uD2B8\uC5D0\uC11C \uC62E\uAE40: ${move.from} \u2192 ${move.to}`
18697
+ );
18698
+ }
18699
+ return { moves };
18700
+ }
18701
+ /** 새 부모 아래의 자식이 놓일 폴더 — 새 페이지를 받을 때와 같은 규칙. 루트면 `""`, 볼트에 없는 부모면 null. */
18702
+ parentFolder(parentId) {
18703
+ if (notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
18704
+ const parent = this.stateDb.getByNotionId(parentId);
18705
+ return parent ? childFolderOf(parent, this.folderLookup()) : null;
18706
+ }
18707
+ /**
18708
+ * 함께 옮길 것 — 폴더로 추적하는 페이지는 그 폴더, 폴더 노트(`X/X.md`)는 든 폴더, 노트는 그 파일과 옆의 자식
18709
+ * 폴더(`X/`, 있으면).
18710
+ */
18711
+ unitsOf(record) {
18712
+ if (isFolderRecord(record)) return [record.obsidianPath];
18713
+ if (record.fileType !== "file") return [parentFolderOf(record.obsidianPath)];
18714
+ const children = stripMarkdownExtension(record.obsidianPath);
18715
+ const hasChildren = this.stateDb.getAll().some((r) => within(r.obsidianPath, children));
18716
+ return hasChildren ? [record.obsidianPath, children] : [record.obsidianPath];
18717
+ }
18718
+ /** 옮기지 못할 까닭 — 없으면 null. */
18719
+ async blocked(move) {
18720
+ if (within(move.to, move.from)) return "\uC81C \uC548\uC73C\uB85C \uC62E\uAE38 \uC218 \uC5C6\uB2E4";
18721
+ if (!await this.vaultFs.exists(move.from)) return "\uBCFC\uD2B8\uC5D0 \uB178\uD2B8\uAC00 \uC5C6\uB2E4";
18722
+ if (await this.vaultFs.exists(move.to) || this.stateDb.getAll().some((r) => within(r.obsidianPath, move.to))) {
18723
+ return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uC5D0 \uAC19\uC740 \uC774\uB984\uC774 \uC788\uB2E4`;
18724
+ }
18725
+ const isFolder = !isMarkdownPath(move.from);
18726
+ if (this.vaultFs.excludesPath?.(move.to, isFolder ? "folder" : "file")) {
18727
+ return `\uC62E\uAE38 \uC790\uB9AC(${move.to})\uAC00 \uB3D9\uAE30\uD654\uC5D0\uC11C \uBE60\uC9C4\uB2E4(paths \xB7 .im-nobsidian-ignore)`;
18728
+ }
18729
+ if (isFolder && this.holdsDatabaseOrConnection(move.from)) {
18730
+ return "\uD3F4\uB354 \uC548\uC5D0 DB \uB098 \uB2E4\uB978 \uC5F0\uACB0\uC774 \uC788\uB2E4 \u2014 DB \uBCF4\uAE30(.base)\uAC00 \uD3F4\uB354 \uACBD\uB85C\uB97C \uD488\uB294\uB2E4. Obsidian \uC5D0\uC11C \uC62E\uAE30\uBA74 \uB2E4\uC74C push \uAC00 Notion \uC744 \uB9DE\uCD98\uB2E4";
18731
+ }
18732
+ return null;
18733
+ }
18734
+ holdsDatabaseOrConnection(folder) {
18735
+ const databases = parseDiscoveredDbs(this.stateDb.getMeta(DISCOVERED_DBS_META_KEY));
18736
+ if (databases.some((db) => within(db.localFolder.replace(/\/+$/, ""), folder))) return true;
18737
+ const baseFiles = this.stateDb.getMeta(DB_BASE_FILES_META_KEY);
18738
+ if (baseFiles) {
18739
+ try {
18740
+ const entries = Object.values(
18741
+ JSON.parse(baseFiles)
18742
+ );
18743
+ if (entries.some((entry) => entry.basePath && within(entry.basePath, folder))) return true;
18744
+ } catch {
18745
+ }
18746
+ }
18747
+ if (this.stateDb.getAll().some((r) => r.fileType === "db-row" && within(r.obsidianPath, folder))) {
18748
+ return true;
18749
+ }
18750
+ return this.vaultFs.holdsOtherConnection?.(folder) ?? false;
18751
+ }
18752
+ /** 옮긴 것 아래의 레코드 · 위키링크 · 보존 마커 · 첨부 기록을 새 경로로 옮겨 적는다. */
18753
+ rewritePaths(move) {
18754
+ for (const record of this.stateDb.getAll()) {
18755
+ if (!within(record.obsidianPath, move.from)) continue;
18756
+ const from = record.obsidianPath;
18757
+ const to = move.to + from.slice(move.from.length);
18758
+ this.stateDb.updatePath(record.id, to);
18759
+ const entry = record.notionPageId ? this.stateDb.resolvePageId(record.notionPageId) : null;
18760
+ if (entry && entry.obsidianPath === from) {
18761
+ this.stateDb.deleteWikilink(from);
18762
+ this.stateDb.upsertWikilink({
18763
+ obsidianPath: to,
18764
+ notionPageId: entry.notionPageId,
18765
+ title: wikilinkTitleFromPath(to),
18766
+ aliases: entry.aliases
18767
+ });
18768
+ }
18769
+ const markers = this.stateDb.getPreserveMarkers(from);
18770
+ if (markers.length > 0) {
18771
+ this.stateDb.storePreserveMarkers(to, markers);
18772
+ this.stateDb.storePreserveMarkers(from, []);
18773
+ }
18774
+ if (record.notionPageId) {
18775
+ for (const file of this.stateDb.getFilesByPageId(record.notionPageId)) {
18776
+ if (!within(file.localPath, move.from)) continue;
18777
+ this.stateDb.registerFile({
18778
+ localPath: move.to + file.localPath.slice(move.from.length),
18779
+ notionPageId: file.notionPageId,
18780
+ fileUploadId: file.fileUploadId,
18781
+ fileType: file.fileType,
18782
+ fileHash: file.fileHash,
18783
+ fileSize: file.fileSize
18784
+ });
18785
+ this.stateDb.deleteFileRegistry(file.localPath);
18786
+ }
18787
+ }
18788
+ }
18789
+ }
18790
+ };
18791
+ function within(path, unit) {
18792
+ return path === unit || path.startsWith(`${unit}/`);
18793
+ }
18794
+ function baseName2(path) {
18795
+ return path.slice(path.lastIndexOf("/") + 1);
18796
+ }
18797
+ function joinPath(folder, name) {
18798
+ return folder === "" ? name : `${folder}/${name}`;
18799
+ }
18800
+
18590
18801
  // src/sync/change-inspector.ts
18591
18802
  var ChangeInspector = class {
18592
18803
  constructor(stateDb, vaultFs, notionClient, planner, puller) {
@@ -21660,17 +21871,7 @@ var PagePuller = class {
21660
21871
  const parentId = await extractParentId(this.notionClient, page);
21661
21872
  if (!parentId || notionIdsEqual(parentId, this.config.notion.rootPageId)) return "";
21662
21873
  const parentRecord = this.stateDb.getByNotionId(parentId);
21663
- if (parentRecord) {
21664
- if (isFolderRecord(parentRecord) && !this.placement.folderLookup().databaseAt(parentRecord.obsidianPath)) {
21665
- return parentRecord.obsidianPath;
21666
- }
21667
- if (parentRecord.fileType === "folder-note" || parentRecord.fileType === "folder-only") {
21668
- const pathParts = parentRecord.obsidianPath.split("/");
21669
- pathParts.pop();
21670
- return pathParts.join("/");
21671
- }
21672
- return stripMarkdownExtension(parentRecord.obsidianPath);
21673
- }
21874
+ if (parentRecord) return childFolderOf(parentRecord, this.placement.folderLookup());
21674
21875
  try {
21675
21876
  const parentPage = await this.notionClient.getPage(parentId);
21676
21877
  const parentTitle = this.notionClient.extractTitle(parentPage);
@@ -22048,6 +22249,9 @@ var RemoteDetector = class _RemoteDetector {
22048
22249
  // 감지가 빠뜨린 자식을 {@link takeUndiscoveredChildren} 가 둘의 차로 고른다.
22049
22250
  _bodyChildren = /* @__PURE__ */ new Map();
22050
22251
  _queued = /* @__PURE__ */ new Set();
22252
+ // Notion 에서 부모가 바뀐 추적 페이지(정규화 id → 새 부모 페이지 id) — pull 이 볼트에서도 옮긴다(ADR-039).
22253
+ // 옮겨도 수정 시각이 바뀌지 않아(실측) 변경으로는 보이지 않는다. 감지마다 다시 채운다.
22254
+ _moved = /* @__PURE__ */ new Map();
22051
22255
  /** 이번 감지에서 자식 페이지를 가진 것으로 본 페이지인가 — 폴더노트 판정의 1차 근거. */
22052
22256
  hasChildPages(pageId) {
22053
22257
  return this._childParentIds.has(normalizeNotionId(pageId));
@@ -22109,6 +22313,16 @@ var RemoteDetector = class _RemoteDetector {
22109
22313
  }
22110
22314
  return changes;
22111
22315
  }
22316
+ /** 이번 감지가 본, Notion 에서 옮긴 추적 페이지 — 페이지 id → 새 부모 페이지 id. */
22317
+ get movedPages() {
22318
+ return this._moved;
22319
+ }
22320
+ /** 받은 부모가 상태 DB 의 것과 다르면 옮긴 페이지로 적는다. */
22321
+ noteParent(pageId, parentId) {
22322
+ const record = this.stateDb.getByNotionId(pageId);
22323
+ if (record && movedInNotion(record, parentId))
22324
+ this._moved.set(normalizeNotionId(pageId), parentId);
22325
+ }
22112
22326
  /** 새 감지를 시작한다 — 지난 실행의 본문 자식과 받기로 한 페이지를 비우고 이번 변경을 적는다. */
22113
22327
  beginDetection(changes) {
22114
22328
  this._bodyChildren.clear();
@@ -22155,6 +22369,7 @@ var RemoteDetector = class _RemoteDetector {
22155
22369
  const changes = [];
22156
22370
  this._childParentIds.clear();
22157
22371
  this._pages.clear();
22372
+ this._moved.clear();
22158
22373
  const lastPull = this.stateDb.getMeta(LAST_PULL_META_KEY);
22159
22374
  const syncedRecords = this.stateDb.getAll();
22160
22375
  const trackedPageIds = new Set(
@@ -22212,7 +22427,10 @@ var RemoteDetector = class _RemoteDetector {
22212
22427
  for (const page of underRoot) {
22213
22428
  throwIfAborted(signal);
22214
22429
  const parentId = await extractParentId(this.notionClient, page);
22215
- if (parentId) this._childParentIds.add(normalizeNotionId(parentId));
22430
+ if (parentId) {
22431
+ this._childParentIds.add(normalizeNotionId(parentId));
22432
+ this.noteParent(page.id, parentId);
22433
+ }
22216
22434
  }
22217
22435
  for (const page of underRoot) {
22218
22436
  titles.set(normalizeNotionId(page.id), this.notionClient.extractTitle(page));
@@ -22297,6 +22515,7 @@ var RemoteDetector = class _RemoteDetector {
22297
22515
  const databaseIds = /* @__PURE__ */ new Set();
22298
22516
  this._childParentIds.clear();
22299
22517
  this._pages.clear();
22518
+ this._moved.clear();
22300
22519
  const searchSince = incrementalSearchSince(since);
22301
22520
  const recentPages = await this.notionClient.searchRecentPages(searchSince, options.signal);
22302
22521
  const untracked = [];
@@ -22319,6 +22538,7 @@ var RemoteDetector = class _RemoteDetector {
22319
22538
  }
22320
22539
  continue;
22321
22540
  }
22541
+ if (page.parent.type === "page_id") this.noteParent(page.id, page.parent.page_id);
22322
22542
  const modified = this.observation.modification(record, page);
22323
22543
  const rowDatabase = page.parentDatabaseId ?? (record.fileType === "db-row" ? record.notionParentId : null);
22324
22544
  if (modified && rowDatabase) databaseIds.add(compactNotionId(rowDatabase));
@@ -22888,6 +23108,12 @@ var SyncOrchestrator = class {
22888
23108
  );
22889
23109
  this.placement.useDatabaseCreator(this.databaseCreator);
22890
23110
  this.planner = new LocalPlanner(config, stateDb, vaultFs, changeDetector, this.placement);
23111
+ this.remoteMoves = new RemoteMoveFollower(
23112
+ config,
23113
+ stateDb,
23114
+ vaultFs,
23115
+ () => this.placement.folderLookup()
23116
+ );
22891
23117
  this.puller = new PagePuller(
22892
23118
  config,
22893
23119
  stateDb,
@@ -22955,6 +23181,7 @@ var SyncOrchestrator = class {
22955
23181
  databaseCreator;
22956
23182
  // 이번 실행의 로컬 변경 — 옮긴 노트 · 폴더를 짝짓고 상태 DB 에 옮겨 적는다(S-11).
22957
23183
  planner;
23184
+ remoteMoves;
22958
23185
  // 원격에서 바뀐 것 — 전체 대조 · 증분 감지(ADR-027).
22959
23186
  detector;
22960
23187
  // 지난번에 본 뒤로 원격이 바뀌었나(N-05) — 덮어쓰기 · 지우기 전에, 변경을 보이기 전에 본다.
@@ -23299,6 +23526,17 @@ var SyncOrchestrator = class {
23299
23526
  async pull(options) {
23300
23527
  return this.gate.run("pull", () => this.executePull(options));
23301
23528
  }
23529
+ /** 감지가 본, Notion 에서 옮긴 페이지를 볼트에서 옮긴다 — 경로를 좁힌 pull 은 그 범위의 것만. */
23530
+ async followRemoteMoves(options) {
23531
+ const moved = new Map(
23532
+ [...this.detector.movedPages].filter(([pageId]) => {
23533
+ const record = this.stateDb.getByNotionId(pageId);
23534
+ return !!record && (!options?.paths || inAnyPathScope(record.obsidianPath, options.paths));
23535
+ })
23536
+ );
23537
+ if (moved.size === 0) return { moved: [], skipped: [] };
23538
+ return await this.remoteMoves.follow(moved, { dryRun: options?.dryRun });
23539
+ }
23302
23540
  async executePull(options) {
23303
23541
  const startTime = Date.now();
23304
23542
  this.assertConnectionScope();
@@ -23358,6 +23596,7 @@ var SyncOrchestrator = class {
23358
23596
  }
23359
23597
  const remoteChanges = detection.changes;
23360
23598
  detected = remoteChanges;
23599
+ const remoteMoves = await this.followRemoteMoves(options);
23361
23600
  const pendingDatabases = parsePendingDatabases(
23362
23601
  this.stateDb.getMeta(PENDING_DATABASES_META_KEY)
23363
23602
  );
@@ -23407,7 +23646,9 @@ var SyncOrchestrator = class {
23407
23646
  this.config.sync.deleteSync
23408
23647
  ),
23409
23648
  skippedDatabases: ledger.skipped
23410
- }
23649
+ },
23650
+ moved: remoteMoves.moved,
23651
+ moveSkipped: remoteMoves.skipped
23411
23652
  };
23412
23653
  };
23413
23654
  const excludes = (path) => this.vaultFs.excludesPath?.(path) ?? false;
@@ -23438,7 +23679,7 @@ var SyncOrchestrator = class {
23438
23679
  }) : [];
23439
23680
  const workItems = [...filtered, ...restoreChanges, ...rerenderChanges];
23440
23681
  if (options?.dryRun) {
23441
- return this.pullPlanner.planPull(
23682
+ const plan = await this.pullPlanner.planPull(
23442
23683
  filtered,
23443
23684
  restoreChanges,
23444
23685
  restoreIds,
@@ -23448,6 +23689,7 @@ var SyncOrchestrator = class {
23448
23689
  scan,
23449
23690
  ledger
23450
23691
  );
23692
+ return { ...plan, moved: remoteMoves.moved, moveSkipped: remoteMoves.skipped };
23451
23693
  }
23452
23694
  let pullTotal = workItems.length;
23453
23695
  let pullCompleted = 0;