@hanna84/mcp-writing 2.12.4 → 2.12.6

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 (39) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/async-jobs.js +1 -218
  3. package/async-progress.js +1 -1
  4. package/helpers.js +2 -2
  5. package/importer.js +1 -448
  6. package/index.js +1 -501
  7. package/metadata-lint.js +1 -468
  8. package/package.json +32 -2
  9. package/runtime-diagnostics.js +1 -97
  10. package/scene-character-batch.js +1 -246
  11. package/scene-character-normalization.js +1 -199
  12. package/scripts/async-job-runner.mjs +3 -3
  13. package/scripts/generate-tool-docs.mjs +21 -3
  14. package/scripts/import.js +1 -1
  15. package/scripts/lint-metadata.mjs +1 -1
  16. package/scripts/manual-scrivener-realtest.mjs +3 -3
  17. package/scripts/merge-scrivx.js +2 -2
  18. package/scripts/new-world-entity.js +2 -2
  19. package/scripts/normalize-scene-characters.mjs +3 -3
  20. package/scripts/profile-review-bundles.mjs +1 -1
  21. package/scrivener-direct.js +1 -843
  22. package/src/index.js +502 -0
  23. package/src/runtime/async-jobs.js +218 -0
  24. package/src/runtime/async-progress.js +1 -0
  25. package/src/runtime/runtime-diagnostics.js +97 -0
  26. package/src/sync/importer.js +448 -0
  27. package/src/sync/metadata-lint.js +468 -0
  28. package/src/sync/scene-character-batch.js +246 -0
  29. package/src/sync/scene-character-normalization.js +199 -0
  30. package/src/sync/scrivener-direct.js +843 -0
  31. package/src/sync/sync.js +755 -0
  32. package/src/world/world-entity-templates.js +116 -0
  33. package/sync.js +1 -755
  34. package/tools/editing.js +1 -1
  35. package/tools/metadata.js +2 -2
  36. package/tools/review-bundles.js +1 -1
  37. package/tools/styleguide.js +1 -1
  38. package/tools/sync.js +2 -2
  39. package/world-entity-templates.js +1 -116
