@hanna84/mcp-writing 3.26.0 → 3.27.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,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.27.0](https://github.com/hannasdev/mcp-writing/compare/v3.26.0...v3.27.0)
8
+
9
+ - feat(metadata): accept forgiving relationship evidence inputs [`#237`](https://github.com/hannasdev/mcp-writing/pull/237)
10
+
7
11
  #### [v3.26.0](https://github.com/hannasdev/mcp-writing/compare/v3.25.0...v3.26.0)
8
12
 
13
+ > 6 June 2026
14
+
9
15
  - feat: add canonical target resolver [`#236`](https://github.com/hannasdev/mcp-writing/pull/236)
16
+ - Release 3.26.0 [`bb01109`](https://github.com/hannasdev/mcp-writing/commit/bb01109bdef98ad7020ffabc4fd249017b51998c)
10
17
 
11
18
  #### [v3.25.0](https://github.com/hannasdev/mcp-writing/compare/v3.24.1...v3.25.0)
12
19
 
package/README.md CHANGED
@@ -152,7 +152,7 @@ Outcome: subplot structure stays visible and auditable, which reduces dropped th
152
152
  Goal: keep indexes accurate without manually re-tagging everything.
153
153
 
154
154
  1. After rewriting scenes, call `enrich_scene` to re-derive lightweight metadata from current prose.
155
- 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.
155
+ 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. Those relationship evidence tools prefer canonical IDs but also accept unambiguous scene titles, character names, place names, and case variants; ambiguous or suggested-only matches fail without mutating state. 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.
156
156
  3. Use `search_metadata` for keyword/FTS metadata searches across indexed titles, loglines, tags, characters, places, and versions, and use `find_scenes` to verify scenes are discoverable under structured filters. After identifying likely scenes, use `get_scene_prose` for prose context.
157
157
 
158
158
  Outcome: your AI assistant can reliably find the right scenes without drifting from the manuscript.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.26.0",
3
+ "version": "3.27.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",
@@ -211,6 +211,17 @@ function resolveFromRows({
211
211
  formatCandidate,
212
212
  }) {
213
213
  const normalizedInput = normalizeValue(input);
214
+ if (!normalizedInput) {
215
+ return buildResolutionFailure({
216
+ targetKind,
217
+ input,
218
+ projectId,
219
+ universeId,
220
+ candidateLimit,
221
+ candidates: [],
222
+ });
223
+ }
224
+
214
225
  const exactIdRows = rows.filter(row => row[idField] === input);
215
226
  if (exactIdRows.length === 1) {
216
227
  const candidate = formatCandidate(exactIdRows[0], { matchedField: idField, matchType: "exact_id" });
@@ -5,6 +5,11 @@ import matter from "gray-matter";
5
5
  import { readMeta, readSourceMeta, writeMeta, indexSceneFile, isManagedStructureProject, normalizeSceneMetaForPath, resolveSceneCharacterCompatibilityId } from "../sync/sync.js";
6
6
  import { validateProjectId, validateUniverseId } from "../sync/importer.js";
7
7
  import { resolveValidatedChapterFilter } from "../core/chapter-resolution.js";
8
+ import {
9
+ resolveCharacterTargetForProject,
10
+ resolvePlaceTargetForProject,
11
+ resolveSceneTarget,
12
+ } from "../core/canonical-target-resolution.js";
8
13
  import {
9
14
  FILESYSTEM_ARTIFACT_CLASSES,
10
15
  assertRegularFileReadTarget,
@@ -130,38 +135,26 @@ function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedF
130
135
  };
131
136
  }
132
137
 
133
- function relationshipEvidenceNotFoundDetails({ lookupKind, input, projectId, sceneId }) {
134
- const details = {
135
- lookup_kind: lookupKind,
136
- input,
137
- project_id: projectId,
138
- };
139
- if (sceneId !== undefined) {
140
- details.scene_id = sceneId;
141
- }
142
-
143
- if (lookupKind === "scene") {
144
- return {
145
- ...details,
146
- next_step: "Use find_scenes with the project_id to confirm the canonical scene_id, then retry the relationship evidence tool.",
147
- };
148
- }
138
+ function alreadyLinkedRelationshipNextStep({ entityKind }) {
139
+ return `This ${entityKind} was already linked to the scene, so no canonical relationship rows changed. Use the scene_relationships field in this response to inspect current links; call get_scene_prose with the scene_id and project_id if prose context is needed.`;
140
+ }
149
141
 
150
- if (lookupKind === "character") {
151
- return {
152
- ...details,
153
- next_step: "Use list_characters to find the stable character_id for this project or universe, then retry the relationship evidence tool.",
154
- };
142
+ function mergeResolvedFrom(...resolutions) {
143
+ const merged = {};
144
+ for (const resolution of resolutions) {
145
+ if (resolution?.resolved_from) {
146
+ Object.assign(merged, resolution.resolved_from);
147
+ }
155
148
  }
156
-
157
- return {
158
- ...details,
159
- next_step: "Use list_places to find the stable place_id for this project or universe, then retry the relationship evidence tool.",
160
- };
149
+ return Object.keys(merged).length > 0 ? merged : undefined;
161
150
  }
162
151
 
163
- function alreadyLinkedRelationshipNextStep({ entityKind }) {
164
- return `This ${entityKind} was already linked to the scene, so no canonical relationship rows changed. Use the scene_relationships field in this response to inspect current links; call get_scene_prose with the scene_id and project_id if prose context is needed.`;
152
+ function relationshipEvidenceResolutionError(errorResponse, resolution, { sceneId } = {}) {
153
+ const details = {
154
+ ...(resolution.error.details ?? {}),
155
+ ...(sceneId !== undefined ? { scene_id: sceneId } : {}),
156
+ };
157
+ return errorResponse(resolution.error.code, resolution.error.message, details);
165
158
  }
166
159
 
167
160
  function persistReferenceDocLink({ filePath, syncDir, targetDocId, relation }) {
@@ -461,21 +454,6 @@ function resolveCharacterForProject(db, { characterId, projectId }) {
461
454
  `).get(characterId, projectId, universeId);
462
455
  }
463
456
 
464
- function resolvePlaceForProject(db, { placeId, projectId }) {
465
- const universeId = getProjectUniverseId(db, projectId);
466
- return db.prepare(`
467
- SELECT place_id, project_id, universe_id, name
468
- FROM places
469
- WHERE place_id = ?
470
- AND (
471
- project_id = ?
472
- OR (universe_id IS NOT NULL AND universe_id = ?)
473
- OR (project_id IS NULL AND universe_id IS NULL)
474
- )
475
- LIMIT 1
476
- `).get(placeId, projectId, universeId);
477
- }
478
-
479
457
  function querySceneRelationshipSnapshot(db, { sceneId, projectId }) {
480
458
  return {
481
459
  characters: db.prepare(`
@@ -890,61 +868,53 @@ export function registerMetadataTools(s, {
890
868
  return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
891
869
  }
892
870
 
871
+ const sceneResolution = resolveSceneTarget(db, {
872
+ projectId: project_id,
873
+ input: scene_id,
874
+ argumentName: "scene_id",
875
+ });
876
+ if (!sceneResolution.ok) {
877
+ return relationshipEvidenceResolutionError(errorResponse, sceneResolution);
878
+ }
879
+ const canonicalSceneId = sceneResolution.scene_id;
880
+
893
881
  const scene = db.prepare(`
894
882
  SELECT scene_id, project_id, file_path
895
883
  FROM scenes
896
884
  WHERE scene_id = ? AND project_id = ?
897
- `).get(scene_id, project_id);
898
- if (!scene) {
899
- return errorResponse(
900
- "NOT_FOUND",
901
- `Scene '${scene_id}' not found in project '${project_id}'.`,
902
- relationshipEvidenceNotFoundDetails({
903
- lookupKind: "scene",
904
- input: scene_id,
905
- projectId: project_id,
906
- })
907
- );
908
- }
885
+ `).get(canonicalSceneId, project_id);
909
886
 
910
- const character = resolveCharacterForProject(db, { characterId: character_id, projectId: project_id });
911
- if (!character) {
912
- return errorResponse(
913
- "NOT_FOUND",
914
- `Character '${character_id}' is not indexed for project '${project_id}' or its universe.`,
915
- relationshipEvidenceNotFoundDetails({
916
- lookupKind: "character",
917
- input: character_id,
918
- projectId: project_id,
919
- sceneId: scene_id,
920
- })
921
- );
887
+ const characterResolution = resolveCharacterTargetForProject(db, {
888
+ projectId: project_id,
889
+ input: character_id,
890
+ argumentName: "character_id",
891
+ });
892
+ if (!characterResolution.ok) {
893
+ return relationshipEvidenceResolutionError(errorResponse, characterResolution, { sceneId: canonicalSceneId });
922
894
  }
923
- const place = resolvePlaceForProject(db, { placeId: place_id, projectId: project_id });
924
- if (!place) {
925
- return errorResponse(
926
- "NOT_FOUND",
927
- `Place '${place_id}' is not indexed for project '${project_id}' or its universe.`,
928
- relationshipEvidenceNotFoundDetails({
929
- lookupKind: "place",
930
- input: place_id,
931
- projectId: project_id,
932
- sceneId: scene_id,
933
- })
934
- );
895
+ const canonicalCharacterId = characterResolution.character_id;
896
+
897
+ const placeResolution = resolvePlaceTargetForProject(db, {
898
+ projectId: project_id,
899
+ input: place_id,
900
+ argumentName: "place_id",
901
+ });
902
+ if (!placeResolution.ok) {
903
+ return relationshipEvidenceResolutionError(errorResponse, placeResolution, { sceneId: canonicalSceneId });
935
904
  }
905
+ const canonicalPlaceId = placeResolution.place_id;
936
906
 
937
- const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
907
+ const before = querySceneRelationshipSnapshot(db, { sceneId: canonicalSceneId, projectId: project_id });
938
908
  try {
939
909
  db.exec("BEGIN");
940
910
  db.prepare(`
941
911
  INSERT OR IGNORE INTO scene_characters (scene_id, project_id, character_id)
942
912
  VALUES (?, ?, ?)
943
- `).run(scene_id, project_id, character_id);
913
+ `).run(canonicalSceneId, project_id, canonicalCharacterId);
944
914
  db.prepare(`
945
915
  INSERT OR IGNORE INTO scene_places (scene_id, project_id, place_id)
946
916
  VALUES (?, ?, ?)
947
- `).run(scene_id, project_id, place_id);
917
+ `).run(canonicalSceneId, project_id, canonicalPlaceId);
948
918
  db.exec("COMMIT");
949
919
  } catch (err) {
950
920
  try {
@@ -952,10 +922,10 @@ export function registerMetadataTools(s, {
952
922
  } catch (rollbackErr) {
953
923
  void rollbackErr;
954
924
  }
955
- return errorResponse("IO_ERROR", `Failed to connect character/place evidence for scene '${scene_id}': ${err.message}`);
925
+ return errorResponse("IO_ERROR", `Failed to connect character/place evidence for scene '${canonicalSceneId}': ${err.message}`);
956
926
  }
957
927
 
958
- const after = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
928
+ const after = querySceneRelationshipSnapshot(db, { sceneId: canonicalSceneId, projectId: project_id });
959
929
  const backupResult = refreshProjectScopedBackupAfterMutation(db, {
960
930
  syncDir: SYNC_DIR,
961
931
  projectId: project_id,
@@ -963,11 +933,11 @@ export function registerMetadataTools(s, {
963
933
  operation: "connect_character_place_evidence",
964
934
  actor: createToolActor("connect_character_place_evidence"),
965
935
  affected: {
966
- scenes: [scene_id],
967
- characters: [character_id],
968
- places: [place_id],
936
+ scenes: [canonicalSceneId],
937
+ characters: [canonicalCharacterId],
938
+ places: [canonicalPlaceId],
969
939
  },
970
- summary: `Connected character "${character_id}" and place "${place_id}" as evidence in scene "${scene_id}".`,
940
+ summary: `Connected character "${canonicalCharacterId}" and place "${canonicalPlaceId}" as evidence in scene "${canonicalSceneId}".`,
971
941
  before: {
972
942
  scene_relationships: before,
973
943
  },
@@ -984,10 +954,10 @@ export function registerMetadataTools(s, {
984
954
  compatibilityDiagnostics.push({
985
955
  code: "STALE_PATH",
986
956
  severity: "warning",
987
- message: `Canonical scene relationship evidence was committed, but scene '${scene_id}' has no indexed file path for generated compatibility output.`,
957
+ message: `Canonical scene relationship evidence was committed, but scene '${canonicalSceneId}' has no indexed file path for generated compatibility output.`,
988
958
  next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
989
959
  details: {
990
- scene_id,
960
+ scene_id: canonicalSceneId,
991
961
  project_id,
992
962
  indexed_path: null,
993
963
  },
@@ -996,7 +966,7 @@ export function registerMetadataTools(s, {
996
966
  try {
997
967
  writeSceneRelationshipCompatibilityOutput({
998
968
  db,
999
- sceneId: scene_id,
969
+ sceneId: canonicalSceneId,
1000
970
  projectId: project_id,
1001
971
  scenePath: scene.file_path,
1002
972
  syncDir: SYNC_DIR,
@@ -1009,7 +979,7 @@ export function registerMetadataTools(s, {
1009
979
  message: `Canonical scene relationship evidence was committed, but generated scene metadata compatibility output could not be refreshed: ${err.message}`,
1010
980
  next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
1011
981
  details: {
1012
- scene_id,
982
+ scene_id: canonicalSceneId,
1013
983
  project_id,
1014
984
  indexed_path: scene.file_path,
1015
985
  },
@@ -1017,13 +987,15 @@ export function registerMetadataTools(s, {
1017
987
  }
1018
988
  }
1019
989
 
990
+ const resolvedFrom = mergeResolvedFrom(sceneResolution, characterResolution, placeResolution);
1020
991
  return jsonResponse({
1021
992
  ok: true,
1022
993
  action: "connected",
1023
- scene_id,
994
+ scene_id: canonicalSceneId,
1024
995
  project_id,
1025
- character_id,
1026
- place_id,
996
+ character_id: canonicalCharacterId,
997
+ place_id: canonicalPlaceId,
998
+ ...(resolvedFrom ? { resolved_from: resolvedFrom } : {}),
1027
999
  note: note ?? null,
1028
1000
  mutation_order: [
1029
1001
  "validated_request",
@@ -1059,45 +1031,36 @@ export function registerMetadataTools(s, {
1059
1031
  return errorResponse("INVALID_PROJECT_ID", projectIdCheck.reason, { project_id });
1060
1032
  }
1061
1033
 
1034
+ const sceneResolution = resolveSceneTarget(db, {
1035
+ projectId: project_id,
1036
+ input: scene_id,
1037
+ argumentName: "scene_id",
1038
+ });
1039
+ if (!sceneResolution.ok) {
1040
+ return relationshipEvidenceResolutionError(errorResponse, sceneResolution);
1041
+ }
1042
+ const canonicalSceneId = sceneResolution.scene_id;
1043
+
1062
1044
  const scene = db.prepare(`
1063
1045
  SELECT scene_id, project_id, file_path
1064
1046
  FROM scenes
1065
1047
  WHERE scene_id = ? AND project_id = ?
1066
- `).get(scene_id, project_id);
1067
- if (!scene) {
1068
- return errorResponse(
1069
- "NOT_FOUND",
1070
- `Scene '${scene_id}' not found in project '${project_id}'.`,
1071
- relationshipEvidenceNotFoundDetails({
1072
- lookupKind: "scene",
1073
- input: scene_id,
1074
- projectId: project_id,
1075
- })
1076
- );
1077
- }
1048
+ `).get(canonicalSceneId, project_id);
1078
1049
 
1079
- if (!resolveEntity(entity_id)) {
1080
- const label = entityKind[0].toUpperCase() + entityKind.slice(1);
1081
- return errorResponse(
1082
- "NOT_FOUND",
1083
- `${label} '${entity_id}' is not indexed for project '${project_id}' or its universe.`,
1084
- relationshipEvidenceNotFoundDetails({
1085
- lookupKind: entityKind,
1086
- input: entity_id,
1087
- projectId: project_id,
1088
- sceneId: scene_id,
1089
- })
1090
- );
1050
+ const entityResolution = resolveEntity(entity_id);
1051
+ if (!entityResolution.ok) {
1052
+ return relationshipEvidenceResolutionError(errorResponse, entityResolution, { sceneId: canonicalSceneId });
1091
1053
  }
1054
+ const canonicalEntityId = entityResolution[idField];
1092
1055
 
1093
- const before = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
1094
- const alreadyLinked = before[entityKind === "character" ? "characters" : "places"].includes(entity_id);
1056
+ const before = querySceneRelationshipSnapshot(db, { sceneId: canonicalSceneId, projectId: project_id });
1057
+ const alreadyLinked = before[entityKind === "character" ? "characters" : "places"].includes(canonicalEntityId);
1095
1058
  try {
1096
1059
  db.exec("BEGIN");
1097
1060
  db.prepare(`
1098
1061
  INSERT OR IGNORE INTO ${tableName} (scene_id, project_id, ${idField})
1099
1062
  VALUES (?, ?, ?)
1100
- `).run(scene_id, project_id, entity_id);
1063
+ `).run(canonicalSceneId, project_id, canonicalEntityId);
1101
1064
  db.exec("COMMIT");
1102
1065
  } catch (err) {
1103
1066
  try {
@@ -1105,10 +1068,10 @@ export function registerMetadataTools(s, {
1105
1068
  } catch (rollbackErr) {
1106
1069
  void rollbackErr;
1107
1070
  }
1108
- return errorResponse("IO_ERROR", `Failed to connect scene ${entityKind} evidence for scene '${scene_id}': ${err.message}`);
1071
+ return errorResponse("IO_ERROR", `Failed to connect scene ${entityKind} evidence for scene '${canonicalSceneId}': ${err.message}`);
1109
1072
  }
1110
1073
 
1111
- const after = querySceneRelationshipSnapshot(db, { sceneId: scene_id, projectId: project_id });
1074
+ const after = querySceneRelationshipSnapshot(db, { sceneId: canonicalSceneId, projectId: project_id });
1112
1075
  const backupResult = refreshProjectScopedBackupAfterMutation(db, {
1113
1076
  syncDir: SYNC_DIR,
1114
1077
  projectId: project_id,
@@ -1116,10 +1079,10 @@ export function registerMetadataTools(s, {
1116
1079
  operation,
1117
1080
  actor: createToolActor(operation),
1118
1081
  affected: {
1119
- scenes: [scene_id],
1120
- [`${entityKind}s`]: [entity_id],
1082
+ scenes: [canonicalSceneId],
1083
+ [`${entityKind}s`]: [canonicalEntityId],
1121
1084
  },
1122
- summary: `Connected ${entityKind} "${entity_id}" as evidence in scene "${scene_id}".`,
1085
+ summary: `Connected ${entityKind} "${canonicalEntityId}" as evidence in scene "${canonicalSceneId}".`,
1123
1086
  before: {
1124
1087
  scene_relationships: before,
1125
1088
  },
@@ -1136,10 +1099,10 @@ export function registerMetadataTools(s, {
1136
1099
  compatibilityDiagnostics.push({
1137
1100
  code: "STALE_PATH",
1138
1101
  severity: "warning",
1139
- message: `Canonical scene ${entityKind} evidence was committed, but scene '${scene_id}' has no indexed file path for generated compatibility output.`,
1102
+ message: `Canonical scene ${entityKind} evidence was committed, but scene '${canonicalSceneId}' has no indexed file path for generated compatibility output.`,
1140
1103
  next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
1141
1104
  details: {
1142
- scene_id,
1105
+ scene_id: canonicalSceneId,
1143
1106
  project_id,
1144
1107
  indexed_path: null,
1145
1108
  },
@@ -1148,7 +1111,7 @@ export function registerMetadataTools(s, {
1148
1111
  try {
1149
1112
  writeSceneRelationshipCompatibilityOutput({
1150
1113
  db,
1151
- sceneId: scene_id,
1114
+ sceneId: canonicalSceneId,
1152
1115
  projectId: project_id,
1153
1116
  scenePath: scene.file_path,
1154
1117
  syncDir: SYNC_DIR,
@@ -1161,7 +1124,7 @@ export function registerMetadataTools(s, {
1161
1124
  message: `Canonical scene ${entityKind} evidence was committed, but generated scene metadata compatibility output could not be refreshed: ${err.message}`,
1162
1125
  next_step: "Treat SQLite and project backup artifacts as current. Run sync and inspect the indexed scene path before retrying compatibility output.",
1163
1126
  details: {
1164
- scene_id,
1127
+ scene_id: canonicalSceneId,
1165
1128
  project_id,
1166
1129
  indexed_path: scene.file_path,
1167
1130
  },
@@ -1169,6 +1132,7 @@ export function registerMetadataTools(s, {
1169
1132
  }
1170
1133
  }
1171
1134
 
1135
+ const resolvedFrom = mergeResolvedFrom(sceneResolution, entityResolution);
1172
1136
  return jsonResponse({
1173
1137
  ok: true,
1174
1138
  action: "connected",
@@ -1177,9 +1141,10 @@ export function registerMetadataTools(s, {
1177
1141
  ...(alreadyLinked
1178
1142
  ? { next_step: alreadyLinkedRelationshipNextStep({ entityKind }) }
1179
1143
  : {}),
1180
- scene_id,
1144
+ scene_id: canonicalSceneId,
1181
1145
  project_id,
1182
- [idField]: entity_id,
1146
+ [idField]: canonicalEntityId,
1147
+ ...(resolvedFrom ? { resolved_from: resolvedFrom } : {}),
1183
1148
  note: note ?? null,
1184
1149
  scene_relationships: after,
1185
1150
  mutation_order: [
@@ -1207,7 +1172,11 @@ export function registerMetadataTools(s, {
1207
1172
  entityKind: "character",
1208
1173
  idField: "character_id",
1209
1174
  tableName: "scene_characters",
1210
- resolveEntity: (characterId) => resolveCharacterForProject(db, { characterId, projectId: args.project_id }),
1175
+ resolveEntity: (characterId) => resolveCharacterTargetForProject(db, {
1176
+ projectId: args.project_id,
1177
+ input: characterId,
1178
+ argumentName: "character_id",
1179
+ }),
1211
1180
  });
1212
1181
  }
1213
1182
 
@@ -1222,7 +1191,11 @@ export function registerMetadataTools(s, {
1222
1191
  entityKind: "place",
1223
1192
  idField: "place_id",
1224
1193
  tableName: "scene_places",
1225
- resolveEntity: (placeId) => resolvePlaceForProject(db, { placeId, projectId: args.project_id }),
1194
+ resolveEntity: (placeId) => resolvePlaceTargetForProject(db, {
1195
+ projectId: args.project_id,
1196
+ input: placeId,
1197
+ argumentName: "place_id",
1198
+ }),
1226
1199
  });
1227
1200
  }
1228
1201
 
@@ -1733,12 +1706,12 @@ export function registerMetadataTools(s, {
1733
1706
  // ---- connect_character_place_evidence ------------------------------------
1734
1707
  s.tool(
1735
1708
  "connect_character_place_evidence",
1736
- "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.",
1709
+ "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. Canonical IDs are preferred; unambiguous case-insensitive scene titles, character names, place names, and ID variants are resolved to canonical IDs before mutation. Ambiguous or suggested-only matches fail without mutating state. Use connect_scene_character_evidence or connect_scene_place_evidence for one-sided scene evidence.",
1737
1710
  {
1738
1711
  project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1739
- scene_id: z.string().describe("Scene that provides the evidence for this character/place association."),
1740
- character_id: z.string().describe("Character present in the scene. Use list_characters to find valid IDs."),
1741
- place_id: z.string().describe("Place present in the scene. Use list_places to find valid IDs."),
1712
+ scene_id: z.string().describe("Scene that provides the evidence. Prefer canonical scene_id; an unambiguous case-insensitive scene_id or unique scene title in this project is also accepted."),
1713
+ character_id: z.string().describe("Character present in the scene. Prefer canonical character_id; an unambiguous case-insensitive character_id or character name in this project/universe scope is also accepted."),
1714
+ place_id: z.string().describe("Place present in the scene. Prefer canonical place_id; an unambiguous case-insensitive place_id or place name in this project/universe scope is also accepted."),
1742
1715
  note: z.string().optional().describe("Optional review note explaining the evidence. Stored in operation history, not in compatibility sidecars."),
1743
1716
  },
1744
1717
  async (args) => connectCharacterPlaceEvidence(args)
@@ -1747,11 +1720,11 @@ export function registerMetadataTools(s, {
1747
1720
  // ---- connect_scene_character_evidence -----------------------------------
1748
1721
  s.tool(
1749
1722
  "connect_scene_character_evidence",
1750
- "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.",
1723
+ "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. Canonical IDs are preferred; unambiguous case-insensitive scene titles, character names, and ID variants are resolved to canonical IDs before mutation. Ambiguous or suggested-only matches fail without mutating state.",
1751
1724
  {
1752
1725
  project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1753
- scene_id: z.string().describe("Scene that provides the evidence for this character."),
1754
- character_id: z.string().describe("Sheet-backed character_id present in the scene. Use list_characters to find valid IDs; freeform names are rejected."),
1726
+ scene_id: z.string().describe("Scene that provides the evidence. Prefer canonical scene_id; an unambiguous case-insensitive scene_id or unique scene title in this project is also accepted."),
1727
+ character_id: z.string().describe("Sheet-backed character present in the scene. Prefer canonical character_id; an unambiguous case-insensitive character_id or character name in this project/universe scope is also accepted."),
1755
1728
  note: z.string().optional().describe("Optional review note explaining the evidence. Stored in operation history, not in compatibility sidecars."),
1756
1729
  },
1757
1730
  async (args) => connectSceneCharacterEvidence(args)
@@ -1760,11 +1733,11 @@ export function registerMetadataTools(s, {
1760
1733
  // ---- connect_scene_place_evidence ---------------------------------------
1761
1734
  s.tool(
1762
1735
  "connect_scene_place_evidence",
1763
- "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.",
1736
+ "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. Canonical IDs are preferred; unambiguous case-insensitive scene titles, place names, and ID variants are resolved to canonical IDs before mutation. Ambiguous or suggested-only matches fail without mutating state.",
1764
1737
  {
1765
1738
  project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
1766
- scene_id: z.string().describe("Scene that provides the evidence for this place."),
1767
- place_id: z.string().describe("Sheet-backed place_id present in the scene. Use list_places to find valid IDs; freeform names are rejected."),
1739
+ scene_id: z.string().describe("Scene that provides the evidence. Prefer canonical scene_id; an unambiguous case-insensitive scene_id or unique scene title in this project is also accepted."),
1740
+ place_id: z.string().describe("Sheet-backed place present in the scene. Prefer canonical place_id; an unambiguous case-insensitive place_id or place name in this project/universe scope is also accepted."),
1768
1741
  note: z.string().optional().describe("Optional review note explaining the evidence. Stored in operation history, not in compatibility sidecars."),
1769
1742
  },
1770
1743
  async (args) => connectScenePlaceEvidence(args)