@hanna84/mcp-writing 1.12.0 → 1.13.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/CHANGELOG.md +10 -0
- package/package.json +1 -1
- package/scrivener-direct.js +87 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,11 +4,21 @@ All notable changes to this project will be documented in this file. Dates are d
|
|
|
4
4
|
|
|
5
5
|
Generated by [`auto-changelog`](https://github.com/CookPete/auto-changelog).
|
|
6
6
|
|
|
7
|
+
#### [v1.13.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
|
+
/compare/v1.12.0...v1.13.0)
|
|
9
|
+
|
|
10
|
+
- feat(scrivener-direct): add ambiguity warning taxonomy for beta merge [`#69`](https://github.com/hannasdev/mcp-writing.git
|
|
11
|
+
/pull/69)
|
|
12
|
+
|
|
7
13
|
#### [v1.12.0](https://github.com/hannasdev/mcp-writing.git
|
|
8
14
|
/compare/v1.11.8...v1.12.0)
|
|
9
15
|
|
|
16
|
+
> 24 April 2026
|
|
17
|
+
|
|
10
18
|
- feat(scrivener-direct): ownership enforcement (PR-3a) [`#68`](https://github.com/hannasdev/mcp-writing.git
|
|
11
19
|
/pull/68)
|
|
20
|
+
- Release 1.12.0 [`e249edd`](https://github.com/hannasdev/mcp-writing.git
|
|
21
|
+
/commit/e249edd1df6b1f77253200296057fe1cb468d2d0)
|
|
12
22
|
|
|
13
23
|
#### [v1.11.8](https://github.com/hannasdev/mcp-writing.git
|
|
14
24
|
/compare/v1.11.7...v1.11.8)
|
package/package.json
CHANGED
package/scrivener-direct.js
CHANGED
|
@@ -126,6 +126,73 @@ const KNOWN_CUSTOM_FIELD_IDS = new Set([
|
|
|
126
126
|
]);
|
|
127
127
|
|
|
128
128
|
const MAX_RETURNED_WARNINGS = 25;
|
|
129
|
+
const STRUCTURE_MAPPING_FIELDS = new Set(["part", "chapter", "chapter_title"]);
|
|
130
|
+
|
|
131
|
+
function normalizeComparisonValue(key, value) {
|
|
132
|
+
if ((key === "tags" || key === "scene_functions") && Array.isArray(value)) {
|
|
133
|
+
return [...new Set(value.map(item => String(item)))].sort();
|
|
134
|
+
}
|
|
135
|
+
return value;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function valuesEquivalentForMerge(key, a, b) {
|
|
139
|
+
return JSON.stringify(normalizeComparisonValue(key, a)) === JSON.stringify(normalizeComparisonValue(key, b));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function collectAmbiguityWarnings(existing, mergeData, { file, uuid }) {
|
|
143
|
+
const out = [];
|
|
144
|
+
|
|
145
|
+
const externalSource = existing?.external_source;
|
|
146
|
+
if (externalSource !== undefined && externalSource !== null && externalSource !== "scrivener") {
|
|
147
|
+
out.push({
|
|
148
|
+
code: "ambiguous_identity_tie",
|
|
149
|
+
message: "Existing sidecar identity source conflicts with Scrivener mapping; keeping existing sidecar identity.",
|
|
150
|
+
reason: "external_source_conflict",
|
|
151
|
+
external_source: String(externalSource),
|
|
152
|
+
file,
|
|
153
|
+
uuid,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
if (externalSource === "scrivener" && (existing?.external_id === undefined || existing?.external_id === null || String(existing.external_id).trim() === "")) {
|
|
157
|
+
out.push({
|
|
158
|
+
code: "ambiguous_identity_tie",
|
|
159
|
+
message: "Existing sidecar identity is marked as Scrivener but missing external_id; keeping existing sidecar identity.",
|
|
160
|
+
reason: "missing_external_id",
|
|
161
|
+
file,
|
|
162
|
+
uuid,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
for (const [key, scrivenerValue] of Object.entries(mergeData)) {
|
|
167
|
+
if (!Object.hasOwn(existing, key)) continue;
|
|
168
|
+
if (valuesEquivalentForMerge(key, existing[key], scrivenerValue)) continue;
|
|
169
|
+
|
|
170
|
+
if (STRUCTURE_MAPPING_FIELDS.has(key)) {
|
|
171
|
+
out.push({
|
|
172
|
+
code: "ambiguous_structure_mapping",
|
|
173
|
+
message: `Existing sidecar field '${key}' conflicts with Scrivener-derived structure; keeping existing value.`,
|
|
174
|
+
field: key,
|
|
175
|
+
existing_value: existing[key],
|
|
176
|
+
scrivener_value: scrivenerValue,
|
|
177
|
+
file,
|
|
178
|
+
uuid,
|
|
179
|
+
});
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
out.push({
|
|
184
|
+
code: "ambiguous_metadata_mapping",
|
|
185
|
+
message: `Existing sidecar field '${key}' conflicts with Scrivener metadata; keeping existing value.`,
|
|
186
|
+
field: key,
|
|
187
|
+
existing_value: existing[key],
|
|
188
|
+
scrivener_value: scrivenerValue,
|
|
189
|
+
file,
|
|
190
|
+
uuid,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
129
196
|
|
|
130
197
|
function recordWarning(summary, warning) {
|
|
131
198
|
if (!summary[warning.code]) {
|
|
@@ -137,7 +204,21 @@ function recordWarning(summary, warning) {
|
|
|
137
204
|
|
|
138
205
|
if (entry.examples.length < 5) {
|
|
139
206
|
const example = { message: warning.message };
|
|
140
|
-
for (const key of [
|
|
207
|
+
for (const key of [
|
|
208
|
+
"file",
|
|
209
|
+
"sync_number",
|
|
210
|
+
"field",
|
|
211
|
+
"field_id",
|
|
212
|
+
"value",
|
|
213
|
+
"reason",
|
|
214
|
+
"external_source",
|
|
215
|
+
"existing_value",
|
|
216
|
+
"scrivener_value",
|
|
217
|
+
"uuid",
|
|
218
|
+
"from_path",
|
|
219
|
+
"to_path",
|
|
220
|
+
"moved_to",
|
|
221
|
+
]) {
|
|
141
222
|
if (warning[key] !== undefined && warning[key] !== null) {
|
|
142
223
|
example[key] = warning[key];
|
|
143
224
|
}
|
|
@@ -500,6 +581,11 @@ export function mergeScrivenerProjectMetadata({
|
|
|
500
581
|
throw new Error(`Invalid sidecar YAML mapping at ${sidecarPath}`);
|
|
501
582
|
}
|
|
502
583
|
const existing = existingRaw ?? {};
|
|
584
|
+
const ambiguityWarnings = collectAmbiguityWarnings(existing, mergeData, { file: filename, uuid });
|
|
585
|
+
for (const warning of ambiguityWarnings) {
|
|
586
|
+
warningsTruncated = pushWarning(warnings, warningSummary, warning) || warningsTruncated;
|
|
587
|
+
}
|
|
588
|
+
|
|
503
589
|
const { merged, changed, newKeys } = mergeSidecarData(existing, mergeData);
|
|
504
590
|
const effective = changed ? merged : existing;
|
|
505
591
|
const targetDir = sceneContainerDir(
|