@hanna84/mcp-writing 3.23.0 → 3.23.2
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 +14 -0
- package/README.md +3 -3
- package/package.json +1 -1
- package/src/sync/scene-character-batch.js +11 -1
- package/src/sync/sync.js +179 -38
- package/src/tools/metadata.js +74 -10
- package/src/tools/search.js +1 -1
- package/src/tools/sync.js +17 -4
- package/src/workflows/workflow-catalogue.js +5 -5
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.23.2](https://github.com/hannasdev/mcp-writing/compare/v3.23.1...v3.23.2)
|
|
8
|
+
|
|
9
|
+
- docs: clarify relationship workflow guidance [`#232`](https://github.com/hannasdev/mcp-writing/pull/232)
|
|
10
|
+
|
|
11
|
+
#### [v3.23.1](https://github.com/hannasdev/mcp-writing/compare/v3.23.0...v3.23.1)
|
|
12
|
+
|
|
13
|
+
> 30 May 2026
|
|
14
|
+
|
|
15
|
+
- fix: align relationship compatibility sync and audit [`#231`](https://github.com/hannasdev/mcp-writing/pull/231)
|
|
16
|
+
- Release 3.23.1 [`f0b643b`](https://github.com/hannasdev/mcp-writing/commit/f0b643bcb7b09adf066380d37718b09001dd3020)
|
|
17
|
+
|
|
7
18
|
#### [v3.23.0](https://github.com/hannasdev/mcp-writing/compare/v3.22.5...v3.23.0)
|
|
8
19
|
|
|
20
|
+
> 30 May 2026
|
|
21
|
+
|
|
9
22
|
- feat: guard scene relationship metadata updates [`#230`](https://github.com/hannasdev/mcp-writing/pull/230)
|
|
23
|
+
- Release 3.23.0 [`93c359f`](https://github.com/hannasdev/mcp-writing/commit/93c359f71dde69afed34e7b024aa21729810df2b)
|
|
10
24
|
|
|
11
25
|
#### [v3.22.5](https://github.com/hannasdev/mcp-writing/compare/v3.22.4...v3.22.5)
|
|
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:**
|
|
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)
|
|
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, and keep one-sided scene links for a deliberately named relationship workflow. 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
|
|
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
|
@@ -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 =
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
...
|
|
1460
|
-
...
|
|
1461
|
-
...
|
|
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
|
|
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 {
|
|
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);
|
package/src/tools/metadata.js
CHANGED
|
@@ -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 {
|
|
@@ -121,7 +121,7 @@ function buildRelationshipMetadataBoundaryDetails({ projectId, sceneId, blockedF
|
|
|
121
121
|
boundary: "scene_relationship_metadata",
|
|
122
122
|
relationship_tools: ["connect_character_place_evidence", "audit_relationship_metadata"],
|
|
123
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
|
|
124
|
+
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; use audit_relationship_metadata to review legacy sidecar/frontmatter relationship fields. Independent character-only or place-only scene links remain valid, but are not changed through update_scene_metadata.",
|
|
125
125
|
};
|
|
126
126
|
}
|
|
127
127
|
|
|
@@ -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 when evidence is paired, and leave independent 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,10 +1209,11 @@ 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: [
|
|
1152
|
-
"Use connect_character_place_evidence
|
|
1216
|
+
"Use connect_character_place_evidence when scene-backed character/place evidence is paired; independent one-sided links remain valid but need a deliberately named workflow.",
|
|
1153
1217
|
"Use record_character_relationship_beat for relationship arcs between characters.",
|
|
1154
1218
|
"Use link_reference_evidence for explicit reference evidence.",
|
|
1155
1219
|
"Use export_project_backup when a fresh recovery snapshot is needed.",
|
|
@@ -1376,7 +1440,7 @@ export function registerMetadataTools(s, {
|
|
|
1376
1440
|
// ---- connect_character_place_evidence ------------------------------------
|
|
1377
1441
|
s.tool(
|
|
1378
1442
|
"connect_character_place_evidence",
|
|
1379
|
-
"Connect a character and place as scene-backed story evidence. This
|
|
1443
|
+
"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 refreshed only as generated compatibility output. Independent character-only or place-only scene links remain valid, but are not handled by update_scene_metadata and need a deliberately named workflow if promoted later.",
|
|
1380
1444
|
{
|
|
1381
1445
|
project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
|
|
1382
1446
|
scene_id: z.string().describe("Scene that provides the evidence for this character/place association."),
|
|
@@ -1406,7 +1470,7 @@ export function registerMetadataTools(s, {
|
|
|
1406
1470
|
// ---- audit_relationship_metadata -----------------------------------------
|
|
1407
1471
|
s.tool(
|
|
1408
1472
|
"audit_relationship_metadata",
|
|
1409
|
-
"Review relationship metadata authority, stale indexes,
|
|
1473
|
+
"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.",
|
|
1410
1474
|
{
|
|
1411
1475
|
project_id: z.string().optional().describe("Optional project scope for the audit."),
|
|
1412
1476
|
},
|
|
@@ -2280,7 +2344,7 @@ export function registerMetadataTools(s, {
|
|
|
2280
2344
|
// ---- update_scene_metadata -----------------------------------------------
|
|
2281
2345
|
s.tool(
|
|
2282
2346
|
"update_scene_metadata",
|
|
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
|
|
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 when evidence is paired, and audit_relationship_metadata for legacy sidecar/frontmatter relationship review. Independent character-only or place-only scene links remain valid, but are intentionally not changed through this generic metadata tool. Allowed changes are immediately reflected in the index. Only available when the sync dir is writable.",
|
|
2284
2348
|
{
|
|
2285
2349
|
scene_id: z.string().describe("The scene_id to update (e.g. 'sc-011-sebastian')."),
|
|
2286
2350
|
project_id: z.string().describe("Project the scene belongs to (e.g. 'the-lamb')."),
|
|
@@ -2297,8 +2361,8 @@ export function registerMetadataTools(s, {
|
|
|
2297
2361
|
timeline_position: z.number().int().optional().describe("Rejected by update_scene_metadata. Use move_scene for ordering changes."),
|
|
2298
2362
|
story_time: z.string().optional(),
|
|
2299
2363
|
tags: 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
|
|
2301
|
-
places: z.array(z.string()).optional().describe("Rejected by update_scene_metadata. Use find_scenes, list_characters, list_places, connect_character_place_evidence
|
|
2364
|
+
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, and audit_relationship_metadata for compatibility review; independent character-only evidence remains valid but needs a deliberately named workflow."),
|
|
2365
|
+
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, and audit_relationship_metadata for compatibility review; independent place-only evidence remains valid but needs a deliberately named workflow."),
|
|
2302
2366
|
}).describe("Fields to update. Only supplied keys are changed."),
|
|
2303
2367
|
},
|
|
2304
2368
|
async ({ scene_id, project_id, fields }) => {
|
|
@@ -2543,7 +2607,7 @@ export function registerMetadataTools(s, {
|
|
|
2543
2607
|
// ---- update_place_sheet --------------------------------------------------
|
|
2544
2608
|
s.tool(
|
|
2545
2609
|
"update_place_sheet",
|
|
2546
|
-
"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.
|
|
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. Use connect_character_place_evidence when scene-backed character/place evidence is paired; independent place-only links remain valid but need a deliberately named workflow.",
|
|
2547
2611
|
{
|
|
2548
2612
|
place_id: z.string().describe("The place_id to update (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
|
|
2549
2613
|
fields: z.object({
|
|
@@ -2652,7 +2716,7 @@ export function registerMetadataTools(s, {
|
|
|
2652
2716
|
}),
|
|
2653
2717
|
non_canonical_fields: ["associated_characters", "tags"].filter(field => Object.hasOwn(fields, field)),
|
|
2654
2718
|
next_step: Object.hasOwn(fields, "associated_characters")
|
|
2655
|
-
? "Use connect_character_place_evidence
|
|
2719
|
+
? "Use connect_character_place_evidence when paired scene-backed character/place evidence should become authoritative; independent one-sided links remain valid but need a deliberately named workflow."
|
|
2656
2720
|
: undefined,
|
|
2657
2721
|
...backupMutationFields(backupResult),
|
|
2658
2722
|
});
|
package/src/tools/search.js
CHANGED
|
@@ -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
|
|
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; independent place-only links remain valid and need an explicit relationship workflow rather than a place-sheet read. 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
|
},
|
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, {
|
|
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, {
|
|
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, {
|
|
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, {
|
|
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);
|
|
@@ -68,12 +68,12 @@ 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
|
|
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. Independent character-only or place-only scene links remain valid, but need a deliberately named future workflow for daily repair." },
|
|
74
74
|
{ tool: "record_character_relationship_beat", note: "Use when a scene proves a relationship beat between two characters without exposing the character_relationships table." },
|
|
75
75
|
{ 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
|
|
76
|
+
{ 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
77
|
{ 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
78
|
{ 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
79
|
{ 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 +101,9 @@ export const WORKFLOW_CATALOGUE = [
|
|
|
101
101
|
steps: [
|
|
102
102
|
{ tool: "sync", note: "Refresh SQLite indexes from current compatibility inputs, then review warnings instead of patching sidecars as the first repair step." },
|
|
103
103
|
{ 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." },
|
|
104
|
+
{ 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
105
|
{ tool: "track_thread_arc", note: "Use for current thread authority instead of editing sidecar threads." },
|
|
106
|
-
{ tool: "connect_character_place_evidence", note: "Use for
|
|
106
|
+
{ 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; independent one-sided links remain valid but are outside this generic metadata path." },
|
|
107
107
|
{ tool: "link_reference_evidence", note: "Use for current reference-link authority instead of editing sidecar/frontmatter aliases." },
|
|
108
108
|
{ 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
109
|
],
|