emdash-emit 0.1.17 → 0.1.25
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/CHANGELOG.md +152 -0
- package/README.md +22 -11
- package/dist/cli/commands/new.d.ts.map +1 -1
- package/dist/cli/commands/new.js +30 -0
- package/dist/cli/commands/new.js.map +1 -1
- package/dist/cli/commands/pull.d.ts +2 -10
- package/dist/cli/commands/pull.d.ts.map +1 -1
- package/dist/cli/commands/pull.js +12 -30
- package/dist/cli/commands/pull.js.map +1 -1
- package/dist/cli/commands/status.d.ts.map +1 -1
- package/dist/cli/commands/status.js +7 -2
- package/dist/cli/commands/status.js.map +1 -1
- package/dist/client/api.d.ts +20 -0
- package/dist/client/api.d.ts.map +1 -1
- package/dist/client/api.js +89 -0
- package/dist/client/api.js.map +1 -1
- package/dist/client/auth.d.ts.map +1 -1
- package/dist/client/auth.js +2 -1
- package/dist/client/auth.js.map +1 -1
- package/dist/client/endpoints.d.ts +19 -0
- package/dist/client/endpoints.d.ts.map +1 -0
- package/dist/client/endpoints.js +69 -0
- package/dist/client/endpoints.js.map +1 -0
- package/dist/client/http.d.ts +4 -2
- package/dist/client/http.d.ts.map +1 -1
- package/dist/client/http.js.map +1 -1
- package/dist/client/lock.d.ts.map +1 -1
- package/dist/client/lock.js +4 -3
- package/dist/client/lock.js.map +1 -1
- package/dist/client/mapper.d.ts.map +1 -1
- package/dist/client/mapper.js +5 -3
- package/dist/client/mapper.js.map +1 -1
- package/dist/client/media-id.d.ts +2 -0
- package/dist/client/media-id.d.ts.map +1 -0
- package/dist/client/media-id.js +23 -0
- package/dist/client/media-id.js.map +1 -0
- package/dist/client/media.d.ts +8 -7
- package/dist/client/media.d.ts.map +1 -1
- package/dist/client/media.js +58 -17
- package/dist/client/media.js.map +1 -1
- package/dist/db/baseline.d.ts +38 -0
- package/dist/db/baseline.d.ts.map +1 -0
- package/dist/db/baseline.js +74 -0
- package/dist/db/baseline.js.map +1 -0
- package/dist/db/cache.d.ts +8 -2
- package/dist/db/cache.d.ts.map +1 -1
- package/dist/db/cache.js +24 -2
- package/dist/db/cache.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/mcp/tools/sync-new.d.ts.map +1 -1
- package/dist/mcp/tools/sync-new.js +42 -1
- package/dist/mcp/tools/sync-new.js.map +1 -1
- package/dist/mcp/tools/sync-pull.d.ts.map +1 -1
- package/dist/mcp/tools/sync-pull.js +15 -2
- package/dist/mcp/tools/sync-pull.js.map +1 -1
- package/dist/mcp/tools/sync-status.d.ts.map +1 -1
- package/dist/mcp/tools/sync-status.js +1 -0
- package/dist/mcp/tools/sync-status.js.map +1 -1
- package/dist/presentation/article_report.d.ts +40 -0
- package/dist/presentation/article_report.d.ts.map +1 -0
- package/dist/presentation/article_report.js +80 -0
- package/dist/presentation/article_report.js.map +1 -0
- package/dist/sync/bylines.d.ts.map +1 -1
- package/dist/sync/bylines.js +2 -1
- package/dist/sync/bylines.js.map +1 -1
- package/dist/sync/conflicts.js +1 -1
- package/dist/sync/conflicts.js.map +1 -1
- package/dist/sync/delete.d.ts +0 -5
- package/dist/sync/delete.d.ts.map +1 -1
- package/dist/sync/delete.js +5 -11
- package/dist/sync/delete.js.map +1 -1
- package/dist/sync/diff.d.ts.map +1 -1
- package/dist/sync/diff.js +13 -9
- package/dist/sync/diff.js.map +1 -1
- package/dist/sync/doctor.d.ts.map +1 -1
- package/dist/sync/doctor.js +11 -14
- package/dist/sync/doctor.js.map +1 -1
- package/dist/sync/guards.d.ts +5 -0
- package/dist/sync/guards.d.ts.map +1 -1
- package/dist/sync/guards.js +62 -1
- package/dist/sync/guards.js.map +1 -1
- package/dist/sync/media/body_scan.d.ts.map +1 -1
- package/dist/sync/media/body_scan.js +9 -3
- package/dist/sync/media/body_scan.js.map +1 -1
- package/dist/sync/media/companions.d.ts +2 -9
- package/dist/sync/media/companions.d.ts.map +1 -1
- package/dist/sync/media/companions.js +5 -23
- package/dist/sync/media/companions.js.map +1 -1
- package/dist/sync/media/mirror_item.d.ts.map +1 -1
- package/dist/sync/media/mirror_item.js +33 -21
- package/dist/sync/media/mirror_item.js.map +1 -1
- package/dist/sync/media/pull_article.d.ts.map +1 -1
- package/dist/sync/media/pull_article.js +3 -2
- package/dist/sync/media/pull_article.js.map +1 -1
- package/dist/sync/media/pull_library.d.ts.map +1 -1
- package/dist/sync/media/pull_library.js +16 -11
- package/dist/sync/media/pull_library.js.map +1 -1
- package/dist/sync/media/remote_index.d.ts.map +1 -1
- package/dist/sync/media/remote_index.js +2 -1
- package/dist/sync/media/remote_index.js.map +1 -1
- package/dist/sync/media/types.d.ts +4 -3
- package/dist/sync/media/types.d.ts.map +1 -1
- package/dist/sync/media/types.js.map +1 -1
- package/dist/sync/merge.d.ts +1 -1
- package/dist/sync/merge.d.ts.map +1 -1
- package/dist/sync/merge.js +50 -17
- package/dist/sync/merge.js.map +1 -1
- package/dist/sync/new.d.ts +11 -0
- package/dist/sync/new.d.ts.map +1 -1
- package/dist/sync/new.js +81 -4
- package/dist/sync/new.js.map +1 -1
- package/dist/sync/preflight.d.ts +10 -2
- package/dist/sync/preflight.d.ts.map +1 -1
- package/dist/sync/preflight.js +119 -17
- package/dist/sync/preflight.js.map +1 -1
- package/dist/sync/pull.d.ts +6 -44
- package/dist/sync/pull.d.ts.map +1 -1
- package/dist/sync/pull.js +154 -207
- package/dist/sync/pull.js.map +1 -1
- package/dist/sync/push/commit.d.ts +61 -0
- package/dist/sync/push/commit.d.ts.map +1 -0
- package/dist/sync/push/commit.js +258 -0
- package/dist/sync/push/commit.js.map +1 -0
- package/dist/sync/push/guards.d.ts +33 -0
- package/dist/sync/push/guards.d.ts.map +1 -0
- package/dist/sync/push/guards.js +110 -0
- package/dist/sync/push/guards.js.map +1 -0
- package/dist/sync/push/index.d.ts +9 -0
- package/dist/sync/push/index.d.ts.map +1 -0
- package/dist/sync/push/index.js +372 -0
- package/dist/sync/push/index.js.map +1 -0
- package/dist/sync/push/media_stage.d.ts +56 -0
- package/dist/sync/push/media_stage.d.ts.map +1 -0
- package/dist/sync/push/media_stage.js +338 -0
- package/dist/sync/push/media_stage.js.map +1 -0
- package/dist/sync/push/merge_stage.d.ts +37 -0
- package/dist/sync/push/merge_stage.d.ts.map +1 -0
- package/dist/sync/push/merge_stage.js +257 -0
- package/dist/sync/push/merge_stage.js.map +1 -0
- package/dist/sync/push/preflight_stage.d.ts +6 -0
- package/dist/sync/push/preflight_stage.d.ts.map +1 -0
- package/dist/sync/push/preflight_stage.js +40 -0
- package/dist/sync/push/preflight_stage.js.map +1 -0
- package/dist/sync/push/tax_audit.d.ts +24 -0
- package/dist/sync/push/tax_audit.d.ts.map +1 -0
- package/dist/sync/push/tax_audit.js +70 -0
- package/dist/sync/push/tax_audit.js.map +1 -0
- package/dist/sync/push/taxonomy_plan.d.ts +36 -0
- package/dist/sync/push/taxonomy_plan.d.ts.map +1 -0
- package/dist/sync/push/taxonomy_plan.js +136 -0
- package/dist/sync/push/taxonomy_plan.js.map +1 -0
- package/dist/sync/push/transport.d.ts +14 -0
- package/dist/sync/push/transport.d.ts.map +1 -0
- package/dist/sync/push/transport.js +74 -0
- package/dist/sync/push/transport.js.map +1 -0
- package/dist/sync/push/types.d.ts +62 -0
- package/dist/sync/push/types.d.ts.map +1 -0
- package/dist/sync/push/types.js +23 -0
- package/dist/sync/push/types.js.map +1 -0
- package/dist/sync/push.d.ts +2 -57
- package/dist/sync/push.d.ts.map +1 -1
- package/dist/sync/push.js +2 -1184
- package/dist/sync/push.js.map +1 -1
- package/dist/sync/status.d.ts +3 -0
- package/dist/sync/status.d.ts.map +1 -1
- package/dist/sync/status.js +18 -8
- package/dist/sync/status.js.map +1 -1
- package/dist/sync/taxonomies.d.ts.map +1 -1
- package/dist/sync/taxonomies.js +21 -9
- package/dist/sync/taxonomies.js.map +1 -1
- package/dist/sync/terms.d.ts.map +1 -1
- package/dist/sync/terms.js +2 -1
- package/dist/sync/terms.js.map +1 -1
- package/dist/sync/workspace/commit.d.ts +46 -0
- package/dist/sync/workspace/commit.d.ts.map +1 -0
- package/dist/sync/workspace/commit.js +85 -0
- package/dist/sync/workspace/commit.js.map +1 -0
- package/dist/sync/workspace/index.d.ts +42 -0
- package/dist/sync/workspace/index.d.ts.map +1 -0
- package/dist/sync/workspace/index.js +125 -0
- package/dist/sync/workspace/index.js.map +1 -0
- package/dist/utils/disk-spelling.d.ts +26 -0
- package/dist/utils/disk-spelling.d.ts.map +1 -0
- package/dist/utils/disk-spelling.js +51 -0
- package/dist/utils/disk-spelling.js.map +1 -0
- package/dist/utils/draft-revision.d.ts +15 -0
- package/dist/utils/draft-revision.d.ts.map +1 -0
- package/dist/utils/draft-revision.js +36 -0
- package/dist/utils/draft-revision.js.map +1 -0
- package/dist/utils/frontmatter.d.ts +21 -2
- package/dist/utils/frontmatter.d.ts.map +1 -1
- package/dist/utils/frontmatter.js +71 -10
- package/dist/utils/frontmatter.js.map +1 -1
- package/dist/utils/media-body-refs.d.ts +24 -0
- package/dist/utils/media-body-refs.d.ts.map +1 -1
- package/dist/utils/media-body-refs.js +51 -0
- package/dist/utils/media-body-refs.js.map +1 -1
- package/dist/utils/media-companion.d.ts +13 -0
- package/dist/utils/media-companion.d.ts.map +1 -0
- package/dist/utils/media-companion.js +42 -0
- package/dist/utils/media-companion.js.map +1 -0
- package/dist/utils/media-id.d.ts +46 -0
- package/dist/utils/media-id.d.ts.map +1 -0
- package/dist/utils/media-id.js +84 -0
- package/dist/utils/media-id.js.map +1 -0
- package/dist/utils/naming.d.ts.map +1 -1
- package/dist/utils/naming.js +3 -2
- package/dist/utils/naming.js.map +1 -1
- package/dist/utils/ulid.d.ts +31 -0
- package/dist/utils/ulid.d.ts.map +1 -0
- package/dist/utils/ulid.js +65 -0
- package/dist/utils/ulid.js.map +1 -0
- package/package.json +1 -1
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 SPORT-SERVICE RS★R
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import yaml from 'js-yaml';
|
|
25
|
+
import { extractMediaUlid } from '../../client/mapper.js';
|
|
26
|
+
import { toRootRelativeMediaUrl, resolveImageMimeType, uploadMediaFile, fetchRemoteMediaDetail, } from '../../client/media.js';
|
|
27
|
+
import { classifyCodeFenceLines } from '../../client/portable-text.js';
|
|
28
|
+
import { promoteDraftMediaFile } from '../media.js';
|
|
29
|
+
import { ApiError } from '../../client/error.js';
|
|
30
|
+
import { resolveDraftImagePath } from '../preflight.js';
|
|
31
|
+
import { CANONICAL_ULID_REGEX } from '../../utils/ulid.js';
|
|
32
|
+
/**
|
|
33
|
+
* Resolves the canonical media record ULID for frontmatter featuredImage.
|
|
34
|
+
*
|
|
35
|
+
* If featuredImage is a local path (draft or promoted), it resolves the mediaId from
|
|
36
|
+
* the shared uploadedByAbsPath map. Returns autoUploaded: true when resolved from
|
|
37
|
+
* a newly uploaded or reused draft image reference so that advisory verification
|
|
38
|
+
* warnings can be suppressed.
|
|
39
|
+
*/
|
|
40
|
+
export function resolveFeaturedImageMediaId(featuredImage, uploadedByAbsPath, filePath, workspaceRoot) {
|
|
41
|
+
if (!featuredImage)
|
|
42
|
+
return null;
|
|
43
|
+
const trimmed = featuredImage.trim();
|
|
44
|
+
if (CANONICAL_ULID_REGEX.test(trimmed)) {
|
|
45
|
+
return { mediaId: trimmed.toUpperCase(), autoUploaded: false };
|
|
46
|
+
}
|
|
47
|
+
const extracted = extractMediaUlid(trimmed);
|
|
48
|
+
if (extracted) {
|
|
49
|
+
return { mediaId: extracted.toUpperCase(), autoUploaded: false };
|
|
50
|
+
}
|
|
51
|
+
const resolved = resolveDraftImagePath(filePath, trimmed, workspaceRoot);
|
|
52
|
+
const uploaded = uploadedByAbsPath.get(resolved.candidateAbs);
|
|
53
|
+
if (uploaded) {
|
|
54
|
+
return { mediaId: uploaded.mediaId, autoUploaded: true };
|
|
55
|
+
}
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Featured-image preservation / resolution helper.
|
|
60
|
+
*
|
|
61
|
+
* EmDash stores `featured_image: { id: "<ULID>" }`.
|
|
62
|
+
* When pushing an article whose frontmatter sets `featuredImage: <ULID>`, Emit cross-checks that the
|
|
63
|
+
* referenced media exists locally (or was mirrored), and preserves the exact remote object structure
|
|
64
|
+
* from the cache when unchanged.
|
|
65
|
+
*/
|
|
66
|
+
export function preserveOrResolveFeaturedImage(payload, featuredImage, cache, id, workspaceRoot, options) {
|
|
67
|
+
const warnings = [];
|
|
68
|
+
if (featuredImage === undefined)
|
|
69
|
+
return warnings;
|
|
70
|
+
if (featuredImage === null) {
|
|
71
|
+
payload.data.featured_image = null;
|
|
72
|
+
return warnings;
|
|
73
|
+
}
|
|
74
|
+
// Canonicalize once: uppercase Crockford base32. extractMediaUlid() also folds a media
|
|
75
|
+
// URL-shaped value back to its ULID so cached-remote comparison still succeeds.
|
|
76
|
+
const targetUlid = (extractMediaUlid(featuredImage) ?? featuredImage.trim()).toUpperCase();
|
|
77
|
+
// 1. Resend the exact remote object when it already points at this media ULID.
|
|
78
|
+
if (cache) {
|
|
79
|
+
try {
|
|
80
|
+
const cachedEntry = cache.getEntry(id);
|
|
81
|
+
if (cachedEntry) {
|
|
82
|
+
const raw = JSON.parse(cachedEntry.rawJson);
|
|
83
|
+
const item = (raw.item || raw);
|
|
84
|
+
const itemData = (item.data || {});
|
|
85
|
+
const remoteFi = itemData.featured_image;
|
|
86
|
+
if (remoteFi && extractMediaUlid(remoteFi) === targetUlid) {
|
|
87
|
+
payload.data.featured_image = remoteFi;
|
|
88
|
+
return warnings;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
// Non-fatal: cache inspection failure falls through to canonical minimal payload.
|
|
94
|
+
warnings.push(`[push] Unable to inspect cache for featured_image restoration: ${err.message}`);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
// 2. Cross-check the reference against the workspace media mirror (verification only).
|
|
98
|
+
// A missing entry here is NOT a payload problem - { id: <ULID> } is a valid request that
|
|
99
|
+
// EmDash resolves. It only means Emit cannot confirm the media exists, which is exactly
|
|
100
|
+
// where a hallucinated or mistyped ULID would surface. A workspace that does not mirror
|
|
101
|
+
// media (no media/, or an empty one) is not warned about.
|
|
102
|
+
// Skipped when featuredImage was just auto-uploaded from drafts (R1).
|
|
103
|
+
if (workspaceRoot && !options?.skipCompanionCheck) {
|
|
104
|
+
const mediaDir = path.join(workspaceRoot, 'media');
|
|
105
|
+
const mirrored = fs.existsSync(mediaDir) ? fs.readdirSync(mediaDir) : [];
|
|
106
|
+
if (mirrored.length > 0) {
|
|
107
|
+
let companionFound = false;
|
|
108
|
+
let mismatchedCompanion = null;
|
|
109
|
+
for (const f of mirrored) {
|
|
110
|
+
if (!f.endsWith('.yaml') || f.startsWith('legacy_'))
|
|
111
|
+
continue;
|
|
112
|
+
try {
|
|
113
|
+
const meta = yaml.load(fs.readFileSync(path.join(mediaDir, f), 'utf8'));
|
|
114
|
+
const metaId = typeof meta?.id === 'string' ? meta.id.trim().toUpperCase() : undefined;
|
|
115
|
+
if (metaId === targetUlid) {
|
|
116
|
+
companionFound = true;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
if ((f.includes(`_${targetUlid}-`) || f.startsWith(`${targetUlid}-`)) &&
|
|
120
|
+
metaId &&
|
|
121
|
+
metaId !== targetUlid) {
|
|
122
|
+
mismatchedCompanion = { name: f, metaId };
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
catch (err) {
|
|
126
|
+
// Non-fatal: metadata inspection is advisory only.
|
|
127
|
+
warnings.push(`[push] Failed to read local media metadata '${f}': ${err.message}`);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (!companionFound) {
|
|
131
|
+
if (mismatchedCompanion) {
|
|
132
|
+
warnings.push(`Local media metadata '${mismatchedCompanion.name}' describes a different media ID (${mismatchedCompanion.metaId}); it was ignored for featuredImage ${targetUlid}.`);
|
|
133
|
+
}
|
|
134
|
+
warnings.push(`featuredImage ${targetUlid} was not found in the mirrored media under media/, so Emit could not confirm this media ID exists. The ID is sent as-is and EmDash resolves it, but if the ID is wrong the hero image will be missing. Upload the image first (EmDash Admin or media_upload) and use the ULID it returns, or run "emit pull-media" to mirror remote media locally.`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// 3. Canonical minimal payload (identical to what localToApiPayload produces).
|
|
139
|
+
payload.data.featured_image = { id: targetUlid };
|
|
140
|
+
return warnings;
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* Rewrites standalone local draft image lines in Markdown body to their confirmed
|
|
144
|
+
* canonical root-relative URLs (`/_emdash/api/media/file/<storageKey>`), skipping
|
|
145
|
+
* fenced code blocks and opaque block fences.
|
|
146
|
+
*/
|
|
147
|
+
export function rewriteDraftImagesInBody(body, draftImages, uploadedByAbsPath) {
|
|
148
|
+
if (draftImages.length === 0)
|
|
149
|
+
return body;
|
|
150
|
+
const rawPathToResult = new Map();
|
|
151
|
+
for (const ref of draftImages) {
|
|
152
|
+
const res = uploadedByAbsPath.get(ref.resolvedAbsPath);
|
|
153
|
+
if (res) {
|
|
154
|
+
rawPathToResult.set(ref.rawPath, res);
|
|
155
|
+
rawPathToResult.set(ref.rawPath.trim(), res);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
const lines = body.split('\n');
|
|
159
|
+
// Same CommonMark-aware fence walker that G5 / G11 use, so a line can never be
|
|
160
|
+
// rewritten here while being skipped there (or vice versa).
|
|
161
|
+
const fenceStates = classifyCodeFenceLines(body);
|
|
162
|
+
const standaloneRegex = /^!\[([^\]]*)\]\(([^)]+)\)$/;
|
|
163
|
+
return lines
|
|
164
|
+
.map((line, index) => {
|
|
165
|
+
const state = fenceStates[index];
|
|
166
|
+
if (state.isFenceDelimiter || state.insideCodeFence) {
|
|
167
|
+
return line;
|
|
168
|
+
}
|
|
169
|
+
const hasCr = line.endsWith('\r');
|
|
170
|
+
const cleanLine = hasCr ? line.slice(0, -1) : line;
|
|
171
|
+
const trimmed = cleanLine.trim();
|
|
172
|
+
// Opaque EmDash block fences are managed by G9 and must stay untouched
|
|
173
|
+
if (/^<!--ec:block .+ -->$/.test(trimmed)) {
|
|
174
|
+
return line;
|
|
175
|
+
}
|
|
176
|
+
const match = trimmed.match(standaloneRegex);
|
|
177
|
+
if (!match)
|
|
178
|
+
return line;
|
|
179
|
+
const alt = match[1];
|
|
180
|
+
const rawPath = match[2];
|
|
181
|
+
const uploaded = rawPathToResult.get(rawPath) ?? rawPathToResult.get(rawPath.trim());
|
|
182
|
+
if (!uploaded)
|
|
183
|
+
return line;
|
|
184
|
+
const replaced = ``;
|
|
185
|
+
return hasCr ? `${replaced}\r` : replaced;
|
|
186
|
+
})
|
|
187
|
+
.join('\n');
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Promotes uploaded draft images from `media/drafts/<filename>` into
|
|
191
|
+
* `media/<markdownRef>.<ext>` along with companion `.yaml` metadata, where markdownRef is derived from the
|
|
192
|
+
* and commits `status = 'ready'` in `media_state`.
|
|
193
|
+
*
|
|
194
|
+
* Protected by try/catch per asset (C-2) so that any local filesystem failure
|
|
195
|
+
* after a successful remote article PUT/POST is downgraded to a warning rather than
|
|
196
|
+
* aborting `saveBaseSnapshot` / `saveSyncState`.
|
|
197
|
+
*/
|
|
198
|
+
export function finalizePromotedDraftImages(draftImages, uploadedByAbsPath, options, warnings) {
|
|
199
|
+
if (draftImages.length === 0)
|
|
200
|
+
return;
|
|
201
|
+
const processedAbsPaths = new Set();
|
|
202
|
+
const promotedRelPaths = [];
|
|
203
|
+
const reusedRelPaths = [];
|
|
204
|
+
const unverifiedAssets = [];
|
|
205
|
+
for (const ref of draftImages) {
|
|
206
|
+
if (processedAbsPaths.has(ref.resolvedAbsPath))
|
|
207
|
+
continue;
|
|
208
|
+
processedAbsPaths.add(ref.resolvedAbsPath);
|
|
209
|
+
const uploaded = uploadedByAbsPath.get(ref.resolvedAbsPath);
|
|
210
|
+
if (ref.alreadyPromoted) {
|
|
211
|
+
reusedRelPaths.push(ref.alreadyPromoted.promotedRelPath);
|
|
212
|
+
if (uploaded?.remoteVerification === 'unknown') {
|
|
213
|
+
unverifiedAssets.push(`${ref.alreadyPromoted.promotedRelPath} (${ref.alreadyPromoted.mediaId})${uploaded.remoteVerificationNote ? `: ${uploaded.remoteVerificationNote}` : ''}`);
|
|
214
|
+
}
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (!uploaded)
|
|
218
|
+
continue;
|
|
219
|
+
if (uploaded.remoteVerification === 'unknown') {
|
|
220
|
+
unverifiedAssets.push(`${uploaded.filename} (${uploaded.mediaId})${uploaded.remoteVerificationNote ? `: ${uploaded.remoteVerificationNote}` : ''}`);
|
|
221
|
+
}
|
|
222
|
+
try {
|
|
223
|
+
const promoted = promoteDraftMediaFile({
|
|
224
|
+
workspaceRoot: options.workspaceRoot,
|
|
225
|
+
origin: options.origin,
|
|
226
|
+
draftAbsPath: ref.resolvedAbsPath,
|
|
227
|
+
item: {
|
|
228
|
+
id: uploaded.mediaId,
|
|
229
|
+
filename: uploaded.filename,
|
|
230
|
+
mimeType: uploaded.mimeType,
|
|
231
|
+
size: uploaded.size,
|
|
232
|
+
width: uploaded.width ?? null,
|
|
233
|
+
height: uploaded.height ?? null,
|
|
234
|
+
alt: ref.alt || '',
|
|
235
|
+
caption: '',
|
|
236
|
+
storageKey: uploaded.storageKey,
|
|
237
|
+
contentHash: uploaded.contentHash,
|
|
238
|
+
status: 'ready',
|
|
239
|
+
url: uploaded.url,
|
|
240
|
+
createdAt: uploaded.createdAt || new Date().toISOString(),
|
|
241
|
+
},
|
|
242
|
+
removeDraft: true,
|
|
243
|
+
});
|
|
244
|
+
options.cache.saveMediaState({
|
|
245
|
+
localHash: uploaded.localHash,
|
|
246
|
+
mediaId: uploaded.mediaId,
|
|
247
|
+
storageKey: uploaded.storageKey,
|
|
248
|
+
url: uploaded.url,
|
|
249
|
+
workspacePath: promoted.filePath,
|
|
250
|
+
markdownRef: promoted.markdownRef,
|
|
251
|
+
status: 'ready',
|
|
252
|
+
});
|
|
253
|
+
promotedRelPaths.push(promoted.filePath);
|
|
254
|
+
}
|
|
255
|
+
catch (err) {
|
|
256
|
+
// Keep status='uploaded' in media_state so subsequent runs know the remote upload succeeded
|
|
257
|
+
warnings.push(`[LOCAL_IMAGE_AUTO_UPLOAD] Uploaded '${ref.filename}' (${uploaded.mediaId}) to remote, but failed to promote local file to media/: ${err.message}`);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (promotedRelPaths.length > 0) {
|
|
261
|
+
warnings.push(`[LOCAL_IMAGE_AUTO_UPLOAD] Auto-uploaded ${promotedRelPaths.length} draft image(s) and promoted to: ${promotedRelPaths.join(', ')}`);
|
|
262
|
+
}
|
|
263
|
+
if (reusedRelPaths.length > 0) {
|
|
264
|
+
warnings.push(`[LOCAL_IMAGE_AUTO_UPLOAD] Reused ${reusedRelPaths.length} already-promoted media asset(s): ${reusedRelPaths.join(', ')}`);
|
|
265
|
+
}
|
|
266
|
+
if (unverifiedAssets.length > 0) {
|
|
267
|
+
warnings.push(`[MEDIA_EXISTENCE_UNVERIFIED] The remote server did not confirm these reused media records (GET /_emdash/api/media/{id} unsupported or failed): ${unverifiedAssets.join(', ')}. The cached /_emdash/api/media/file/<storageKey> URL was reused as-is; run "emit pull-media" to re-verify or re-upload if the images 404.`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Executes P2–P4 (upload/resolve draft images and rewrite Markdown body in-memory).
|
|
272
|
+
* Already-promoted assets are verified against the remote before their canonical URL is
|
|
273
|
+
* baked into the body, so a media record deleted on the EmDash side can never produce an
|
|
274
|
+
* article that renders HTTP 404 images in production.
|
|
275
|
+
*/
|
|
276
|
+
export async function uploadAndRewriteDraftImages(body, draftImages, options, fetchFn) {
|
|
277
|
+
const uploadedByAbsPath = new Map();
|
|
278
|
+
if (draftImages.length === 0) {
|
|
279
|
+
return { body, uploadedByAbsPath };
|
|
280
|
+
}
|
|
281
|
+
for (const draftRef of draftImages) {
|
|
282
|
+
if (uploadedByAbsPath.has(draftRef.resolvedAbsPath)) {
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
if (draftRef.alreadyPromoted) {
|
|
286
|
+
const promoted = draftRef.alreadyPromoted;
|
|
287
|
+
const remoteCheck = await fetchRemoteMediaDetail(promoted.mediaId, {
|
|
288
|
+
origin: options.origin,
|
|
289
|
+
token: options.token,
|
|
290
|
+
fetchFn,
|
|
291
|
+
});
|
|
292
|
+
if (remoteCheck.exists === false) {
|
|
293
|
+
// Purge the stale local record so the next run does not keep reusing a dead URL
|
|
294
|
+
const staleState = options.cache.getMediaStateByMediaId(promoted.mediaId);
|
|
295
|
+
if (staleState) {
|
|
296
|
+
options.cache.deleteMediaState(staleState.storageKey);
|
|
297
|
+
}
|
|
298
|
+
throw new ApiError({
|
|
299
|
+
message: `Image '${draftRef.filename}' was already promoted to '${promoted.promotedRelPath}' (media id ${promoted.mediaId}), but that media record no longer exists on remote.`,
|
|
300
|
+
code: 'PROMOTED_MEDIA_NOT_FOUND_REMOTE',
|
|
301
|
+
status: 404,
|
|
302
|
+
endpoint: `/_emdash/api/media/${promoted.mediaId}`,
|
|
303
|
+
suggestion: 'Restore the media on the EmDash side (then run "emit pull-media"), or place the image back under media/drafts/ and push again so Emit uploads a fresh record.',
|
|
304
|
+
});
|
|
305
|
+
}
|
|
306
|
+
const detail = remoteCheck.exists === true ? remoteCheck.item : undefined;
|
|
307
|
+
const promotedStorageKey = detail?.storageKey || promoted.storageKey;
|
|
308
|
+
uploadedByAbsPath.set(draftRef.resolvedAbsPath, {
|
|
309
|
+
mediaId: promoted.mediaId,
|
|
310
|
+
storageKey: promotedStorageKey,
|
|
311
|
+
url: toRootRelativeMediaUrl(detail?.url || promoted.url, promotedStorageKey, options.origin),
|
|
312
|
+
localHash: '',
|
|
313
|
+
contentHash: '',
|
|
314
|
+
size: 0,
|
|
315
|
+
mimeType: resolveImageMimeType(draftRef.filename) || 'image/png',
|
|
316
|
+
filename: draftRef.filename,
|
|
317
|
+
deduplicated: true,
|
|
318
|
+
remoteVerification: remoteCheck.exists === true ? 'verified' : 'unknown',
|
|
319
|
+
remoteVerificationNote: remoteCheck.exists === 'unknown' ? remoteCheck.reason : undefined,
|
|
320
|
+
});
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
const uploadRes = await uploadMediaFile({
|
|
324
|
+
filePath: draftRef.resolvedAbsPath,
|
|
325
|
+
filename: draftRef.filename,
|
|
326
|
+
alt: draftRef.alt || undefined,
|
|
327
|
+
origin: options.origin,
|
|
328
|
+
token: options.token,
|
|
329
|
+
refreshToken: options.refreshToken,
|
|
330
|
+
cache: options.cache,
|
|
331
|
+
fetchFn: options.fetchFn,
|
|
332
|
+
});
|
|
333
|
+
uploadedByAbsPath.set(draftRef.resolvedAbsPath, uploadRes);
|
|
334
|
+
}
|
|
335
|
+
const rewrittenBody = rewriteDraftImagesInBody(body, draftImages, uploadedByAbsPath);
|
|
336
|
+
return { body: rewrittenBody, uploadedByAbsPath };
|
|
337
|
+
}
|
|
338
|
+
//# sourceMappingURL=media_stage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"media_stage.js","sourceRoot":"","sources":["../../../src/sync/push/media_stage.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { BaseSnapshot, SyncState } from '../../db/cache.js';
|
|
2
|
+
import { type ParsedMarkdown, type TaxonomyCandidates } from '../../utils/frontmatter.js';
|
|
3
|
+
import { type MergeResult } from '../merge.js';
|
|
4
|
+
import type { PushOptions, PushResult } from './types.js';
|
|
5
|
+
export interface ConflictResolutionOutcome {
|
|
6
|
+
handled: boolean;
|
|
7
|
+
pushResult?: PushResult;
|
|
8
|
+
mergedParsed?: ParsedMarkdown;
|
|
9
|
+
remoteParsedRev?: string;
|
|
10
|
+
mergeResult?: MergeResult;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Handles 409 conflict, performs GET remote, and executes Hybrid 3-Way Merge.
|
|
14
|
+
* If conflicts remain unresolved, writes conflict markers, saves quarantine copy and returns PushResult.
|
|
15
|
+
* If merged cleanly, returns mergedParsed and remoteParsedRev for PUT retry.
|
|
16
|
+
*/
|
|
17
|
+
export declare function resolveConflictAndMerge(params: {
|
|
18
|
+
collection: string;
|
|
19
|
+
id: string;
|
|
20
|
+
targetFilePath: string;
|
|
21
|
+
fullPath: string;
|
|
22
|
+
putUrl: string;
|
|
23
|
+
res?: Response | null;
|
|
24
|
+
taxonomyDivergenceDetected?: boolean;
|
|
25
|
+
parsed: ParsedMarkdown;
|
|
26
|
+
syncState: SyncState | null | undefined;
|
|
27
|
+
baseSnapshot: BaseSnapshot | null | undefined;
|
|
28
|
+
remoteTaxAudit: {
|
|
29
|
+
categories: string[] | null;
|
|
30
|
+
tags: string[] | null;
|
|
31
|
+
} | null;
|
|
32
|
+
candidates: TaxonomyCandidates;
|
|
33
|
+
options: PushOptions;
|
|
34
|
+
fetchFn: typeof fetch;
|
|
35
|
+
warnings: string[];
|
|
36
|
+
}): Promise<ConflictResolutionOutcome>;
|
|
37
|
+
//# sourceMappingURL=merge_stage.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"merge_stage.d.ts","sourceRoot":"","sources":["../../../src/sync/push/merge_stage.ts"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAEL,KAAK,cAAc,EACnB,KAAK,kBAAkB,EACxB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAmB,KAAK,WAAW,EAAE,MAAM,aAAa,CAAC;AAOhE,OAAO,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,UAAU,CAAC;IACxB,YAAY,CAAC,EAAE,cAAc,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,WAAW,CAAC,EAAE,WAAW,CAAC;CAC3B;AAED;;;;GAIG;AACH,wBAAsB,uBAAuB,CAAC,MAAM,EAAE;IACpD,UAAU,EAAE,MAAM,CAAC;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,GAAG,CAAC,EAAE,QAAQ,GAAG,IAAI,CAAC;IACtB,0BAA0B,CAAC,EAAE,OAAO,CAAC;IACrC,MAAM,EAAE,cAAc,CAAC;IACvB,SAAS,EAAE,SAAS,GAAG,IAAI,GAAG,SAAS,CAAC;IACxC,YAAY,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,CAAC;IAC9C,cAAc,EAAE;QAAE,UAAU,EAAE,MAAM,EAAE,GAAG,IAAI,CAAC;QAAC,IAAI,EAAE,MAAM,EAAE,GAAG,IAAI,CAAA;KAAE,GAAG,IAAI,CAAC;IAC9E,UAAU,EAAE,kBAAkB,CAAC;IAC/B,OAAO,EAAE,WAAW,CAAC;IACrB,OAAO,EAAE,OAAO,KAAK,CAAC;IACtB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB,GAAG,OAAO,CAAC,yBAAyB,CAAC,CAqRrC"}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright (C) 2026 SPORT-SERVICE RS★R
|
|
3
|
+
*
|
|
4
|
+
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
* of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
* in the Software without restriction, including without limitation the rights
|
|
7
|
+
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
* copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
* furnished to do so, subject to the following conditions:
|
|
10
|
+
*
|
|
11
|
+
* The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
* copies or substantial portions of the Software.
|
|
13
|
+
*
|
|
14
|
+
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
* SOFTWARE.
|
|
21
|
+
*/
|
|
22
|
+
import fs from 'node:fs';
|
|
23
|
+
import path from 'node:path';
|
|
24
|
+
import { extractComparisonRev } from '../../client/api.js';
|
|
25
|
+
import { ApiError } from '../../client/error.js';
|
|
26
|
+
import { apiResponseToLocal } from '../../client/mapper.js';
|
|
27
|
+
import { articleContentEndpoint } from '../../client/endpoints.js';
|
|
28
|
+
import { readBaseline, readBaselineFrontmatter } from '../../db/baseline.js';
|
|
29
|
+
import { serializeMarkdown, } from '../../utils/frontmatter.js';
|
|
30
|
+
import { hybrid3WayMerge } from '../merge.js';
|
|
31
|
+
import { fetchAssignedTaxonomies, computeTaxonomyHash } from '../terms.js';
|
|
32
|
+
import { calculateHash, quarantineLocalCopy, commitArticleSnapshot } from '../workspace/index.js';
|
|
33
|
+
import { hasUncommittedDraftRevision } from '../../utils/draft-revision.js';
|
|
34
|
+
import { resolveDiskSpelling, renderForDisk } from '../../utils/disk-spelling.js';
|
|
35
|
+
import { throwRemoteArticleDeleted } from './transport.js';
|
|
36
|
+
import { pendingFlagsFor } from './commit.js';
|
|
37
|
+
/**
|
|
38
|
+
* Handles 409 conflict, performs GET remote, and executes Hybrid 3-Way Merge.
|
|
39
|
+
* If conflicts remain unresolved, writes conflict markers, saves quarantine copy and returns PushResult.
|
|
40
|
+
* If merged cleanly, returns mergedParsed and remoteParsedRev for PUT retry.
|
|
41
|
+
*/
|
|
42
|
+
export async function resolveConflictAndMerge(params) {
|
|
43
|
+
const { collection, id, targetFilePath, fullPath, putUrl, res, taxonomyDivergenceDetected, parsed, syncState, baseSnapshot, remoteTaxAudit, candidates, options, fetchFn, warnings, } = params;
|
|
44
|
+
if (res && !taxonomyDivergenceDetected) {
|
|
45
|
+
const errJson = (await res.json().catch(() => ({})));
|
|
46
|
+
const errorCode = errJson.error?.code || 'CONFLICT';
|
|
47
|
+
if (errorCode === 'ENTRY_LOCKED') {
|
|
48
|
+
throw new ApiError({
|
|
49
|
+
message: `Entry is currently locked by another user. (${errJson.error?.message || ''})`,
|
|
50
|
+
code: 'ENTRY_LOCKED',
|
|
51
|
+
status: 409,
|
|
52
|
+
endpoint: putUrl,
|
|
53
|
+
suggestion: 'Wait for the other user to finish editing or release their lock before retrying.',
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
// True revision mismatch conflict or taxonomy divergence: fetch remote latest and run Hybrid 3-Way Merge
|
|
58
|
+
const getUrl = articleContentEndpoint(options.origin, collection, id);
|
|
59
|
+
const getRes = await fetchFn(getUrl, {
|
|
60
|
+
method: 'GET',
|
|
61
|
+
headers: {
|
|
62
|
+
Authorization: `Bearer ${options.token}`,
|
|
63
|
+
Accept: 'application/json',
|
|
64
|
+
},
|
|
65
|
+
});
|
|
66
|
+
if (!getRes.ok) {
|
|
67
|
+
if (getRes.status === 404 || getRes.status === 410) {
|
|
68
|
+
throwRemoteArticleDeleted(collection, id, targetFilePath, getUrl);
|
|
69
|
+
}
|
|
70
|
+
throw new ApiError({
|
|
71
|
+
message: `Failed to fetch latest remote version during conflict resolution: ${getRes.statusText}`,
|
|
72
|
+
code: 'CONFLICT_REMOTE_FETCH_FAILED',
|
|
73
|
+
status: getRes.status,
|
|
74
|
+
endpoint: getUrl,
|
|
75
|
+
suggestion: 'Check remote server availability to resolve conflict.',
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
const remoteItem = (await getRes.json());
|
|
79
|
+
const remoteParsed = apiResponseToLocal(remoteItem, options);
|
|
80
|
+
// Fetch remote taxonomy assignments (reuse remoteTaxAudit if available)
|
|
81
|
+
const remoteTax = remoteTaxAudit ||
|
|
82
|
+
(await fetchAssignedTaxonomies({
|
|
83
|
+
origin: options.origin,
|
|
84
|
+
token: options.token,
|
|
85
|
+
collection,
|
|
86
|
+
id,
|
|
87
|
+
fetchFn,
|
|
88
|
+
}));
|
|
89
|
+
if (!remoteParsed.frontmatter.taxonomies) {
|
|
90
|
+
remoteParsed.frontmatter.taxonomies = {};
|
|
91
|
+
}
|
|
92
|
+
const baseFm = readBaselineFrontmatter(baseSnapshot);
|
|
93
|
+
if (remoteTax.categories !== null) {
|
|
94
|
+
remoteParsed.frontmatter.taxonomies.categories = remoteTax.categories;
|
|
95
|
+
}
|
|
96
|
+
else if (baseFm?.taxonomies?.categories) {
|
|
97
|
+
remoteParsed.frontmatter.taxonomies.categories = baseFm.taxonomies.categories;
|
|
98
|
+
}
|
|
99
|
+
if (remoteTax.tags !== null) {
|
|
100
|
+
remoteParsed.frontmatter.taxonomies.tags = remoteTax.tags;
|
|
101
|
+
}
|
|
102
|
+
else if (baseFm?.taxonomies?.tags) {
|
|
103
|
+
remoteParsed.frontmatter.taxonomies.tags = baseFm.taxonomies.tags;
|
|
104
|
+
}
|
|
105
|
+
// Perform Hybrid 3-Way Merge
|
|
106
|
+
const baseInfo = baseSnapshot ? readBaseline(baseSnapshot) : null;
|
|
107
|
+
const mergeResult = hybrid3WayMerge(parsed, baseInfo, {
|
|
108
|
+
frontmatter: remoteParsed.frontmatter,
|
|
109
|
+
body: remoteParsed.body,
|
|
110
|
+
}, {
|
|
111
|
+
strategy: options.resolveTaxonomy,
|
|
112
|
+
});
|
|
113
|
+
warnings.push(...mergeResult.warnings);
|
|
114
|
+
// Check destructive taxonomy wipe guard during 3-way merge (exempt if strategy is explicitly specified)
|
|
115
|
+
if (!options.resolveTaxonomy && mergeResult.wipeWarnings && mergeResult.wipeWarnings.length > 0) {
|
|
116
|
+
if (!options.allowWipeTaxonomies && !options.force) {
|
|
117
|
+
const details = mergeResult.wipeWarnings
|
|
118
|
+
.map((w) => `${w.name}: clearing ${w.remoteTerms.length} assignment(s)`)
|
|
119
|
+
.join(', ');
|
|
120
|
+
throw new ApiError({
|
|
121
|
+
endpoint: `/_emdash/api/content/${collection}/${id}`,
|
|
122
|
+
code: 'DESTRUCTIVE_TAXONOMY_WIPE',
|
|
123
|
+
status: 400,
|
|
124
|
+
message: `Local article specified empty array for taxonomies (${details}), which would wipe existing remote assignments.`,
|
|
125
|
+
suggestion: `Pass --allow-wipe-taxonomies (or --force) to confirm clearing: "emit push ${targetFilePath} --allow-wipe-taxonomies"`,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
// Log clean auto-merged taxonomy changes
|
|
130
|
+
if (mergeResult.autoMergedTaxonomies && mergeResult.autoMergedTaxonomies.length > 0) {
|
|
131
|
+
for (const item of mergeResult.autoMergedTaxonomies) {
|
|
132
|
+
const parts = [];
|
|
133
|
+
if (item.remoteAdded.length > 0)
|
|
134
|
+
parts.push(`+${item.remoteAdded.join(', ')}`);
|
|
135
|
+
if (item.remoteRemoved.length > 0)
|
|
136
|
+
parts.push(`-${item.remoteRemoved.join(', ')}`);
|
|
137
|
+
warnings.push(`🏷️ Merged remote taxonomy change(s) for ${item.name}: ${parts.join(' ')}`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Log strategy adoptions
|
|
141
|
+
if (mergeResult.strategyAdoptions && mergeResult.strategyAdoptions.length > 0) {
|
|
142
|
+
for (const item of mergeResult.strategyAdoptions) {
|
|
143
|
+
const parts = [];
|
|
144
|
+
if (item.discardedAdded.length > 0)
|
|
145
|
+
parts.push(`+${item.discardedAdded.join(', ')}`);
|
|
146
|
+
if (item.discardedRemoved.length > 0)
|
|
147
|
+
parts.push(`-${item.discardedRemoved.join(', ')}`);
|
|
148
|
+
const discardedStr = parts.length > 0 ? `: discarded ${parts.join(' ')}` : '';
|
|
149
|
+
warnings.push(`🏷️ Resolved '${item.name}' with strategy '${item.side}'${discardedStr}.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
// Fallback post-merge resolution only if strategy was NOT specified and taxonomy conflicts occurred
|
|
153
|
+
if (!options.resolveTaxonomy &&
|
|
154
|
+
mergeResult.taxonomyConflicts &&
|
|
155
|
+
mergeResult.taxonomyConflicts.length > 0) {
|
|
156
|
+
if (options.force) {
|
|
157
|
+
const strategy = 'union';
|
|
158
|
+
const unionTaxonomies = {};
|
|
159
|
+
const allTaxNames = new Set([
|
|
160
|
+
...Object.keys(parsed.frontmatter.taxonomies || {}),
|
|
161
|
+
...Object.keys(remoteParsed.frontmatter.taxonomies || {}),
|
|
162
|
+
]);
|
|
163
|
+
for (const tName of allTaxNames) {
|
|
164
|
+
const lTerms = parsed.frontmatter.taxonomies?.[tName] || [];
|
|
165
|
+
const rTerms = remoteParsed.frontmatter.taxonomies?.[tName] || [];
|
|
166
|
+
unionTaxonomies[tName] = Array.from(new Set([...lTerms, ...rTerms]));
|
|
167
|
+
}
|
|
168
|
+
mergeResult.frontmatter.taxonomies = unionTaxonomies;
|
|
169
|
+
warnings.push(`Resolved taxonomy conflicts using strategy '${strategy}'.`);
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
mergeResult.hasConflict = true;
|
|
173
|
+
warnings.push(`Conflict on taxonomies (${mergeResult.taxonomyConflicts.join(', ')}): local and remote changes could not be cleanly merged. Push aborted to prevent data loss.`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (mergeResult.hasConflict) {
|
|
177
|
+
const isTaxonomyConflict = Boolean(mergeResult.taxonomyConflicts && mergeResult.taxonomyConflicts.length > 0);
|
|
178
|
+
const hasBodyConflict = Boolean(mergeResult.body.includes('<<<<<<< LOCAL') || mergeResult.body.includes('<<<<<<< local'));
|
|
179
|
+
const isTaxonomyOnlyConflict = isTaxonomyConflict && !hasBodyConflict;
|
|
180
|
+
const remoteComparisonRev = extractComparisonRev(remoteItem, remoteParsed.rev);
|
|
181
|
+
const hasDraft = hasUncommittedDraftRevision({
|
|
182
|
+
status: remoteParsed.frontmatter.status,
|
|
183
|
+
remoteRev: remoteComparisonRev,
|
|
184
|
+
});
|
|
185
|
+
const conflictedMarkdown = serializeMarkdown(mergeResult.frontmatter, mergeResult.body, options.candidates, { hasDraftRevision: hasDraft });
|
|
186
|
+
let quarantineRel = null;
|
|
187
|
+
let activeDiskContent = conflictedMarkdown;
|
|
188
|
+
// Only write conflict markers into the article body if body actually has conflicts
|
|
189
|
+
if (!isTaxonomyOnlyConflict) {
|
|
190
|
+
const currentDisk = fs.readFileSync(fullPath, 'utf8');
|
|
191
|
+
quarantineRel = quarantineLocalCopy(options.workspaceRoot, collection, targetFilePath, currentDisk, { isForce: false });
|
|
192
|
+
const spelling = resolveDiskSpelling(currentDisk, syncState);
|
|
193
|
+
activeDiskContent = renderForDisk(conflictedMarkdown, spelling, {
|
|
194
|
+
workspaceRoot: options.workspaceRoot,
|
|
195
|
+
articleRelPath: targetFilePath,
|
|
196
|
+
fs,
|
|
197
|
+
});
|
|
198
|
+
fs.writeFileSync(fullPath, activeDiskContent, 'utf8');
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
activeDiskContent = fs.readFileSync(fullPath, 'utf8');
|
|
202
|
+
}
|
|
203
|
+
let actualConflictCopyHash = null;
|
|
204
|
+
if (quarantineRel) {
|
|
205
|
+
const qFull = path.join(options.workspaceRoot, quarantineRel);
|
|
206
|
+
if (fs.existsSync(qFull)) {
|
|
207
|
+
actualConflictCopyHash = calculateHash(fs.readFileSync(qFull, 'utf8'));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
const remoteTaxHash = computeTaxonomyHash(remoteParsed.frontmatter.taxonomies?.categories, remoteParsed.frontmatter.taxonomies?.tags);
|
|
211
|
+
const flags = pendingFlagsFor({
|
|
212
|
+
mode: 'conflict',
|
|
213
|
+
hasBodyConflict,
|
|
214
|
+
isTaxonomyConflict,
|
|
215
|
+
conflictedHash: actualConflictCopyHash,
|
|
216
|
+
quarantineRelPath: quarantineRel,
|
|
217
|
+
remoteTaxonomyHash: remoteTaxHash,
|
|
218
|
+
});
|
|
219
|
+
// Update sync state and base snapshot via centralized commitArticleSnapshot
|
|
220
|
+
commitArticleSnapshot(options.cache, options.workspaceRoot, {
|
|
221
|
+
id,
|
|
222
|
+
relPath: targetFilePath,
|
|
223
|
+
diskContent: activeDiskContent,
|
|
224
|
+
canonicalContent: serializeMarkdown(remoteParsed.frontmatter, remoteParsed.body, candidates, {
|
|
225
|
+
hasDraftRevision: hasDraft,
|
|
226
|
+
}),
|
|
227
|
+
frontmatter: remoteParsed.frontmatter,
|
|
228
|
+
bodyMarkdown: remoteParsed.body,
|
|
229
|
+
revForTransport: remoteParsed.rev,
|
|
230
|
+
revForComparison: remoteComparisonRev,
|
|
231
|
+
flags,
|
|
232
|
+
skipDiskWrite: true,
|
|
233
|
+
});
|
|
234
|
+
return {
|
|
235
|
+
handled: true,
|
|
236
|
+
pushResult: {
|
|
237
|
+
success: false,
|
|
238
|
+
id,
|
|
239
|
+
rev: remoteParsed.rev,
|
|
240
|
+
filePath: targetFilePath,
|
|
241
|
+
conflict: true,
|
|
242
|
+
taxonomyConflict: isTaxonomyOnlyConflict,
|
|
243
|
+
warnings,
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
// Cleanly merged: return mergedParsed for PUT retry
|
|
248
|
+
parsed.frontmatter = mergeResult.frontmatter;
|
|
249
|
+
parsed.body = mergeResult.body;
|
|
250
|
+
return {
|
|
251
|
+
handled: false,
|
|
252
|
+
mergedParsed: parsed,
|
|
253
|
+
remoteParsedRev: remoteParsed.rev,
|
|
254
|
+
mergeResult,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
//# sourceMappingURL=merge_stage.js.map
|