@hanna84/mcp-writing 3.23.1 → 3.24.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 CHANGED
@@ -4,9 +4,23 @@ 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.24.0](https://github.com/hannasdev/mcp-writing/compare/v3.23.2...v3.24.0)
8
+
9
+ - feat: add one-sided scene evidence workflows [`#233`](https://github.com/hannasdev/mcp-writing/pull/233)
10
+
11
+ #### [v3.23.2](https://github.com/hannasdev/mcp-writing/compare/v3.23.1...v3.23.2)
12
+
13
+ > 6 June 2026
14
+
15
+ - docs: clarify relationship workflow guidance [`#232`](https://github.com/hannasdev/mcp-writing/pull/232)
16
+ - Release 3.23.2 [`a4bd2e9`](https://github.com/hannasdev/mcp-writing/commit/a4bd2e9e957ebaf8b933325d04202411ab0b86e8)
17
+
7
18
  #### [v3.23.1](https://github.com/hannasdev/mcp-writing/compare/v3.23.0...v3.23.1)
8
19
 
20
+ > 30 May 2026
21
+
9
22
  - fix: align relationship compatibility sync and audit [`#231`](https://github.com/hannasdev/mcp-writing/pull/231)
23
+ - Release 3.23.1 [`f0b643b`](https://github.com/hannasdev/mcp-writing/commit/f0b643bcb7b09adf066380d37718b09001dd3020)
10
24
 
11
25
  #### [v3.23.0](https://github.com/hannasdev/mcp-writing/compare/v3.22.5...v3.23.0)
12
26
 
package/README.md CHANGED
@@ -30,7 +30,7 @@ Instead of feeding an entire manuscript to an AI and hoping it fits in the conte
30
30
  - **Core platform complete:** Metadata-first analysis, SQLite-canonical structural and relationship metadata, compatibility sidecar maintenance, AI-assisted prose editing with confirmation + git history, review bundles, and Scrivener Direct extraction are all implemented.
31
31
  - **Recently completed:** Database Backup and Recovery added project backup export, freshness diagnostics, advisory operation history, automatic backup refresh after sanctioned project-scoped canonical mutations, dry-run restore planning, transactional restore application, and backup/restore operations guidance.
32
32
  - **Previous milestone:** Docker, CI, and Deployment Workflow made Docker a supported way to build, run, smoke-test, and deploy Writing MCP.
33
- - **Active development:** Architecture Alignment Follow-up M5, documenting sidecar compatibility, migration, and deprecation expectations.
33
+ - **Active development:** Relationship Metadata Boundary M3, aligning workflow and generated documentation around SQLite-canonical relationship authority.
34
34
  - **Deferred backlog:** OpenClaw integration, client-agnostic setup, divisions, and embeddings search.
35
35
  - **Ideas and open questions:** tracked separately so future exploration does not distort the active roadmap.
36
36
 
@@ -153,7 +153,7 @@ Outcome: subplot structure stays visible and auditable, which reduces dropped th
153
153
  Goal: keep indexes accurate without manually re-tagging everything.
154
154
 
155
155
  1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
156
- 2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, status, and tags); use `list_chapters` plus `assign_scene_to_chapter` or `move_scene` for chapter placement and ordering. Numeric chapter filters are compatibility aliases for read scopes, not mutation targets.
156
+ 2. Use `update_scene_metadata` for intentional editorial fields (for example, beat, POV, status, and tags). It rejects scene `characters` and `places`; use `connect_character_place_evidence` when a scene proves paired sheet-backed character/place evidence, `connect_scene_character_evidence` for character-only evidence, and `connect_scene_place_evidence` for place-only evidence. Use `audit_relationship_metadata` for retained sidecar/frontmatter relationship fields. Use `list_chapters` plus `assign_scene_to_chapter` or `move_scene` for chapter placement and ordering.
157
157
  3. Use `search_metadata` and `find_scenes` to verify scenes are discoverable under the expected filters.
158
158
 
159
159
  Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
@@ -179,7 +179,7 @@ Goal: rebuild scene-to-character links in a controlled way after imported prose
179
179
  4. Re-run `enrich_scene_characters_batch` with `dry_run=false` once the preview looks correct.
180
180
  5. If you want a destructive overwrite instead of additive merge behavior, use `replace_mode=replace` with `confirm_replace=true` deliberately.
181
181
 
182
- Outcome: character-link maintenance becomes a preview-first batch operation instead of a one-off regex script or manual sidecar cleanup.
182
+ Outcome: character-link maintenance becomes a preview-first relationship repair operation instead of a one-off regex script or manual sidecar cleanup.
183
183
 
184
184
  ### 6) Post-upgrade recovery after legacy migration warnings
185
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.23.1",
3
+ "version": "3.24.0",
4
4
  "description": "MCP service for AI-assisted reasoning and editing on long-form fiction projects",
5
5
  "homepage": "https://hannasdev.github.io/mcp-writing/",
6
6
  "type": "module",
@@ -119,9 +119,14 @@ function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedF
119
119
  scene_id: sceneId,
120
120
  blocked_fields: blockedFields,
121
121
  boundary: "scene_relationship_metadata",
122
- relationship_tools: ["connect_character_place_evidence", "audit_relationship_metadata"],
122
+ relationship_tools: [
123
+ "connect_character_place_evidence",
124
+ "connect_scene_character_evidence",
125
+ "connect_scene_place_evidence",
126
+ "audit_relationship_metadata",
127
+ ],
123
128
  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.",
129
+ next_step: "Use find_scenes, list_characters, and list_places to identify stable IDs. Use connect_character_place_evidence when the scene proves paired sheet-backed character/place evidence, connect_scene_character_evidence for character-only evidence, connect_scene_place_evidence for place-only evidence, and audit_relationship_metadata to review legacy sidecar/frontmatter relationship fields.",
125
130
  };
126
131
  }
127
132
 
@@ -526,11 +531,50 @@ function buildSceneMetadataSearchKeywords(meta, relationshipSnapshot) {
526
531
  }
527
532
 
528
533
  function restoreSceneRelationshipSearchKeywords(db, { sceneId, projectId, meta, snapshot }) {
529
- db.prepare(`
530
- UPDATE scenes_fts
531
- SET keywords = ?
534
+ const existingFts = db.prepare(`
535
+ SELECT logline, title
536
+ FROM scenes_fts
532
537
  WHERE scene_id = ? AND project_id = ?
533
- `).run(buildSceneMetadataSearchKeywords(meta, snapshot), sceneId, projectId);
538
+ `).get(sceneId, projectId);
539
+ try {
540
+ db.exec("SAVEPOINT scene_relationship_fts_refresh");
541
+ db.prepare(`DELETE FROM scenes_fts WHERE scene_id = ? AND project_id = ?`).run(sceneId, projectId);
542
+ db.prepare(`
543
+ INSERT INTO scenes_fts (scene_id, project_id, logline, title, keywords)
544
+ VALUES (?, ?, ?, ?, ?)
545
+ `).run(
546
+ sceneId,
547
+ projectId,
548
+ meta.logline ?? meta.synopsis ?? existingFts?.logline ?? "",
549
+ meta.title ?? existingFts?.title ?? "",
550
+ buildSceneMetadataSearchKeywords(meta, snapshot),
551
+ );
552
+ db.exec("RELEASE scene_relationship_fts_refresh");
553
+ } catch (err) {
554
+ try {
555
+ db.exec("ROLLBACK TO scene_relationship_fts_refresh");
556
+ db.exec("RELEASE scene_relationship_fts_refresh");
557
+ } catch (rollbackErr) {
558
+ void rollbackErr;
559
+ }
560
+ throw err;
561
+ }
562
+ }
563
+
564
+ function writeSceneRelationshipCompatibilityOutput({ db, sceneId, projectId, scenePath, syncDir, snapshot }) {
565
+ const { sourceMeta } = readSourceMeta(scenePath, syncDir, { writable: true });
566
+ const nextMeta = {
567
+ ...sourceMeta,
568
+ characters: uniqueSorted(snapshot.characters),
569
+ places: uniqueSorted(snapshot.places),
570
+ };
571
+ writeMeta(scenePath, nextMeta, { syncDir });
572
+ restoreSceneRelationshipSearchKeywords(db, {
573
+ sceneId,
574
+ projectId,
575
+ meta: nextMeta,
576
+ snapshot,
577
+ });
534
578
  }
535
579
 
536
580
  export function registerMetadataTools(s, {
@@ -890,12 +934,14 @@ export function registerMetadataTools(s, {
890
934
  });
891
935
  } else {
892
936
  try {
893
- const { sourceMeta } = readSourceMeta(scene.file_path, SYNC_DIR, { writable: true });
894
- writeMeta(scene.file_path, {
895
- ...sourceMeta,
896
- characters: uniqueSorted([...normalizeStringList(sourceMeta.characters), character_id]),
897
- places: uniqueSorted([...normalizeStringList(sourceMeta.places), place_id]),
898
- }, { syncDir: SYNC_DIR });
937
+ writeSceneRelationshipCompatibilityOutput({
938
+ db,
939
+ sceneId: scene_id,
940
+ projectId: project_id,
941
+ scenePath: scene.file_path,
942
+ syncDir: SYNC_DIR,
943
+ snapshot: after,
944
+ });
899
945
  } catch (err) {
900
946
  compatibilityDiagnostics.push({
901
947
  code: err?.code ?? "COMPATIBILITY_OUTPUT_FAILED",
@@ -933,6 +979,172 @@ export function registerMetadataTools(s, {
933
979
  });
934
980
  }
935
981
 
982
+ async function connectOneSidedSceneEvidence({
983
+ project_id,
984
+ scene_id,
985
+ entity_id,
986
+ note,
987
+ }, {
988
+ operation,
989
+ entityKind,
990
+ idField,
991
+ tableName,
992
+ resolveEntity,
993
+ }) {
994
+ if (!SYNC_DIR_WRITABLE) {
995
+ return errorResponse("READ_ONLY", `Cannot connect scene ${entityKind} evidence: sync dir is read-only.`);
996
+ }
997
+ const projectIdCheck = validateProjectId(project_id);
998
+ if (!projectIdCheck.ok) {
999
+ return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
1000
+ }
1001
+
1002
+ const scene = db.prepare(`
1003
+ SELECT scene_id, project_id, file_path
1004
+ FROM scenes
1005
+ WHERE scene_id = ? AND project_id = ?
1006
+ `).get(scene_id, project_id);
1007
+ if (!scene) {
1008
+ return errorResponse("NOT_FOUND", `Scene '${scene_id}' not found in project '${project_id}'.`);
1009
+ }
1010
+
1011
+ if (!resolveEntity(entity_id)) {
1012
+ const label = entityKind[0].toUpperCase() + entityKind.slice(1);
1013
+ return errorResponse("NOT_FOUND", `${label} '${entity_id}' is not indexed for project '${project_id}' or its universe.`);
1014
+ }
1015
+
1016
+ const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
1017
+ const alreadyLinked = before[entityKind === "character" ? "characters" : "places"].includes(entity_id);
1018
+ try {
1019
+ db.exec("BEGIN");
1020
+ db.prepare(`
1021
+ INSERT OR IGNORE INTO ${tableName} (scene_id, project_id, ${idField})
1022
+ VALUES (?, ?, ?)
1023
+ `).run(scene_id, project_id, entity_id);
1024
+ db.exec("COMMIT");
1025
+ } catch (err) {
1026
+ try {
1027
+ db.exec("ROLLBACK");
1028
+ } catch (rollbackErr) {
1029
+ void rollbackErr;
1030
+ }
1031
+ return errorResponse("IO_ERROR", `Failed to connect scene ${entityKind} evidence for scene '${scene_id}': ${err.message}`);
1032
+ }
1033
+
1034
+ const after = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
1035
+ const backupResult = refreshProjectScopedBackupAfterMutation(db, {
1036
+ syncDir: SYNC_DIR,
1037
+ projectId: project_id,
1038
+ applicationVersion: MCP_SERVER_VERSION,
1039
+ operation,
1040
+ actor: createToolActor(operation),
1041
+ affected: {
1042
+ scenes: [scene_id],
1043
+ [`${entityKind}s`]: [entity_id],
1044
+ },
1045
+ summary: `Connected ${entityKind} "${entity_id}" as evidence in scene "${scene_id}".`,
1046
+ before: {
1047
+ scene_relationships: before,
1048
+ },
1049
+ after: {
1050
+ scene_relationships: after,
1051
+ },
1052
+ metadata: {
1053
+ note: note ?? null,
1054
+ },
1055
+ });
1056
+
1057
+ const compatibilityDiagnostics = [];
1058
+ if (!scene.file_path) {
1059
+ compatibilityDiagnostics.push({
1060
+ code: "STALE_PATH",
1061
+ severity: "warning",
1062
+ message: `Canonical scene ${entityKind} evidence was committed, but scene '${scene_id}' has no indexed file path for generated compatibility output.`,
1063
+ next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
1064
+ details: {
1065
+ scene_id,
1066
+ project_id,
1067
+ indexed_path: null,
1068
+ },
1069
+ });
1070
+ } else {
1071
+ try {
1072
+ writeSceneRelationshipCompatibilityOutput({
1073
+ db,
1074
+ sceneId: scene_id,
1075
+ projectId: project_id,
1076
+ scenePath: scene.file_path,
1077
+ syncDir: SYNC_DIR,
1078
+ snapshot: after,
1079
+ });
1080
+ } catch (err) {
1081
+ compatibilityDiagnostics.push({
1082
+ code: err?.code ?? "COMPATIBILITY_OUTPUT_FAILED",
1083
+ severity: "warning",
1084
+ message: `Canonical scene ${entityKind} evidence was committed, but generated scene metadata compatibility output could not be refreshed: ${err.message}`,
1085
+ next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
1086
+ details: {
1087
+ scene_id,
1088
+ project_id,
1089
+ indexed_path: scene.file_path,
1090
+ },
1091
+ });
1092
+ }
1093
+ }
1094
+
1095
+ return jsonResponse({
1096
+ ok: true,
1097
+ action: "connected",
1098
+ already_linked: alreadyLinked,
1099
+ scene_id,
1100
+ project_id,
1101
+ [idField]: entity_id,
1102
+ note: note ?? null,
1103
+ scene_relationships: after,
1104
+ mutation_order: [
1105
+ "validated_request",
1106
+ "sqlite_commit",
1107
+ "project_backup_refresh",
1108
+ "compatibility_output_refresh",
1109
+ ],
1110
+ compatibility_output: buildCompatibilityOutput({
1111
+ refreshed: compatibilityDiagnostics.length === 0,
1112
+ diagnostics: compatibilityDiagnostics,
1113
+ }),
1114
+ ...backupMutationFields(backupResult),
1115
+ });
1116
+ }
1117
+
1118
+ async function connectSceneCharacterEvidence(args) {
1119
+ return connectOneSidedSceneEvidence({
1120
+ project_id: args.project_id,
1121
+ scene_id: args.scene_id,
1122
+ entity_id: args.character_id,
1123
+ note: args.note,
1124
+ }, {
1125
+ operation: "connect_scene_character_evidence",
1126
+ entityKind: "character",
1127
+ idField: "character_id",
1128
+ tableName: "scene_characters",
1129
+ resolveEntity: (characterId) => resolveCharacterForProject(db, { characterId, projectId: args.project_id }),
1130
+ });
1131
+ }
1132
+
1133
+ async function connectScenePlaceEvidence(args) {
1134
+ return connectOneSidedSceneEvidence({
1135
+ project_id: args.project_id,
1136
+ scene_id: args.scene_id,
1137
+ entity_id: args.place_id,
1138
+ note: args.note,
1139
+ }, {
1140
+ operation: "connect_scene_place_evidence",
1141
+ entityKind: "place",
1142
+ idField: "place_id",
1143
+ tableName: "scene_places",
1144
+ resolveEntity: (placeId) => resolvePlaceForProject(db, { placeId, projectId: args.project_id }),
1145
+ });
1146
+ }
1147
+
936
1148
  async function recordCharacterRelationshipBeat({
937
1149
  project_id,
938
1150
  from_character,
@@ -1120,7 +1332,7 @@ export function registerMetadataTools(s, {
1120
1332
  project_id: scene.project_id,
1121
1333
  compatibility: compatibilityRelationships,
1122
1334
  canonical: canonicalRelationships,
1123
- next_step: "Treat SQLite relationship rows as canonical. Use find_scenes, list_characters, and list_places to inspect stable IDs; use connect_character_place_evidence for paired sheet-backed evidence or leave character-only/place-only repairs to a deliberately named future workflow.",
1335
+ next_step: "Treat SQLite relationship rows as canonical. Use find_scenes, list_characters, and list_places to inspect stable IDs; use connect_character_place_evidence when evidence is paired, connect_scene_character_evidence for character-only evidence, and connect_scene_place_evidence for place-only evidence.",
1124
1336
  });
1125
1337
  }
1126
1338
  }
@@ -1213,7 +1425,7 @@ export function registerMetadataTools(s, {
1213
1425
  compatibility_note_count: diagnostics.filter(diagnostic => diagnostic.severity === "info").length,
1214
1426
  },
1215
1427
  next_steps: [
1216
- "Use connect_character_place_evidence for scene-backed character/place relationships.",
1428
+ "Use connect_character_place_evidence when scene-backed character/place evidence is paired; use connect_scene_character_evidence or connect_scene_place_evidence for one-sided scene evidence.",
1217
1429
  "Use record_character_relationship_beat for relationship arcs between characters.",
1218
1430
  "Use link_reference_evidence for explicit reference evidence.",
1219
1431
  "Use export_project_backup when a fresh recovery snapshot is needed.",
@@ -1440,7 +1652,7 @@ export function registerMetadataTools(s, {
1440
1652
  // ---- connect_character_place_evidence ------------------------------------
1441
1653
  s.tool(
1442
1654
  "connect_character_place_evidence",
1443
- "Connect a character and place as scene-backed story evidence. This is the outcome-level workflow for character/place association: SQLite scene relationship indexes commit first, project backups refresh after commit, and scene sidecar characters/places are refreshed only as generated compatibility output.",
1655
+ "Connect a character and place as paired scene-backed story evidence. This outcome-level workflow covers sheet-backed character/place associations: SQLite scene relationship indexes commit first, project backups refresh after commit, and scene sidecar characters/places are regenerated only as generated compatibility output from canonical indexes. Use connect_scene_character_evidence or connect_scene_place_evidence for one-sided scene evidence.",
1444
1656
  {
1445
1657
  project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1446
1658
  scene_id: z.string().describe("Scene that provides the evidence for this character/place association."),
@@ -1451,6 +1663,32 @@ export function registerMetadataTools(s, {
1451
1663
  async (args) => connectCharacterPlaceEvidence(args)
1452
1664
  );
1453
1665
 
1666
+ // ---- connect_scene_character_evidence -----------------------------------
1667
+ s.tool(
1668
+ "connect_scene_character_evidence",
1669
+ "Connect a sheet-backed character to a scene without requiring paired place evidence. This outcome-level workflow records character-only scene evidence in SQLite first, refreshes project backups after commit, and regenerates scene sidecar characters/places only as generated compatibility output from canonical indexes.",
1670
+ {
1671
+ project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1672
+ scene_id: z.string().describe("Scene that provides the evidence for this character."),
1673
+ character_id: z.string().describe("Sheet-backed character_id present in the scene. Use list_characters to find valid IDs; freeform names are rejected."),
1674
+ note: z.string().optional().describe("Optional review note explaining the evidence. Stored in operation history, not in compatibility sidecars."),
1675
+ },
1676
+ async (args) => connectSceneCharacterEvidence(args)
1677
+ );
1678
+
1679
+ // ---- connect_scene_place_evidence ---------------------------------------
1680
+ s.tool(
1681
+ "connect_scene_place_evidence",
1682
+ "Connect a sheet-backed place to a scene without requiring paired character evidence. This outcome-level workflow records place-only scene evidence in SQLite first, refreshes project backups after commit, and regenerates scene sidecar characters/places only as generated compatibility output from canonical indexes.",
1683
+ {
1684
+ project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1685
+ scene_id: z.string().describe("Scene that provides the evidence for this place."),
1686
+ place_id: z.string().describe("Sheet-backed place_id present in the scene. Use list_places to find valid IDs; freeform names are rejected."),
1687
+ note: z.string().optional().describe("Optional review note explaining the evidence. Stored in operation history, not in compatibility sidecars."),
1688
+ },
1689
+ async (args) => connectScenePlaceEvidence(args)
1690
+ );
1691
+
1454
1692
  // ---- record_character_relationship_beat ----------------------------------
1455
1693
  s.tool(
1456
1694
  "record_character_relationship_beat",
@@ -1470,7 +1708,7 @@ export function registerMetadataTools(s, {
1470
1708
  // ---- audit_relationship_metadata -----------------------------------------
1471
1709
  s.tool(
1472
1710
  "audit_relationship_metadata",
1473
- "Review relationship metadata authority, stale indexes, and retained compatibility notes without mutating SQLite or files. Use this before repair work when character/place associations, sidecar tags, scene threads, or recovery readiness look stale or ambiguous.",
1711
+ "Review relationship metadata authority, stale indexes, retained compatibility notes, and scene character/place sidecar drift without mutating SQLite or files. Use this before repair work when character/place associations, sidecar tags, scene threads, or recovery readiness look stale or ambiguous.",
1474
1712
  {
1475
1713
  project_id: z.string().optional().describe("Optional project scope for the audit."),
1476
1714
  },
@@ -2344,7 +2582,7 @@ export function registerMetadataTools(s, {
2344
2582
  // ---- update_scene_metadata -----------------------------------------------
2345
2583
  s.tool(
2346
2584
  "update_scene_metadata",
2347
- "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.",
2585
+ "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 when evidence is paired, connect_scene_character_evidence for character-only evidence, connect_scene_place_evidence for place-only 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.",
2348
2586
  {
2349
2587
  scene_id: z.string().describe("The scene_id to update (e.g. 'sc-011-sebastian')."),
2350
2588
  project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
@@ -2361,8 +2599,8 @@ export function registerMetadataTools(s, {
2361
2599
  timeline_position: z.number().int().optional().describe("Rejected by update_scene_metadata. Use move_scene for ordering changes."),
2362
2600
  story_time: z.string().optional(),
2363
2601
  tags: z.array(z.string()).optional(),
2364
- 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."),
2365
- 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."),
2602
+ characters: z.array(z.string()).optional().describe("Rejected by update_scene_metadata. Use find_scenes, list_characters, list_places, connect_character_place_evidence when evidence is paired, connect_scene_character_evidence for character-only evidence, and audit_relationship_metadata for compatibility review."),
2603
+ places: z.array(z.string()).optional().describe("Rejected by update_scene_metadata. Use find_scenes, list_characters, list_places, connect_character_place_evidence when evidence is paired, connect_scene_place_evidence for place-only evidence, and audit_relationship_metadata for compatibility review."),
2366
2604
  }).describe("Fields to update. Only supplied keys are changed."),
2367
2605
  },
2368
2606
  async ({ scene_id, project_id, fields }) => {
@@ -2607,7 +2845,7 @@ export function registerMetadataTools(s, {
2607
2845
  // ---- update_place_sheet --------------------------------------------------
2608
2846
  s.tool(
2609
2847
  "update_place_sheet",
2610
- "Update canonical place profile fields and retained compatibility notes. The place name commits to SQLite first and refreshes project backups; associated_characters and tags are compatibility/review metadata only. For current character/place relationship authority, use connect_character_place_evidence.",
2848
+ "Update canonical place profile fields and retained compatibility notes. The place name commits to SQLite first and refreshes project backups; associated_characters and tags are compatibility/review metadata only. Use connect_character_place_evidence when scene-backed character/place evidence is paired, or connect_scene_place_evidence when scene evidence is place-only.",
2611
2849
  {
2612
2850
  place_id: z.string().describe("The place_id to update (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
2613
2851
  fields: z.object({
@@ -2716,7 +2954,7 @@ export function registerMetadataTools(s, {
2716
2954
  }),
2717
2955
  non_canonical_fields: ["associated_characters", "tags"].filter(field => Object.hasOwn(fields, field)),
2718
2956
  next_step: Object.hasOwn(fields, "associated_characters")
2719
- ? "Use connect_character_place_evidence to make scene-backed character/place associations authoritative."
2957
+ ? "Use connect_character_place_evidence when paired scene-backed character/place evidence should become authoritative; use connect_scene_character_evidence or connect_scene_place_evidence for one-sided scene evidence."
2720
2958
  : undefined,
2721
2959
  ...backupMutationFields(backupResult),
2722
2960
  });
@@ -609,7 +609,7 @@ export function registerSearchTools(s, {
609
609
  // ---- get_place_sheet -----------------------------------------------------
610
610
  s.tool(
611
611
  "get_place_sheet",
612
- "Get full place details, including canonical sheet content plus retained sidecar associated_characters and tags as compatibility/review notes. Use connect_character_place_evidence for current scene-backed character/place authority. Response shape note: returns a structured envelope (`results`, `total_count`) with one result row.",
612
+ "Get full place details, including canonical sheet content plus retained sidecar associated_characters and tags as compatibility/review notes. Use connect_character_place_evidence when scene-backed character/place evidence is paired, or connect_scene_place_evidence when scene evidence is place-only. Response shape note: returns a structured envelope (`results`, `total_count`) with one result row.",
613
613
  {
614
614
  place_id: z.string().describe("The place_id to look up (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
615
615
  },
@@ -68,12 +68,14 @@ export const WORKFLOW_CATALOGUE = [
68
68
  label: "Track and repair story relationships",
69
69
  use_when: "Use when the user wants to track an arc, link evidence, review stale relationships, repair metadata parity, or prepare generated recovery material for relationship metadata.",
70
70
  steps: [
71
- { tool: "find_scenes", note: "Identify scene_id and project_id from story context before recording relationships." },
71
+ { tool: "find_scenes", note: "Identify scene_id and project_id from story context before recording relationships; use list_characters and list_places when stable entity IDs need disambiguation." },
72
72
  { tool: "track_thread_arc", note: "Use when a scene should carry a storyline, subplot, setup, escalation, reveal, reversal, payoff, or other thread beat." },
73
- { tool: "connect_character_place_evidence", note: "Use when a scene proves a character/place association; SQLite scene relationship indexes commit first and sidecar characters/places remain generated compatibility output." },
73
+ { tool: "connect_character_place_evidence", note: "Use when a scene proves paired sheet-backed character/place evidence; SQLite scene relationship indexes commit first and sidecar characters/places remain generated compatibility output." },
74
+ { tool: "connect_scene_character_evidence", note: "Use when a scene proves sheet-backed character evidence without a specific place association; SQLite scene character links commit first and sidecar characters/places remain generated compatibility output." },
75
+ { tool: "connect_scene_place_evidence", note: "Use when a scene proves sheet-backed place evidence without a specific character association; SQLite scene place links commit first and sidecar characters/places remain generated compatibility output." },
74
76
  { tool: "record_character_relationship_beat", note: "Use when a scene proves a relationship beat between two characters without exposing the character_relationships table." },
75
77
  { tool: "link_reference_evidence", note: "Use when scene, character, place, or reference evidence should point to a reference document; SQLite commits first and compatibility output is generated transparency." },
76
- { tool: "audit_relationship_metadata", note: "Use before repair work to review stale relationship indexes and sidecar-only compatibility notes without mutating canonical state." },
78
+ { tool: "audit_relationship_metadata", note: "Use before repair work to review stale relationship indexes, retained sidecar/frontmatter characters or places, and compatibility drift without mutating canonical state." },
77
79
  { tool: "suggest_scene_references", note: "Use preview first to review candidate scene-reference links from character/place evidence; use apply only after the relationship outcome is intended." },
78
80
  { tool: "enrich_scene_characters_batch", note: "Use dry_run first when prose-derived character relationship parity needs repair across multiple scenes; apply mode syncs SQLite after compatibility output and refreshes backups for changed scenes." },
79
81
  { tool: "diagnose_project_backups", note: "Relationship mutation tools refresh project backups after canonical commits; run this if backup_warnings were returned or recovery readiness matters." },
@@ -101,9 +103,11 @@ export const WORKFLOW_CATALOGUE = [
101
103
  steps: [
102
104
  { tool: "sync", note: "Refresh SQLite indexes from current compatibility inputs, then review warnings instead of patching sidecars as the first repair step." },
103
105
  { tool: "diagnose_structure", note: "Use when sidecar, frontmatter, folder, chapter, epigraph, or generated-export structure appears to disagree with SQLite canonical state." },
104
- { tool: "audit_relationship_metadata", note: "Use when sidecar threads, tags, flags, associated_characters, characters, places, or reference aliases need authority classification before repair." },
106
+ { tool: "audit_relationship_metadata", note: "Use when sidecar threads, tags, flags, associated_characters, characters, places, or reference aliases need authority classification before repair; relationship fields are compatibility input/review evidence, not generic metadata writes." },
105
107
  { tool: "track_thread_arc", note: "Use for current thread authority instead of editing sidecar threads." },
106
- { tool: "connect_character_place_evidence", note: "Use for current scene-backed character/place authority instead of editing sidecar relationship lists." },
108
+ { tool: "connect_character_place_evidence", note: "Use for paired scene-backed character/place evidence instead of editing sidecar relationship lists or sending characters/places through update_scene_metadata." },
109
+ { tool: "connect_scene_character_evidence", note: "Use for character-only scene evidence instead of editing sidecar characters or sending characters through update_scene_metadata." },
110
+ { tool: "connect_scene_place_evidence", note: "Use for place-only scene evidence instead of editing sidecar places or sending places through update_scene_metadata." },
107
111
  { tool: "link_reference_evidence", note: "Use for current reference-link authority instead of editing sidecar/frontmatter aliases." },
108
112
  { tool: "export_project_backup", note: "Generate a recovery snapshot from SQLite canonical state after meaningful canonical migration or repair work; editing backup artifacts does not mutate current state." },
109
113
  ],