@@ -0,0 +1,199 @@
1
+ export const NON_DISTINCTIVE_TOKENS = new Set([
2
+ "the",
3
+ "and",
4
+ "for",
5
+ "with",
6
+ "from",
7
+ "into",
8
+ "onto",
9
+ "over",
10
+ "under",
11
+ "after",
12
+ "before",
13
+ "about",
14
+ "around",
15
+ ]);
16
+
17
+ export function escapeRegex(text) {
18
+ return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
19
+ }
20
+
21
+ export function isDistinctiveToken(token) {
22
+ const normalized = String(token ?? "").trim().toLowerCase();
23
+ return Boolean(normalized) && normalized.length >= 3 && !NON_DISTINCTIVE_TOKENS.has(normalized);
24
+ }
25
+
26
+ function normalizeRawCharacterValues(values) {
27
+ const raw = Array.isArray(values) ? values : [];
28
+ const seen = new Set();
29
+ const normalized = [];
30
+
31
+ for (const value of raw) {
32
+ const text = String(value ?? "").trim();
33
+ if (!text || seen.has(text)) continue;
34
+ seen.add(text);
35
+ normalized.push(text);
36
+ }
37
+
38
+ return normalized;
39
+ }
40
+
41
+ function tokenizeValue(value) {
42
+ return [...new Set(String(value ?? "").toLowerCase().split(/\s+/).filter(isDistinctiveToken))];
43
+ }
44
+
45
+ export function buildCharacterNormalizationContext(rows) {
46
+ const clean = (Array.isArray(rows) ? rows : [])
47
+ .filter(row => row?.character_id && row?.name)
48
+ .map(row => {
49
+ const character_id = String(row.character_id).trim();
50
+ const name = String(row.name).trim();
51
+ const phrase_tokens = name.toLowerCase().split(/\s+/).filter(Boolean);
52
+ const tokens = [...new Set(phrase_tokens)];
53
+ return {
54
+ character_id,
55
+ name,
56
+ phrase_tokens,
57
+ tokens,
58
+ informative_tokens: tokens.filter(isDistinctiveToken),
59
+ full_name_regex: phrase_tokens.length > 1
60
+ ? new RegExp(`\\b${phrase_tokens.map(escapeRegex).join("\\s+")}\\b`, "i")
61
+ : null,
62
+ };
63
+ })
64
+ .filter(row => row.character_id.length > 0 && row.name.length > 0);
65
+
66
+ const byId = new Map();
67
+ const nameMap = new Map();
68
+ const tokenMap = new Map();
69
+ for (const row of clean) {
70
+ byId.set(row.character_id, row);
71
+ const normalizedName = row.name.toLowerCase();
72
+ const ids = nameMap.get(normalizedName) ?? [];
73
+ ids.push(row.character_id);
74
+ nameMap.set(normalizedName, ids);
75
+
76
+ for (const token of row.informative_tokens) {
77
+ const tokenIds = tokenMap.get(token) ?? [];
78
+ tokenIds.push(row.character_id);
79
+ tokenMap.set(token, tokenIds);
80
+ }
81
+ }
82
+
83
+ return { clean, byId, nameMap, tokenMap };
84
+ }
85
+
86
+ export function resolveCharacterReference(value, context) {
87
+ const text = String(value ?? "").trim();
88
+ if (!text) return null;
89
+
90
+ if (context.byId.has(text)) {
91
+ return text;
92
+ }
93
+
94
+ const exactNameIds = context.nameMap.get(text.toLowerCase());
95
+ if (exactNameIds?.length === 1) {
96
+ return exactNameIds[0];
97
+ }
98
+
99
+ const words = text.toLowerCase().split(/\s+/).filter(isDistinctiveToken);
100
+ if (words.length === 0) {
101
+ return text;
102
+ }
103
+
104
+ const matches = context.clean.filter(row =>
105
+ words.every(word => row.informative_tokens.includes(word))
106
+ );
107
+
108
+ if (matches.length === 1) {
109
+ return matches[0].character_id;
110
+ }
111
+
112
+ return text;
113
+ }
114
+
115
+ function isProperSubset(subsetTokens, supersetTokens) {
116
+ if (subsetTokens.length < 2 || subsetTokens.length >= supersetTokens.length) {
117
+ return false;
118
+ }
119
+ return subsetTokens.every(token => supersetTokens.includes(token));
120
+ }
121
+
122
+ function hasMoreSpecificNonCanonicalSource(candidate, sourceInfo) {
123
+ if (!sourceInfo || sourceInfo.hadCanonicalSource || sourceInfo.nonCanonicalTokens.length === 0) {
124
+ return false;
125
+ }
126
+
127
+ return sourceInfo.nonCanonicalTokens.some(tokens => isProperSubset(candidate.informative_tokens, tokens));
128
+ }
129
+
130
+ function pruneLessSpecificCanonicalIds(values, context, sourceMap) {
131
+ return values.filter((value, idx) => {
132
+ const row = context.byId.get(value);
133
+ if (!row || row.informative_tokens.length === 0) {
134
+ return true;
135
+ }
136
+
137
+ const rowSource = sourceMap.get(value);
138
+ if (!rowSource?.hadCanonicalSource) {
139
+ return true;
140
+ }
141
+
142
+ for (let i = 0; i < values.length; i++) {
143
+ if (i === idx) continue;
144
+
145
+ const otherId = values[i];
146
+ const other = context.byId.get(otherId);
147
+ if (!other || other.informative_tokens.length === 0) continue;
148
+
149
+ if (
150
+ isProperSubset(row.informative_tokens, other.informative_tokens)
151
+ && hasMoreSpecificNonCanonicalSource(row, sourceMap.get(otherId))
152
+ ) {
153
+ return false;
154
+ }
155
+ }
156
+
157
+ return true;
158
+ });
159
+ }
160
+
161
+ export function normalizeSceneCharacters(values, context) {
162
+ const before = normalizeRawCharacterValues(values);
163
+ const resolved = [];
164
+ const seen = new Set();
165
+ const sourceMap = new Map();
166
+
167
+ for (const value of before) {
168
+ const normalized = resolveCharacterReference(value, context);
169
+ if (!normalized) continue;
170
+
171
+ const source = sourceMap.get(normalized) ?? {
172
+ hadCanonicalSource: false,
173
+ nonCanonicalTokens: [],
174
+ };
175
+
176
+ if (context.byId.has(value)) {
177
+ source.hadCanonicalSource = true;
178
+ } else {
179
+ source.nonCanonicalTokens.push(tokenizeValue(value));
180
+ }
181
+ sourceMap.set(normalized, source);
182
+
183
+ if (seen.has(normalized)) continue;
184
+ seen.add(normalized);
185
+ resolved.push(normalized);
186
+ }
187
+
188
+ const after = pruneLessSpecificCanonicalIds(resolved, context, sourceMap);
189
+ const beforeSet = new Set(before);
190
+ const afterSet = new Set(after);
191
+
192
+ return {
193
+ before,
194
+ after,
195
+ changed: before.length !== after.length || before.some((value, idx) => after[idx] !== value),
196
+ added: after.filter(value => !beforeSet.has(value)),
197
+ removed: before.filter(value => !afterSet.has(value)),
198
+ };
199
+ }