@hanna84/mcp-writing 3.23.0 → 3.23.1

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.23.1](https://github.com/hannasdev/mcp-writing/compare/v3.23.0...v3.23.1)
8
+
9
+ - fix: align relationship compatibility sync and audit [`#231`](https://github.com/hannasdev/mcp-writing/pull/231)
10
+
7
11
  #### [v3.23.0](https://github.com/hannasdev/mcp-writing/compare/v3.22.5...v3.23.0)
8
12
 
13
+ > 30 May 2026
14
+
9
15
  - feat: guard scene relationship metadata updates [`#230`](https://github.com/hannasdev/mcp-writing/pull/230)
16
+ - Release 3.23.0 [`93c359f`](https://github.com/hannasdev/mcp-writing/commit/93c359f71dde69afed34e7b024aa21729810df2b)
10
17
 
11
18
  #### [v3.22.5](https://github.com/hannasdev/mcp-writing/compare/v3.22.4...v3.22.5)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.23.0",
3
+ "version": "3.23.1",
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",
@@ -57,6 +57,16 @@ function resolveCharacterEntry(entry, characterRows) {
57
57
  return resolveCharacterReference(entry, characterRows);
58
58
  }
59
59
 
60
+ function normalizeCompatibilityCharacters(value) {
61
+ const rawValues = Array.isArray(value)
62
+ ? value
63
+ : typeof value === "string"
64
+ ? value.split(",")
65
+ : [];
66
+
67
+ return [...new Set(rawValues.map(String).map(character => character.trim()).filter(Boolean))];
68
+ }
69
+
60
70
  function pruneLessSpecificCharacters(characterIds, fullNameMatches, characterRows) {
61
71
  const kept = new Set(characterIds);
62
72
 
@@ -140,7 +150,7 @@ export async function runSceneCharacterBatch({ syncDir, args, onProgress, should
140
150
  const { content: prose } = matter(raw);
141
151
  const { sourceMeta: meta } = readSourceMeta(scene.file_path, syncDir, { writable: !dry_run });
142
152
 
143
- const before_characters = [...new Set((meta.characters ?? []).map(String).filter(Boolean))];
153
+ const before_characters = normalizeCompatibilityCharacters(meta.characters);
144
154
  const normalized_before_characters = [...new Set(
145
155
  before_characters
146
156
  .map(character => resolveCharacterEntry(character, normalizedCharacterRows))
package/src/sync/sync.js CHANGED
@@ -299,6 +299,24 @@ export function normalizeReferenceIdList(values) {
299
299
  )];
300
300
  }
301
301
 
302
+ function normalizeMetadataList(values) {
303
+ const rawValues = Array.isArray(values)
304
+ ? values
305
+ : typeof values === "string"
306
+ ? values.split(",")
307
+ : [];
308
+
309
+ return [...new Set(
310
+ rawValues
311
+ .map(value => String(value).trim())
312
+ .filter(Boolean)
313
+ )];
314
+ }
315
+
316
+ function isVersionContinuityMarker(value) {
317
+ return /^v\d[\d.a-z]*$/i.test(String(value).trim());
318
+ }
319
+
302
320
  function normalizeReferenceRelation(value, fallbackRelation) {
303
321
  const normalized = String(value ?? fallbackRelation ?? "").trim().toLowerCase();
304
322
  if (/^[a-z][a-z0-9_-]*$/.test(normalized)) return normalized;
@@ -1272,7 +1290,131 @@ export function readSceneFileForSync(syncDir, file, { writable = false } = {}) {
1272
1290
  };
1273
1291
  }
1274
1292
 
1275
- export function indexSceneFile(db, syncDir, file, meta, prose, { observedStructure, managedStructure = false } = {}) {
1293
+ export function resolveSceneCharacterCompatibilityId(db, value) {
1294
+ let characterId = value;
1295
+ if (!/^char-/.test(value)) {
1296
+ let row = db.prepare(`SELECT character_id FROM characters WHERE lower(name) = lower(?)`).get(value);
1297
+ if (!row) {
1298
+ const words = value.toLowerCase().split(/\s+/).filter(Boolean);
1299
+ const all = db.prepare(`SELECT character_id, name FROM characters`).all();
1300
+ const match = all.find(r =>
1301
+ words.every(w => r.name.toLowerCase().includes(w))
1302
+ );
1303
+ if (match) row = match;
1304
+ }
1305
+ if (row) characterId = row.character_id;
1306
+ }
1307
+ return characterId;
1308
+ }
1309
+
1310
+ function querySceneRelationshipIds(db, { sceneId, projectId, table, idColumn }) {
1311
+ return db.prepare(`
1312
+ SELECT ${idColumn} AS id
1313
+ FROM ${table}
1314
+ WHERE scene_id = ? AND project_id = ?
1315
+ ORDER BY ${idColumn}
1316
+ `).all(sceneId, projectId).map((row) => row.id);
1317
+ }
1318
+
1319
+ function sameStringSet(a, b) {
1320
+ const left = [...new Set(a.map(String))].sort();
1321
+ const right = [...new Set(b.map(String))].sort();
1322
+ return left.length === right.length && left.every((value, index) => value === right[index]);
1323
+ }
1324
+
1325
+ function selectRelationshipIndexValues({ sceneId, projectId, relationshipKind, existing, compatibility, compatibilityMode, hasCompatibilityField, hasExistingScene }) {
1326
+ if (!hasCompatibilityField) {
1327
+ return {
1328
+ values: existing,
1329
+ diagnostic: null,
1330
+ };
1331
+ }
1332
+ if (compatibilityMode === "adopt_compatibility") {
1333
+ return {
1334
+ values: compatibility,
1335
+ diagnostic: null,
1336
+ };
1337
+ }
1338
+ if (hasExistingScene && !sameStringSet(existing, compatibility)) {
1339
+ return {
1340
+ values: existing,
1341
+ diagnostic: {
1342
+ type: "relationship_compatibility_drift",
1343
+ relationship_kind: relationshipKind,
1344
+ scene_id: sceneId,
1345
+ project_id: projectId,
1346
+ canonical_values: existing,
1347
+ compatibility_values: compatibility,
1348
+ message: `Relationship compatibility drift for scene "${sceneId}" in project "${projectId}": sidecar ${relationshipKind} differ from SQLite ${relationshipKind}; preserved SQLite relationship rows. Run audit_relationship_metadata before applying an outcome-level repair.`,
1349
+ },
1350
+ };
1351
+ }
1352
+ return {
1353
+ values: compatibility,
1354
+ diagnostic: null,
1355
+ };
1356
+ }
1357
+
1358
+ function relationshipCompatibilityFieldsFromMeta(meta = {}) {
1359
+ return {
1360
+ characters: Object.hasOwn(meta, "characters"),
1361
+ places: Object.hasOwn(meta, "places"),
1362
+ };
1363
+ }
1364
+
1365
+ function buildSceneRelationshipIndexPlan(db, { meta, projectId, compatibilityMode = "preserve_existing", compatibilityFields = relationshipCompatibilityFieldsFromMeta(meta), hasExistingScene = false }) {
1366
+ const compatibilityCharacters = [];
1367
+ const compatibilityVersionMarkers = [];
1368
+ for (const value of normalizeMetadataList(meta.characters)) {
1369
+ if (isVersionContinuityMarker(value)) {
1370
+ compatibilityVersionMarkers.push(value);
1371
+ continue;
1372
+ }
1373
+ compatibilityCharacters.push(resolveSceneCharacterCompatibilityId(db, value));
1374
+ }
1375
+ const compatibilityPlaces = normalizeMetadataList(meta.places);
1376
+ const existingCharacters = querySceneRelationshipIds(db, {
1377
+ sceneId: meta.scene_id,
1378
+ projectId,
1379
+ table: "scene_characters",
1380
+ idColumn: "character_id",
1381
+ });
1382
+ const existingPlaces = querySceneRelationshipIds(db, {
1383
+ sceneId: meta.scene_id,
1384
+ projectId,
1385
+ table: "scene_places",
1386
+ idColumn: "place_id",
1387
+ });
1388
+ const characterSelection = selectRelationshipIndexValues({
1389
+ sceneId: meta.scene_id,
1390
+ projectId,
1391
+ relationshipKind: "characters",
1392
+ existing: existingCharacters,
1393
+ compatibility: compatibilityCharacters,
1394
+ compatibilityMode,
1395
+ hasCompatibilityField: compatibilityFields.characters,
1396
+ hasExistingScene,
1397
+ });
1398
+ const placeSelection = selectRelationshipIndexValues({
1399
+ sceneId: meta.scene_id,
1400
+ projectId,
1401
+ relationshipKind: "places",
1402
+ existing: existingPlaces,
1403
+ compatibility: compatibilityPlaces,
1404
+ compatibilityMode,
1405
+ hasCompatibilityField: compatibilityFields.places,
1406
+ hasExistingScene,
1407
+ });
1408
+
1409
+ return {
1410
+ characters: characterSelection.values,
1411
+ places: placeSelection.values,
1412
+ compatibilityVersionMarkers,
1413
+ diagnostics: [characterSelection.diagnostic, placeSelection.diagnostic].filter(Boolean),
1414
+ };
1415
+ }
1416
+
1417
+ export function indexSceneFile(db, syncDir, file, meta, prose, { observedStructure, managedStructure = false, relationshipCompatibilityMode = "preserve_existing", relationshipCompatibilityFields } = {}) {
1276
1418
  const canonicalIndexPlan = buildCanonicalIndexPlan(db, syncDir, file, meta, observedStructure, { managedStructure });
1277
1419
  const { universeId: universe_id, projectId: project_id } = canonicalIndexPlan;
1278
1420
  const { chapterStructure } = canonicalIndexPlan.observedStructure;
@@ -1408,58 +1550,44 @@ export function indexSceneFile(db, syncDir, file, meta, prose, { observedStructu
1408
1550
  new Date().toISOString()
1409
1551
  );
1410
1552
 
1553
+ const relationshipIndexPlan = buildSceneRelationshipIndexPlan(db, {
1554
+ meta,
1555
+ projectId: project_id,
1556
+ compatibilityMode: relationshipCompatibilityMode,
1557
+ compatibilityFields: relationshipCompatibilityFields,
1558
+ hasExistingScene: Boolean(existing),
1559
+ });
1560
+ const tagValues = [
1561
+ ...normalizeMetadataList(meta.tags),
1562
+ ...relationshipIndexPlan.compatibilityVersionMarkers,
1563
+ ...normalizeMetadataList(meta.versions),
1564
+ ];
1565
+
1411
1566
  db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
1412
1567
  db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
1413
1568
  db.prepare(`DELETE FROM scene_tags WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
1569
+ db.prepare(`DELETE FROM scenes_fts WHERE scene_id = ? AND project_id = ?`).run(meta.scene_id, project_id);
1414
1570
 
1415
- for (const c of (meta.characters ?? [])) {
1416
- // Version continuity markers (e.g. v7.3, v3.3b) are tracked as tags, not characters
1417
- if (/^v\d[\d.a-z]*$/i.test(c)) {
1418
- db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(meta.scene_id, project_id, c);
1419
- continue;
1420
- }
1421
- let cid = c;
1422
- // If the value looks like a name rather than an ID, try to resolve it
1423
- if (!/^char-/.test(c)) {
1424
- // 1. Exact name match (case-insensitive)
1425
- let row = db.prepare(`SELECT character_id FROM characters WHERE lower(name) = lower(?)`).get(c);
1426
- // 2. Word-overlap: all words in the keyword appear in the stored name
1427
- // Handles "Victor Sidorin" → "Victor Alexeyvich Sidorin"
1428
- if (!row) {
1429
- const words = c.toLowerCase().split(/\s+/).filter(Boolean);
1430
- const all = db.prepare(`SELECT character_id, name FROM characters`).all();
1431
- const match = all.find(r =>
1432
- words.every(w => r.name.toLowerCase().includes(w))
1433
- );
1434
- if (match) row = match;
1435
- }
1436
- if (row) cid = row.character_id;
1437
- }
1571
+ for (const cid of relationshipIndexPlan.characters) {
1438
1572
  db.prepare(`INSERT OR IGNORE INTO scene_characters (scene_id, project_id, character_id) VALUES (?, ?, ?)`).run(
1439
1573
  meta.scene_id, project_id, cid
1440
1574
  );
1441
1575
  }
1442
- for (const p of (meta.places ?? [])) {
1576
+ for (const p of relationshipIndexPlan.places) {
1443
1577
  db.prepare(`INSERT OR IGNORE INTO scene_places (scene_id, project_id, place_id) VALUES (?, ?, ?)`).run(
1444
1578
  meta.scene_id, project_id, p
1445
1579
  );
1446
1580
  }
1447
- for (const t of (meta.tags ?? [])) {
1581
+ for (const t of tagValues) {
1448
1582
  db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
1449
1583
  meta.scene_id, project_id, t
1450
1584
  );
1451
1585
  }
1452
- for (const v of (meta.versions ?? [])) {
1453
- db.prepare(`INSERT OR IGNORE INTO scene_tags (scene_id, project_id, tag) VALUES (?, ?, ?)`).run(
1454
- meta.scene_id, project_id, v
1455
- );
1456
- }
1457
1586
 
1458
1587
  const keywordTokens = [
1459
- ...(meta.tags ?? []),
1460
- ...(meta.characters ?? []),
1461
- ...(meta.places ?? []),
1462
- ...(meta.versions ?? []),
1588
+ ...tagValues,
1589
+ ...relationshipIndexPlan.characters,
1590
+ ...relationshipIndexPlan.places,
1463
1591
  ]
1464
1592
  .filter(Boolean)
1465
1593
  .map(String)
@@ -1467,7 +1595,7 @@ export function indexSceneFile(db, syncDir, file, meta, prose, { observedStructu
1467
1595
  .filter(Boolean)
1468
1596
  .join(" ");
1469
1597
 
1470
- db.prepare(`INSERT OR REPLACE INTO scenes_fts (scene_id, project_id, logline, title, keywords) VALUES (?, ?, ?, ?, ?)`).run(
1598
+ db.prepare(`INSERT INTO scenes_fts (scene_id, project_id, logline, title, keywords) VALUES (?, ?, ?, ?, ?)`).run(
1471
1599
  meta.scene_id,
1472
1600
  project_id,
1473
1601
  meta.logline ?? meta.synopsis ?? "",
@@ -1493,7 +1621,13 @@ export function indexSceneFile(db, syncDir, file, meta, prose, { observedStructu
1493
1621
  relation: "informs",
1494
1622
  });
1495
1623
 
1496
- return { isStale, chapterId, warning: chapterWarning, canonicalIndexPlan };
1624
+ return {
1625
+ isStale,
1626
+ chapterId,
1627
+ warning: chapterWarning,
1628
+ canonicalIndexPlan,
1629
+ relationshipCompatibilityDiagnostics: relationshipIndexPlan.diagnostics,
1630
+ };
1497
1631
  }
1498
1632
 
1499
1633
  export function isManagedStructureProject(db, projectId) {
@@ -1564,6 +1698,7 @@ const WARNING_TYPE_LABELS = {
1564
1698
  unobserved_canonical_scene: "Unobserved canonical scene",
1565
1699
  missing_canonical_chapter: "Missing canonical chapter",
1566
1700
  missing_canonical_epigraph: "Missing canonical epigraph",
1701
+ relationship_compatibility_drift: "Relationship compatibility drift",
1567
1702
  nested_mirror: "Ignored nested mirror path",
1568
1703
  };
1569
1704
 
@@ -1578,6 +1713,7 @@ const WARNING_PATTERNS = [
1578
1713
  { type: "unobserved_canonical_scene", re: /^Managed sync preserved canonical scene not observed during sync scan:/ },
1579
1714
  { type: "missing_canonical_chapter", re: /^Managed sync preserved canonical chapter missing from filesystem:/ },
1580
1715
  { type: "missing_canonical_epigraph", re: /^Managed sync preserved canonical epigraph missing from filesystem:/ },
1716
+ { type: "relationship_compatibility_drift", re: /^Relationship compatibility drift/ },
1581
1717
  { type: "nested_mirror", re: /^Ignored nested mirror path:/ },
1582
1718
  ];
1583
1719
 
@@ -1598,7 +1734,7 @@ export function buildWarningSummary(warnings) {
1598
1734
  return summary;
1599
1735
  }
1600
1736
 
1601
- export function syncAll(db, syncDir, { quiet = false, writable = false } = {}) {
1737
+ export function syncAll(db, syncDir, { quiet = false, writable = false, relationshipCompatibilityMode = "preserve_existing" } = {}) {
1602
1738
  // Reset per-run inference cache so filesystem changes between sync calls
1603
1739
  // (for example after imports or path repairs) are reflected immediately.
1604
1740
  UNIVERSE_PROJECT_ROOT_CACHE.clear();
@@ -1701,6 +1837,8 @@ export function syncAll(db, syncDir, { quiet = false, writable = false } = {}) {
1701
1837
  const result = indexSceneFile(db, syncDir, file, meta, prose, {
1702
1838
  observedStructure: structureObservation,
1703
1839
  managedStructure: managedProjectIds.has(project_id),
1840
+ relationshipCompatibilityMode,
1841
+ relationshipCompatibilityFields: relationshipCompatibilityFieldsFromMeta(sourceMeta),
1704
1842
  });
1705
1843
  const canonicalDiagnostics = result.canonicalIndexPlan?.diagnostics ?? [];
1706
1844
  if (canonicalDiagnostics.length) {
@@ -1708,6 +1846,9 @@ export function syncAll(db, syncDir, { quiet = false, writable = false } = {}) {
1708
1846
  } else if (result.warning) {
1709
1847
  warnings.push(result.warning);
1710
1848
  }
1849
+ for (const diagnostic of result.relationshipCompatibilityDiagnostics ?? []) {
1850
+ warnings.push(diagnostic.message);
1851
+ }
1711
1852
  if (result.chapterId) {
1712
1853
  const chapterKey = `${result.chapterId}::${project_id}`;
1713
1854
  seenChapterKeys.add(chapterKey);
@@ -2,7 +2,7 @@ import { z } from "zod";
2
2
  import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import matter from "gray-matter";
5
- import { readMeta, readSourceMeta, writeMeta, indexSceneFile, isManagedStructureProject, normalizeSceneMetaForPath } from "../sync/sync.js";
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
8
  import {
@@ -454,6 +454,36 @@ function querySceneRelationshipSnapshot(db, { sceneId, projectId }) {
454
454
  };
455
455
  }
456
456
 
457
+ function sameStringSet(a, b) {
458
+ const left = [...new Set(a.map(String))].sort();
459
+ const right = [...new Set(b.map(String))].sort();
460
+ return left.length === right.length && left.every((value, index) => value === right[index]);
461
+ }
462
+
463
+ function normalizeSceneRelationshipCompatibilityFields(db, sourceMeta) {
464
+ const hasCharactersField = Object.hasOwn(sourceMeta, "characters");
465
+ const hasPlacesField = Object.hasOwn(sourceMeta, "places");
466
+ return {
467
+ has_relationship_fields: hasCharactersField || hasPlacesField,
468
+ has_characters_field: hasCharactersField,
469
+ has_places_field: hasPlacesField,
470
+ characters: normalizeStringList(sourceMeta.characters)
471
+ .filter((value) => !isVersionContinuityMarker(value))
472
+ .map((value) => resolveSceneCharacterCompatibilityId(db, value)),
473
+ places: normalizeStringList(sourceMeta.places),
474
+ };
475
+ }
476
+
477
+ function sceneRelationshipCompatibilityHasDrift(canonicalRelationships, compatibilityRelationships) {
478
+ return (
479
+ compatibilityRelationships.has_characters_field &&
480
+ !sameStringSet(canonicalRelationships.characters, compatibilityRelationships.characters)
481
+ ) || (
482
+ compatibilityRelationships.has_places_field &&
483
+ !sameStringSet(canonicalRelationships.places, compatibilityRelationships.places)
484
+ );
485
+ }
486
+
457
487
  function restoreSceneRelationshipSnapshot(db, { sceneId, projectId, snapshot }) {
458
488
  db.prepare(`DELETE FROM scene_characters WHERE scene_id = ? AND project_id = ?`).run(sceneId, projectId);
459
489
  db.prepare(`DELETE FROM scene_places WHERE scene_id = ? AND project_id = ?`).run(sceneId, projectId);
@@ -1061,6 +1091,39 @@ export function registerMetadataTools(s, {
1061
1091
  }
1062
1092
  try {
1063
1093
  const { sourceMeta } = readSourceMeta(scene.file_path, SYNC_DIR, { writable: false });
1094
+ const compatibilityRelationships = normalizeSceneRelationshipCompatibilityFields(db, sourceMeta);
1095
+ if (compatibilityRelationships.has_relationship_fields) {
1096
+ const canonicalRelationships = querySceneRelationshipSnapshot(db, {
1097
+ sceneId: scene.scene_id,
1098
+ projectId: scene.project_id,
1099
+ });
1100
+ diagnostics.push({
1101
+ type: "scene_relationship_compatibility_input",
1102
+ severity: "info",
1103
+ message: `Scene '${scene.scene_id}' retains sidecar/frontmatter character/place relationship fields as compatibility input; SQLite scene relationship rows remain canonical.`,
1104
+ scene_id: scene.scene_id,
1105
+ project_id: scene.project_id,
1106
+ compatibility: compatibilityRelationships,
1107
+ canonical: canonicalRelationships,
1108
+ authority: {
1109
+ canonical_owner: "SQLite scene_characters/scene_places",
1110
+ compatibility_mutation_surface: false,
1111
+ },
1112
+ next_step: "Use this as migration or review evidence only. Use outcome-level relationship tools for current repairs.",
1113
+ });
1114
+ if (sceneRelationshipCompatibilityHasDrift(canonicalRelationships, compatibilityRelationships)) {
1115
+ diagnostics.push({
1116
+ type: "scene_relationship_compatibility_drift",
1117
+ severity: "warning",
1118
+ message: `Scene '${scene.scene_id}' sidecar/frontmatter relationship fields disagree with canonical SQLite relationship rows.`,
1119
+ scene_id: scene.scene_id,
1120
+ project_id: scene.project_id,
1121
+ compatibility: compatibilityRelationships,
1122
+ 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.",
1124
+ });
1125
+ }
1126
+ }
1064
1127
  if (sourceMeta.threads) {
1065
1128
  diagnostics.push({
1066
1129
  type: "sidecar_threads_compatibility_input",
@@ -1146,6 +1209,7 @@ export function registerMetadataTools(s, {
1146
1209
  summary: {
1147
1210
  diagnostics_count: diagnostics.length,
1148
1211
  stale_scene_count: diagnostics.filter(diagnostic => diagnostic.type === "stale_scene_relationship_index").length,
1212
+ compatibility_drift_count: diagnostics.filter(diagnostic => diagnostic.type === "scene_relationship_compatibility_drift").length,
1149
1213
  compatibility_note_count: diagnostics.filter(diagnostic => diagnostic.severity === "info").length,
1150
1214
  },
1151
1215
  next_steps: [
package/src/tools/sync.js CHANGED
@@ -470,7 +470,10 @@ export function registerSyncTools(s, {
470
470
 
471
471
  let syncResult = null;
472
472
  if (!dry_run && !preflight && auto_sync) {
473
- syncResult = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
473
+ syncResult = syncAll(db, SYNC_DIR, {
474
+ writable: SYNC_DIR_WRITABLE,
475
+ relationshipCompatibilityMode: "adopt_compatibility",
476
+ });
474
477
  }
475
478
 
476
479
  return jsonResponse({
@@ -572,7 +575,10 @@ export function registerSyncTools(s, {
572
575
  },
573
576
  onComplete: (completedJob) => {
574
577
  if (!auto_sync || dry_run || preflight || completedJob.status !== "completed") return;
575
- const syncResult = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
578
+ const syncResult = syncAll(db, SYNC_DIR, {
579
+ writable: SYNC_DIR_WRITABLE,
580
+ relationshipCompatibilityMode: "adopt_compatibility",
581
+ });
576
582
  if (completedJob.result && completedJob.result.ok) {
577
583
  completedJob.result.sync = {
578
584
  indexed: syncResult.indexed,
@@ -652,7 +658,10 @@ export function registerSyncTools(s, {
652
658
  },
653
659
  onComplete: (completedJob) => {
654
660
  if (!auto_sync || dry_run || completedJob.status !== "completed") return;
655
- const syncResult = syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
661
+ const syncResult = syncAll(db, SYNC_DIR, {
662
+ writable: SYNC_DIR_WRITABLE,
663
+ relationshipCompatibilityMode: "adopt_compatibility",
664
+ });
656
665
  if (completedJob.result && completedJob.result.ok) {
657
666
  completedJob.result.sync = {
658
667
  indexed: syncResult.indexed,
@@ -776,7 +785,10 @@ export function registerSyncTools(s, {
776
785
  onComplete: (completedJob) => {
777
786
  if (dry_run || completedJob.status !== "completed" || !completedJob.result?.ok) return;
778
787
 
779
- syncAll(db, SYNC_DIR, { writable: SYNC_DIR_WRITABLE });
788
+ syncAll(db, SYNC_DIR, {
789
+ writable: SYNC_DIR_WRITABLE,
790
+ relationshipCompatibilityMode: "adopt_compatibility",
791
+ });
780
792
 
781
793
  const changedScenes = (completedJob.result.results ?? [])
782
794
  .filter(row => row.status === "changed")
@@ -980,6 +992,7 @@ export function registerSyncTools(s, {
980
992
  writeMeta(scene.file_path, sourceUpdatedMeta, { syncDir: SYNC_DIR });
981
993
  indexSceneFile(db, SYNC_DIR, scene.file_path, updatedMeta, prose, {
982
994
  managedStructure: isManagedStructureProject(db, scene.project_id),
995
+ relationshipCompatibilityMode: "adopt_compatibility",
983
996
  });
984
997
  db.prepare(`UPDATE scenes SET metadata_stale = 0 WHERE scene_id = ? AND project_id = ?`)
985
998
  .run(scene.scene_id, scene.project_id);