feishu-md-sync 0.1.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.
Files changed (74) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +5 -0
  3. package/README.md +107 -0
  4. package/dist/adapters/feishu-adapter.d.ts +44 -0
  5. package/dist/adapters/feishu-adapter.js +1 -0
  6. package/dist/adapters/lark-cli-adapter.d.ts +45 -0
  7. package/dist/adapters/lark-cli-adapter.js +229 -0
  8. package/dist/cli/commands/core.d.ts +2 -0
  9. package/dist/cli/commands/core.js +135 -0
  10. package/dist/cli/commands/publish.d.ts +2 -0
  11. package/dist/cli/commands/publish.js +63 -0
  12. package/dist/cli/env.d.ts +27 -0
  13. package/dist/cli/env.js +113 -0
  14. package/dist/cli/index.d.ts +2 -0
  15. package/dist/cli/index.js +52 -0
  16. package/dist/cli/output.d.ts +4 -0
  17. package/dist/cli/output.js +18 -0
  18. package/dist/config/sync-config.d.ts +13 -0
  19. package/dist/config/sync-config.js +60 -0
  20. package/dist/core/diff.d.ts +1 -0
  21. package/dist/core/diff.js +20 -0
  22. package/dist/core/doc-id.d.ts +12 -0
  23. package/dist/core/doc-id.js +41 -0
  24. package/dist/core/hash.d.ts +6 -0
  25. package/dist/core/hash.js +84 -0
  26. package/dist/core/markdown-canonical.d.ts +2 -0
  27. package/dist/core/markdown-canonical.js +11 -0
  28. package/dist/diff/run-diff.d.ts +23 -0
  29. package/dist/diff/run-diff.js +33 -0
  30. package/dist/feishu/types.d.ts +101 -0
  31. package/dist/feishu/types.js +1 -0
  32. package/dist/markdown/blocks.d.ts +5 -0
  33. package/dist/markdown/blocks.js +305 -0
  34. package/dist/markdown/from-blocks.d.ts +2 -0
  35. package/dist/markdown/from-blocks.js +110 -0
  36. package/dist/markdown/links.d.ts +3 -0
  37. package/dist/markdown/links.js +44 -0
  38. package/dist/merge/line-merge.d.ts +21 -0
  39. package/dist/merge/line-merge.js +270 -0
  40. package/dist/merge/merge-state.d.ts +33 -0
  41. package/dist/merge/merge-state.js +44 -0
  42. package/dist/merge/run-merge.d.ts +38 -0
  43. package/dist/merge/run-merge.js +139 -0
  44. package/dist/profiles/publish-profile.d.ts +9 -0
  45. package/dist/profiles/publish-profile.js +9 -0
  46. package/dist/publish/block-patch-plan.d.ts +34 -0
  47. package/dist/publish/block-patch-plan.js +223 -0
  48. package/dist/publish/block-state.d.ts +8 -0
  49. package/dist/publish/block-state.js +82 -0
  50. package/dist/publish/block-update.d.ts +4 -0
  51. package/dist/publish/block-update.js +40 -0
  52. package/dist/publish/profile-transform.d.ts +5 -0
  53. package/dist/publish/profile-transform.js +6 -0
  54. package/dist/publish/publish-plan.d.ts +35 -0
  55. package/dist/publish/publish-plan.js +113 -0
  56. package/dist/publish/run-publish.d.ts +25 -0
  57. package/dist/publish/run-publish.js +299 -0
  58. package/dist/publish/title.d.ts +9 -0
  59. package/dist/publish/title.js +20 -0
  60. package/dist/pull/run-pull.d.ts +23 -0
  61. package/dist/pull/run-pull.js +57 -0
  62. package/dist/receipts/publish-receipt.d.ts +49 -0
  63. package/dist/receipts/publish-receipt.js +52 -0
  64. package/dist/receipts/pull-receipt.d.ts +31 -0
  65. package/dist/receipts/pull-receipt.js +30 -0
  66. package/dist/status/run-status.d.ts +56 -0
  67. package/dist/status/run-status.js +131 -0
  68. package/dist/transform/include-tags.d.ts +6 -0
  69. package/dist/transform/include-tags.js +22 -0
  70. package/dist/transform/zilliz-publish.d.ts +5 -0
  71. package/dist/transform/zilliz-publish.js +42 -0
  72. package/dist/transform/zilliz-pull.d.ts +6 -0
  73. package/dist/transform/zilliz-pull.js +34 -0
  74. package/package.json +61 -0
