@hanna84/mcp-writing 3.22.5 → 3.23.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 +7 -0
- package/package.json +1 -1
- package/src/tools/metadata.js +93 -3
package/CHANGELOG.md
CHANGED
|
@@ -4,9 +4,16 @@ 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
|
+
#### [v3.23.0](https://github.com/hannasdev/mcp-writing/compare/v3.22.5...v3.23.0)
|
|
8
|
+
|
|
9
|
+
- feat: guard scene relationship metadata updates [`#230`](https://github.com/hannasdev/mcp-writing/pull/230)
|
|
10
|
+
|
|
7
11
|
#### [v3.22.5](https://github.com/hannasdev/mcp-writing/compare/v3.22.4...v3.22.5)
|
|
8
12
|
|
|
13
|
+
> 30 May 2026
|
|
14
|
+
|
|
9
15
|
- chore(deps): Bump qs in the npm_and_yarn group across 1 directory [`#214`](https://github.com/hannasdev/mcp-writing/pull/214)
|
|
16
|
+
- Release 3.22.5 [`a4146a0`](https://github.com/hannasdev/mcp-writing/commit/a4146a03a9976d022afb75842bee3ce50742fe65)
|
|
10
17
|
|
|
11
18
|
#### [v3.22.4](https://github.com/hannasdev/mcp-writing/compare/v3.22.3...v3.22.4)
|
|
12
19
|
|
package/package.json
CHANGED
package/src/tools/metadata.js
CHANGED
|
@@ -34,6 +34,7 @@ import {
|
|
|
34
34
|
} from "../structure/project-backup-refresh.js";
|
|
35
35
|
|
|
36
36
|
const STRUCTURAL_SCENE_METADATA_FIELDS = ["part", "chapter", "chapter_id", "chapter_title", "timeline_position"];
|
|
37
|
+
const RELATIONSHIP_SCENE_METADATA_FIELDS = ["characters", "places"];
|
|
37
38
|
|
|
38
39
|
function emptyBackupMutationResult() {
|
|
39
40
|
return {
|
|
@@ -108,6 +109,22 @@ function getProvidedStructuralSceneMetadataFields(fields) {
|
|
|
108
109
|
return STRUCTURAL_SCENE_METADATA_FIELDS.filter((field) => Object.hasOwn(fields, field));
|
|
109
110
|
}
|
|
110
111
|
|
|
112
|
+
function getProvidedRelationshipSceneMetadataFields(fields) {
|
|
113
|
+
return RELATIONSHIP_SCENE_METADATA_FIELDS.filter((field) => Object.hasOwn(fields, field));
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedFields }) {
|
|
117
|
+
return {
|
|
118
|
+
project_id: projectId,
|
|
119
|
+
scene_id: sceneId,
|
|
120
|
+
blocked_fields: blockedFields,
|
|
121
|
+
boundary: "scene_relationship_metadata",
|
|
122
|
+
relationship_tools: ["connect_character_place_evidence", "audit_relationship_metadata"],
|
|
123
|
+
discovery_workflows: ["describe_workflows", "find_scenes", "list_characters", "list_places"],
|
|
124
|
+
next_step: "Use find_scenes, list_characters, and list_places to identify stable IDs. Use connect_character_place_evidence only when the scene proves paired sheet-backed character/place evidence; use audit_relationship_metadata to review legacy sidecar/frontmatter relationship fields. Character-only or place-only scene evidence is not changed through update_scene_metadata.",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
111
128
|
function persistReferenceDocLink({ filePath, syncDir, targetDocId, relation }) {
|
|
112
129
|
const syncDirAbs = path.resolve(syncDir);
|
|
113
130
|
const syncDirReal = resolveBoundaryRootReal(syncDirAbs);
|
|
@@ -437,6 +454,55 @@ function querySceneRelationshipSnapshot(db, { sceneId, projectId }) {
|
|
|
437
454
|
};
|
|
438
455
|
}
|
|
439
456
|
|
|
457
|
+
function restoreSceneRelationshipSnapshot(db, { sceneId, projectId, snapshot }) {
|
|
458
|
+
db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(sceneId, projectId);
|
|
459
|
+
db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(sceneId, projectId);
|
|
460
|
+
|
|
461
|
+
const insertCharacter = db.prepare(`
|
|
462
|
+
INSERT OR IGNORE INTO scene_characters (scene_id, project_id, character_id)
|
|
463
|
+
VALUES (?, ?, ?)
|
|
464
|
+
`);
|
|
465
|
+
for (const characterId of snapshot.characters) {
|
|
466
|
+
insertCharacter.run(sceneId, projectId, characterId);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
const insertPlace = db.prepare(`
|
|
470
|
+
INSERT OR IGNORE INTO scene_places (scene_id, project_id, place_id)
|
|
471
|
+
VALUES (?, ?, ?)
|
|
472
|
+
`);
|
|
473
|
+
for (const placeId of snapshot.places) {
|
|
474
|
+
insertPlace.run(sceneId, projectId, placeId);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
function isVersionContinuityMarker(value) {
|
|
479
|
+
return /^v\d[\d.a-z]*$/i.test(String(value).trim());
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function buildSceneMetadataSearchKeywords(meta, relationshipSnapshot) {
|
|
483
|
+
const compatibilityVersionMarkers = normalizeStringList(meta.characters).filter(isVersionContinuityMarker);
|
|
484
|
+
return [
|
|
485
|
+
...normalizeStringList(meta.tags),
|
|
486
|
+
...compatibilityVersionMarkers,
|
|
487
|
+
...relationshipSnapshot.characters,
|
|
488
|
+
...relationshipSnapshot.places,
|
|
489
|
+
...normalizeStringList(meta.versions),
|
|
490
|
+
]
|
|
491
|
+
.filter(Boolean)
|
|
492
|
+
.map(String)
|
|
493
|
+
.map((value) => value.trim())
|
|
494
|
+
.filter(Boolean)
|
|
495
|
+
.join(" ");
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function restoreSceneRelationshipSearchKeywords(db, { sceneId, projectId, meta, snapshot }) {
|
|
499
|
+
db.prepare(`
|
|
500
|
+
UPDATE scenes_fts
|
|
501
|
+
SET keywords = ?
|
|
502
|
+
WHERE scene_id = ? AND project_id = ?
|
|
503
|
+
`).run(buildSceneMetadataSearchKeywords(meta, snapshot), sceneId, projectId);
|
|
504
|
+
}
|
|
505
|
+
|
|
440
506
|
export function registerMetadataTools(s, {
|
|
441
507
|
db,
|
|
442
508
|
SYNC_DIR,
|
|
@@ -2214,7 +2280,7 @@ export function registerMetadataTools(s, {
|
|
|
2214
2280
|
// ---- update_scene_metadata -----------------------------------------------
|
|
2215
2281
|
s.tool(
|
|
2216
2282
|
"update_scene_metadata",
|
|
2217
|
-
"Update one or more non-structural metadata fields for a scene. Writes only supplied
|
|
2283
|
+
"Update one or more non-structural, non-relationship metadata fields for a scene. Writes only supplied allowed fields to the .meta.yaml sidecar and preserves existing structural compatibility fields; it never modifies prose, mirrors path-derived structure, or changes scene character/place relationship authority. Structural fields (part, chapter, chapter_id, chapter_title, timeline_position) are rejected here; use list_chapters plus assign_scene_to_chapter, move_scene, rename_chapter, or reorder_chapter for structure changes. Relationship fields (characters, places) are rejected here; use discovery workflows plus connect_character_place_evidence for paired sheet-backed character/place evidence, and audit_relationship_metadata for legacy sidecar/frontmatter relationship review. Allowed changes are immediately reflected in the index. Only available when the sync dir is writable.",
|
|
2218
2284
|
{
|
|
2219
2285
|
scene_id: z.string().describe("The scene_id to update (e.g. 'sc-011-sebastian')."),
|
|
2220
2286
|
project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
|
|
@@ -2231,8 +2297,8 @@ export function registerMetadataTools(s, {
|
|
|
2231
2297
|
timeline_position: z.number().int().optional().describe("Rejected by update_scene_metadata. Use move_scene for ordering changes."),
|
|
2232
2298
|
story_time: z.string().optional(),
|
|
2233
2299
|
tags: z.array(z.string()).optional(),
|
|
2234
|
-
characters: z.array(z.string()).optional(),
|
|
2235
|
-
places: z.array(z.string()).optional(),
|
|
2300
|
+
characters: z.array(z.string()).optional().describe("Rejected by update_scene_metadata. Use find_scenes, list_characters, list_places, connect_character_place_evidence for paired sheet-backed evidence, and audit_relationship_metadata for compatibility review."),
|
|
2301
|
+
places: z.array(z.string()).optional().describe("Rejected by update_scene_metadata. Use find_scenes, list_characters, list_places, connect_character_place_evidence for paired sheet-backed evidence, and audit_relationship_metadata for compatibility review."),
|
|
2236
2302
|
}).describe("Fields to update. Only supplied keys are changed."),
|
|
2237
2303
|
},
|
|
2238
2304
|
async ({ scene_id, project_id, fields }) => {
|
|
@@ -2244,6 +2310,18 @@ export function registerMetadataTools(s, {
|
|
|
2244
2310
|
if (!scene) {
|
|
2245
2311
|
return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
|
|
2246
2312
|
}
|
|
2313
|
+
const relationshipFields = getProvidedRelationshipSceneMetadataFields(fields);
|
|
2314
|
+
if (relationshipFields.length > 0) {
|
|
2315
|
+
return errorResponse(
|
|
2316
|
+
"VALIDATION_ERROR",
|
|
2317
|
+
"update_scene_metadata cannot change relationship-boundary fields characters or places. Scene relationship metadata is sheet-backed and must use outcome-level relationship workflows, not generic sidecar metadata writes.",
|
|
2318
|
+
buildRelationshipMetadataBoundaryDetails({
|
|
2319
|
+
projectId: project_id,
|
|
2320
|
+
sceneId: scene_id,
|
|
2321
|
+
blockedFields: relationshipFields,
|
|
2322
|
+
})
|
|
2323
|
+
);
|
|
2324
|
+
}
|
|
2247
2325
|
const structuralFields = getProvidedStructuralSceneMetadataFields(fields);
|
|
2248
2326
|
if (structuralFields.length > 0) {
|
|
2249
2327
|
return errorResponse(
|
|
@@ -2258,6 +2336,7 @@ export function registerMetadataTools(s, {
|
|
|
2258
2336
|
);
|
|
2259
2337
|
}
|
|
2260
2338
|
try {
|
|
2339
|
+
const relationshipSnapshot = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
|
|
2261
2340
|
const { sourceMeta } = readSourceMeta(scene.file_path, SYNC_DIR, { writable: true });
|
|
2262
2341
|
const updated = { ...sourceMeta, ...fields };
|
|
2263
2342
|
writeMeta(scene.file_path, updated, { syncDir: SYNC_DIR });
|
|
@@ -2267,6 +2346,17 @@ export function registerMetadataTools(s, {
|
|
|
2267
2346
|
indexSceneFile(db, SYNC_DIR, scene.file_path, normalizedUpdated, prose, {
|
|
2268
2347
|
managedStructure: isManagedStructureProject(db, project_id),
|
|
2269
2348
|
});
|
|
2349
|
+
restoreSceneRelationshipSnapshot(db, {
|
|
2350
|
+
sceneId: scene_id,
|
|
2351
|
+
projectId: project_id,
|
|
2352
|
+
snapshot: relationshipSnapshot,
|
|
2353
|
+
});
|
|
2354
|
+
restoreSceneRelationshipSearchKeywords(db, {
|
|
2355
|
+
sceneId: scene_id,
|
|
2356
|
+
projectId: project_id,
|
|
2357
|
+
meta: normalizedUpdated,
|
|
2358
|
+
snapshot: relationshipSnapshot,
|
|
2359
|
+
});
|
|
2270
2360
|
const backupResult = refreshProjectScopedBackupAfterMutation(db, {
|
|
2271
2361
|
syncDir: SYNC_DIR,
|
|
2272
2362
|
projectId: project_id,
|