@@ -0,0 +1,223 @@
1
+ import { sha256, stableStringify } from '../core/hash.js';
2
+ import { feishuBlocksToMarkdown } from '../markdown/from-blocks.js';
3
+ import { isTextLikeBlockPairUpdateable } from './block-update.js';
4
+ export function planPublishBlockPatch(input) {
5
+ const state = {
6
+ operations: [],
7
+ warnings: []
8
+ };
9
+ const fallbackReason = planSequence({
10
+ parentBlockId: input.parentBlockId,
11
+ remoteBlocks: input.remoteBlocks,
12
+ desiredBlocks: input.desiredBlocks,
13
+ path: [],
14
+ state
15
+ });
16
+ const requiresCollaborationRiskConfirmation = state.operations.some((operation) => {
17
+ return operation.kind === 'update' || operation.kind === 'delete';
18
+ });
19
+ return {
20
+ kind: 'publish-block-patch-plan',
21
+ safeToWrite: fallbackReason === undefined,
22
+ requiresCollaborationRiskConfirmation,
23
+ operations: fallbackReason ? [] : state.operations,
24
+ fallbackReason,
25
+ warnings: state.warnings
26
+ };
27
+ }
28
+ function planSequence(input) {
29
+ if (blocksEquivalent(input.remoteBlocks, input.desiredBlocks))
30
+ return undefined;
31
+ const prefixLength = commonPrefixLength(input.remoteBlocks, input.desiredBlocks);
32
+ const suffixLength = commonSuffixLength(input.remoteBlocks, input.desiredBlocks, prefixLength);
33
+ const remoteMiddleEnd = input.remoteBlocks.length - suffixLength;
34
+ const desiredMiddleEnd = input.desiredBlocks.length - suffixLength;
35
+ const remoteMiddle = input.remoteBlocks.slice(prefixLength, remoteMiddleEnd);
36
+ const desiredMiddle = input.desiredBlocks.slice(prefixLength, desiredMiddleEnd);
37
+ if (remoteMiddle.length === 0 && desiredMiddle.length > 0) {
38
+ const unsupported = desiredMiddle.find((block) => !isWritableMarkdownBlockForPatch(block));
39
+ if (unsupported) {
40
+ return `create block_type ${unsupported.block_type} is unsupported at ${formatPath([...input.path, prefixLength])}`;
41
+ }
42
+ const insertAfterBlockId = insertAfterBlockIdForCreate(input.remoteBlocks, input.parentBlockId, prefixLength);
43
+ if (!insertAfterBlockId) {
44
+ return `create anchor is missing at ${formatPath([...input.path, prefixLength])}`;
45
+ }
46
+ input.state.operations.push({
47
+ kind: 'create',
48
+ parentBlockId: input.parentBlockId,
49
+ insertAfterBlockId,
50
+ index: prefixLength,
51
+ path: [...input.path, prefixLength],
52
+ blocks: desiredMiddle
53
+ });
54
+ return undefined;
55
+ }
56
+ if (remoteMiddle.length > 0 && desiredMiddle.length === 0) {
57
+ const unsupported = remoteMiddle.find((block) => !isWritableMarkdownBlockForPatch(block));
58
+ if (unsupported) {
59
+ return `delete block_type ${unsupported.block_type} is unsupported at ${formatPath([...input.path, prefixLength])}`;
60
+ }
61
+ const blockIds = blockIdsForDelete(remoteMiddle);
62
+ if (blockIds.length !== remoteMiddle.length) {
63
+ return `delete block id is missing at ${formatPath([...input.path, prefixLength])}`;
64
+ }
65
+ input.state.operations.push({
66
+ kind: 'delete',
67
+ parentBlockId: input.parentBlockId,
68
+ blockIds,
69
+ startIndex: prefixLength,
70
+ endIndex: remoteMiddleEnd,
71
+ path: [...input.path, prefixLength]
72
+ });
73
+ return undefined;
74
+ }
75
+ if (remoteMiddle.length !== desiredMiddle.length) {
76
+ return `block order or count changed at ${formatPath(input.path)}`;
77
+ }
78
+ for (let index = 0; index < remoteMiddle.length; index += 1) {
79
+ const remote = remoteMiddle[index];
80
+ const desired = desiredMiddle[index];
81
+ const absoluteIndex = prefixLength + index;
82
+ const childPath = [...input.path, absoluteIndex];
83
+ const fallbackReason = planBlockPair({
84
+ remote,
85
+ desired,
86
+ path: childPath,
87
+ state: input.state
88
+ });
89
+ if (fallbackReason)
90
+ return fallbackReason;
91
+ }
92
+ return undefined;
93
+ }
94
+ function planBlockPair(input) {
95
+ if (blocksEquivalent([input.remote], [input.desired]))
96
+ return undefined;
97
+ if (isWhiteboardBlock(input.remote) || isWhiteboardBlock(input.desired)) {
98
+ return `whiteboard block changed at ${formatPath(input.path)}`;
99
+ }
100
+ if (hasBlockChildren(input.remote) || hasBlockChildren(input.desired)) {
101
+ if (!input.remote.block_id)
102
+ return `container block is missing block id at ${formatPath(input.path)}`;
103
+ if (input.remote.block_type !== input.desired.block_type) {
104
+ return `container block type changed at ${formatPath(input.path)}`;
105
+ }
106
+ if (!hasBlockChildren(input.remote) || !hasBlockChildren(input.desired)) {
107
+ return `container block structure changed at ${formatPath(input.path)}`;
108
+ }
109
+ if (!containerShellEquivalent(input.remote, input.desired)) {
110
+ return `container block shell changed at ${formatPath(input.path)}`;
111
+ }
112
+ return planSequence({
113
+ parentBlockId: input.remote.block_id,
114
+ remoteBlocks: input.remote.children,
115
+ desiredBlocks: input.desired.children,
116
+ path: input.path,
117
+ state: input.state
118
+ });
119
+ }
120
+ if (!isTextLikeBlockPairUpdateable(input.remote, input.desired)) {
121
+ return `unsupported block change at ${formatPath(input.path)}`;
122
+ }
123
+ input.state.operations.push({
124
+ kind: 'update',
125
+ remoteBlockId: input.remote.block_id,
126
+ path: input.path,
127
+ blockType: input.remote.block_type
128
+ });
129
+ return undefined;
130
+ }
131
+ function commonPrefixLength(remote, desired) {
132
+ const maxLength = Math.min(remote.length, desired.length);
133
+ for (let index = 0; index < maxLength; index += 1) {
134
+ if (!blocksEquivalent([remote[index]], [desired[index]]))
135
+ return index;
136
+ }
137
+ return maxLength;
138
+ }
139
+ function commonSuffixLength(remote, desired, prefixLength) {
140
+ const maxLength = Math.min(remote.length, desired.length) - prefixLength;
141
+ for (let offset = 0; offset < maxLength; offset += 1) {
142
+ const remoteIndex = remote.length - 1 - offset;
143
+ const desiredIndex = desired.length - 1 - offset;
144
+ if (!blocksEquivalent([remote[remoteIndex]], [desired[desiredIndex]]))
145
+ return offset;
146
+ }
147
+ return maxLength;
148
+ }
149
+ function blocksEquivalent(remote, desired) {
150
+ return hashComparableBlocks(remote) === hashComparableBlocks(desired) ||
151
+ (canUseCanonicalMarkdown(remote) && canUseCanonicalMarkdown(desired) && canonicalMarkdown(remote) === canonicalMarkdown(desired));
152
+ }
153
+ function containerShellEquivalent(remote, desired) {
154
+ return hashComparableBlocks([stripContainerChildren(remote)]) === hashComparableBlocks([stripContainerChildren(desired)]);
155
+ }
156
+ function hashComparableBlocks(blocks) {
157
+ return sha256(stableStringify(blocks.map(normalizeBlockForPatch)));
158
+ }
159
+ function normalizeBlockForPatch(value) {
160
+ if (Array.isArray(value)) {
161
+ return value.map(normalizeBlockForPatch);
162
+ }
163
+ if (value && typeof value === 'object') {
164
+ const normalized = {};
165
+ for (const [key, child] of Object.entries(value)) {
166
+ if (key === 'block_id')
167
+ continue;
168
+ if (key === 'parent_id')
169
+ continue;
170
+ if (key === 'merge_info')
171
+ continue;
172
+ normalized[key] = normalizeBlockForPatch(child);
173
+ }
174
+ return normalized;
175
+ }
176
+ return value;
177
+ }
178
+ function canonicalMarkdown(blocks) {
179
+ return feishuBlocksToMarkdown(blocks)
180
+ .replace(/\r\n/g, '\n')
181
+ .replace(/[ \t]+$/gm, '')
182
+ .replace(/\n{3,}/g, '\n\n')
183
+ .trim();
184
+ }
185
+ function canUseCanonicalMarkdown(blocks) {
186
+ return blocks.every((block) => isWritableMarkdownBlockForPatch(block) && block.block_type !== 31);
187
+ }
188
+ function stripContainerChildren(block) {
189
+ const { children: _children, block_id: _blockId, parent_id: _parentId, ...rest } = block;
190
+ return rest;
191
+ }
192
+ function hasBlockChildren(block) {
193
+ return Array.isArray(block.children) && block.children.every(isFeishuBlock);
194
+ }
195
+ function isFeishuBlock(value) {
196
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value) && 'block_type' in value);
197
+ }
198
+ function isWhiteboardBlock(block) {
199
+ return 'whiteboard' in block;
200
+ }
201
+ function insertAfterBlockIdForCreate(remoteBlocks, parentBlockId, index) {
202
+ if (index === 0)
203
+ return parentBlockId;
204
+ const previous = remoteBlocks[index - 1];
205
+ return previous?.block_id;
206
+ }
207
+ function blockIdsForDelete(blocks) {
208
+ return blocks.flatMap((block) => block.block_id ? [block.block_id] : []);
209
+ }
210
+ function isWritableMarkdownBlockForPatch(block) {
211
+ const writableType = block.block_type === 2 || (block.block_type >= 3 && block.block_type <= 8) || block.block_type === 12 || block.block_type === 13 || block.block_type === 14 || block.block_type === 31;
212
+ if (!writableType)
213
+ return false;
214
+ if (Array.isArray(block.children) && block.children.length > 0)
215
+ return false;
216
+ if (block.block_type !== 31)
217
+ return true;
218
+ const cells = block.table?.cells ?? [];
219
+ return cells.every((cell) => isFeishuBlock(cell) && cell.block_type === 2 && (!Array.isArray(cell.children) || cell.children.length === 0));
220
+ }
221
+ function formatPath(path) {
222
+ return path.length === 0 ? '<root>' : path.join('.');
223
+ }
@@ -0,0 +1,8 @@
1
+ import type { FeishuBlock } from '../feishu/types.js';
2
+ export type PageBlock = FeishuBlock & {
3
+ block_id: string;
4
+ };
5
+ export declare function findPageBlock(blocks: FeishuBlock[], documentId: string): PageBlock;
6
+ export declare function directChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
7
+ export declare function comparableDirectChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
8
+ export declare function renderableDirectChildBlocks(blocks: FeishuBlock[], pageBlock: FeishuBlock): FeishuBlock[];
@@ -0,0 +1,82 @@
1
+ export function findPageBlock(blocks, documentId) {
2
+ const page = blocks.find((block) => block.block_type === 1) ?? blocks.find((block) => block.block_id === documentId);
3
+ if (!page?.block_id) {
4
+ throw new Error(`Could not find page block for document ${documentId}.`);
5
+ }
6
+ return page;
7
+ }
8
+ export function directChildBlocks(blocks, pageBlock) {
9
+ const byId = blockMapById(blocks);
10
+ const children = Array.isArray(pageBlock.children) ? pageBlock.children : [];
11
+ return children
12
+ .map((child) => (typeof child === 'string' ? byId.get(child) : child))
13
+ .filter((child) => Boolean(child));
14
+ }
15
+ export function comparableDirectChildBlocks(blocks, pageBlock) {
16
+ const byId = blockMapById(blocks);
17
+ return directChildBlocks(blocks, pageBlock).map((block) => comparableBlock(block, byId));
18
+ }
19
+ export function renderableDirectChildBlocks(blocks, pageBlock) {
20
+ const byId = blockMapById(blocks);
21
+ return directChildBlocks(blocks, pageBlock).flatMap((block) => renderableBlocks(block, byId));
22
+ }
23
+ function blockMapById(blocks) {
24
+ return new Map(blocks.flatMap((block) => block.block_id ? [[block.block_id, block]] : []));
25
+ }
26
+ function renderableBlocks(block, byId) {
27
+ if (block.block_type === 19 && Array.isArray(block.children)) {
28
+ return [resolveChildContainer(block, byId)];
29
+ }
30
+ if (block.block_type !== 49 || !Array.isArray(block.children)) {
31
+ return [comparableBlock(block, byId)];
32
+ }
33
+ return block.children.flatMap((childRef) => {
34
+ const child = typeof childRef === 'string' ? byId.get(childRef) : asBlock(childRef);
35
+ return child ? renderableBlocks(child, byId) : [];
36
+ });
37
+ }
38
+ function resolveChildContainer(block, byId) {
39
+ const children = Array.isArray(block.children)
40
+ ? block.children
41
+ .map((childRef) => (typeof childRef === 'string' ? byId.get(childRef) : asBlock(childRef)))
42
+ .filter((child) => Boolean(child))
43
+ .map((child) => comparableBlock(child, byId))
44
+ : [];
45
+ return {
46
+ ...block,
47
+ children
48
+ };
49
+ }
50
+ function comparableBlock(block, byId) {
51
+ if (block.block_type === 49 && Array.isArray(block.children)) {
52
+ return resolveChildContainer(block, byId);
53
+ }
54
+ if (block.block_type !== 31 || !isTableBlock(block)) {
55
+ return block;
56
+ }
57
+ const cellRefs = block.table.cells ?? [];
58
+ const resolvedCells = cellRefs.map((cellRef) => {
59
+ const cellBlock = typeof cellRef === 'string' ? byId.get(cellRef) : asBlock(cellRef);
60
+ const firstChildRef = Array.isArray(cellBlock?.children) ? cellBlock.children[0] : undefined;
61
+ const firstChild = typeof firstChildRef === 'string' ? byId.get(firstChildRef) : asBlock(firstChildRef);
62
+ return firstChild ?? { block_type: 2, text: { elements: [], style: { align: 1 } } };
63
+ });
64
+ return {
65
+ ...block,
66
+ table: {
67
+ ...block.table,
68
+ cells: resolvedCells
69
+ }
70
+ };
71
+ }
72
+ function isTableBlock(block) {
73
+ return Boolean(block.table &&
74
+ typeof block.table === 'object' &&
75
+ !Array.isArray(block.table));
76
+ }
77
+ function asBlock(value) {
78
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'block_type' in value) {
79
+ return value;
80
+ }
81
+ return undefined;
82
+ }
@@ -0,0 +1,4 @@
1
+ import type { FeishuBlock, FeishuBlockUpdateRequest, TextElement } from '../feishu/types.js';
2
+ export declare function isTextLikeBlockPairUpdateable(remote: FeishuBlock, desired: FeishuBlock): boolean;
3
+ export declare function buildTextLikeBlockUpdateRequest(remote: FeishuBlock, desired: FeishuBlock): FeishuBlockUpdateRequest;
4
+ export declare function elementsForBlock(block: FeishuBlock): TextElement[] | null;
@@ -0,0 +1,40 @@
1
+ const TEXT_LIKE_KEYS = {
2
+ 2: 'text',
3
+ 3: 'heading1',
4
+ 4: 'heading2',
5
+ 5: 'heading3',
6
+ 6: 'heading4',
7
+ 7: 'heading5',
8
+ 8: 'heading6',
9
+ 12: 'bullet',
10
+ 13: 'ordered',
11
+ 14: 'code'
12
+ };
13
+ export function isTextLikeBlockPairUpdateable(remote, desired) {
14
+ if (!remote.block_id)
15
+ return false;
16
+ if (remote.block_type !== desired.block_type)
17
+ return false;
18
+ return Boolean(TEXT_LIKE_KEYS[remote.block_type] && elementsForBlock(desired));
19
+ }
20
+ export function buildTextLikeBlockUpdateRequest(remote, desired) {
21
+ if (!remote.block_id || !isTextLikeBlockPairUpdateable(remote, desired)) {
22
+ throw new Error(`Block ${remote.block_id ?? '<missing id>'} cannot be updated in place.`);
23
+ }
24
+ return {
25
+ block_id: remote.block_id,
26
+ update_text_elements: {
27
+ elements: elementsForBlock(desired) ?? []
28
+ }
29
+ };
30
+ }
31
+ export function elementsForBlock(block) {
32
+ const key = TEXT_LIKE_KEYS[block.block_type];
33
+ if (!key)
34
+ return null;
35
+ const value = block[key];
36
+ if (!value || typeof value !== 'object' || Array.isArray(value))
37
+ return null;
38
+ const elements = value.elements;
39
+ return Array.isArray(elements) ? elements : null;
40
+ }
@@ -0,0 +1,5 @@
1
+ import type { PublishProfileName } from '../profiles/publish-profile.js';
2
+ export declare function applyPublishTransformForProfile(markdown: string, profile: PublishProfileName): {
3
+ markdown: string;
4
+ warnings: string[];
5
+ };
@@ -0,0 +1,6 @@
1
+ import { applyZillizPublishTransform } from '../transform/zilliz-publish.js';
2
+ export function applyPublishTransformForProfile(markdown, profile) {
3
+ if (profile === 'zilliz')
4
+ return applyZillizPublishTransform(markdown);
5
+ return { markdown, warnings: [] };
6
+ }
@@ -0,0 +1,35 @@
1
+ import type { PublishProfileName } from '../profiles/publish-profile.js';
2
+ import { type PublishReceipt, type PublishReceiptTarget } from '../receipts/publish-receipt.js';
3
+ import type { PublishBlockPatchPlan } from './block-patch-plan.js';
4
+ export type PublishStrategy = 'no-op' | 'block-patch' | 'section-replace' | 'document-replace' | 'create-document';
5
+ export type PublishPlan = {
6
+ target: PublishReceiptTarget;
7
+ profile: PublishProfileName;
8
+ strategy: PublishStrategy;
9
+ safeToWrite: boolean;
10
+ remoteChanged: boolean;
11
+ localSourceHash: string;
12
+ publishDraftHash: string;
13
+ remoteSnapshotHash: string;
14
+ requiresCollaborationRiskConfirmation: boolean;
15
+ requiresUntrackedRemoteConfirmation: boolean;
16
+ blockPatch?: {
17
+ operations: PublishBlockPatchPlan['operations'];
18
+ requiresCollaborationRiskConfirmation: boolean;
19
+ fallbackReason?: string;
20
+ warnings: string[];
21
+ };
22
+ risks: string[];
23
+ warnings: string[];
24
+ };
25
+ export declare function buildPublishPlan(input: {
26
+ target: PublishReceiptTarget;
27
+ profile: PublishProfileName;
28
+ localSource: string;
29
+ publishDraft: string;
30
+ remoteMarkdown: string;
31
+ receipt: PublishReceipt | undefined;
32
+ transformWarnings: string[];
33
+ createDocument?: boolean;
34
+ blockPatch?: PublishBlockPatchPlan;
35
+ }): PublishPlan;
@@ -0,0 +1,113 @@
1
+ import { hashText } from '../receipts/publish-receipt.js';
2
+ export function buildPublishPlan(input) {
3
+ const localSourceHash = hashText(input.localSource);
4
+ const publishDraftHash = hashText(input.publishDraft);
5
+ const remoteSnapshotHash = hashText(input.remoteMarkdown);
6
+ if (input.target.kind === 'folder' || input.createDocument === true) {
7
+ return {
8
+ target: input.target,
9
+ profile: input.profile,
10
+ strategy: 'create-document',
11
+ safeToWrite: true,
12
+ remoteChanged: false,
13
+ localSourceHash,
14
+ publishDraftHash,
15
+ remoteSnapshotHash,
16
+ requiresCollaborationRiskConfirmation: false,
17
+ requiresUntrackedRemoteConfirmation: false,
18
+ risks: [],
19
+ warnings: input.transformWarnings
20
+ };
21
+ }
22
+ const remoteChanged = input.receipt ? input.receipt.remoteSnapshotHash !== remoteSnapshotHash : false;
23
+ const risks = [];
24
+ if (!input.receipt)
25
+ risks.push('untracked remote: no publish receipt exists for this target');
26
+ if (remoteChanged)
27
+ risks.push('remote changed since last publish receipt');
28
+ if (publishDraftHash === remoteSnapshotHash) {
29
+ return {
30
+ target: input.target,
31
+ profile: input.profile,
32
+ strategy: 'no-op',
33
+ safeToWrite: true,
34
+ remoteChanged,
35
+ localSourceHash,
36
+ publishDraftHash,
37
+ remoteSnapshotHash,
38
+ requiresCollaborationRiskConfirmation: false,
39
+ requiresUntrackedRemoteConfirmation: false,
40
+ risks,
41
+ warnings: input.transformWarnings
42
+ };
43
+ }
44
+ if (input.blockPatch?.safeToWrite === true && !remoteChanged) {
45
+ if (input.blockPatch.operations.length === 0) {
46
+ return {
47
+ target: input.target,
48
+ profile: input.profile,
49
+ strategy: 'no-op',
50
+ safeToWrite: true,
51
+ remoteChanged,
52
+ localSourceHash,
53
+ publishDraftHash,
54
+ remoteSnapshotHash,
55
+ requiresCollaborationRiskConfirmation: false,
56
+ requiresUntrackedRemoteConfirmation: false,
57
+ blockPatch: {
58
+ operations: [],
59
+ requiresCollaborationRiskConfirmation: false,
60
+ fallbackReason: input.blockPatch.fallbackReason,
61
+ warnings: input.blockPatch.warnings
62
+ },
63
+ risks,
64
+ warnings: input.transformWarnings
65
+ };
66
+ }
67
+ if (!input.receipt) {
68
+ risks.push('untracked remote block-patch requires explicit confirmation');
69
+ }
70
+ if (input.blockPatch.requiresCollaborationRiskConfirmation) {
71
+ risks.push('changed blocks may lose comments, anchors, or block identity when replaced');
72
+ }
73
+ return {
74
+ target: input.target,
75
+ profile: input.profile,
76
+ strategy: 'block-patch',
77
+ safeToWrite: Boolean(input.receipt) && !input.blockPatch.requiresCollaborationRiskConfirmation,
78
+ remoteChanged,
79
+ localSourceHash,
80
+ publishDraftHash,
81
+ remoteSnapshotHash,
82
+ requiresCollaborationRiskConfirmation: input.blockPatch.requiresCollaborationRiskConfirmation,
83
+ requiresUntrackedRemoteConfirmation: !input.receipt,
84
+ blockPatch: {
85
+ operations: input.blockPatch.operations,
86
+ requiresCollaborationRiskConfirmation: input.blockPatch.requiresCollaborationRiskConfirmation,
87
+ fallbackReason: input.blockPatch.fallbackReason,
88
+ warnings: input.blockPatch.warnings
89
+ },
90
+ risks,
91
+ warnings: input.transformWarnings
92
+ };
93
+ }
94
+ if (input.blockPatch && !input.blockPatch.safeToWrite && input.blockPatch.fallbackReason) {
95
+ risks.push(`block-patch unavailable: ${input.blockPatch.fallbackReason}`);
96
+ }
97
+ const strategy = 'document-replace';
98
+ risks.push('document replace can affect comments, anchors, block identity, and collaboration context');
99
+ return {
100
+ target: input.target,
101
+ profile: input.profile,
102
+ strategy,
103
+ safeToWrite: false,
104
+ remoteChanged,
105
+ localSourceHash,
106
+ publishDraftHash,
107
+ remoteSnapshotHash,
108
+ requiresCollaborationRiskConfirmation: false,
109
+ requiresUntrackedRemoteConfirmation: false,
110
+ risks,
111
+ warnings: input.transformWarnings
112
+ };
113
+ }
@@ -0,0 +1,25 @@
1
+ import type { FeishuAdapter } from '../adapters/feishu-adapter.js';
2
+ import type { PublishProfileName } from '../profiles/publish-profile.js';
3
+ import { type PublishReceiptTarget } from '../receipts/publish-receipt.js';
4
+ import { type PublishPlan, type PublishStrategy } from './publish-plan.js';
5
+ export type RunPublishResult = {
6
+ mode: 'dry-run' | 'write';
7
+ plan: PublishPlan;
8
+ document?: {
9
+ documentId: string;
10
+ url?: string;
11
+ };
12
+ };
13
+ export declare function runPublish(input: {
14
+ cwd: string;
15
+ file: string;
16
+ target: PublishReceiptTarget;
17
+ profile: PublishProfileName;
18
+ write: boolean;
19
+ create: boolean;
20
+ strategy: 'auto' | PublishStrategy;
21
+ confirmDestructive: boolean;
22
+ confirmCollaborationRisk?: boolean;
23
+ confirmUntrackedRemote?: boolean;
24
+ adapter: FeishuAdapter;
25
+ }): Promise<RunPublishResult>;