@hanna84/mcp-writing 3.21.2 → 3.22.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 +14 -0
- package/package.json +1 -1
- package/src/scripts/normalize-scene-characters.mjs +5 -5
- package/src/sync/scene-character-batch.js +13 -4
- package/src/sync/sync.js +29 -9
- package/src/tools/metadata.js +987 -313
- package/src/tools/reference-link-persistence.js +7 -2
- package/src/tools/search.js +84 -44
- package/src/tools/sync.js +20 -7
- package/src/workflows/workflow-catalogue.js +20 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readSourceMeta, writeMeta, normalizeReferenceLinkList } from "../sync/sync.js";
|
|
2
2
|
|
|
3
3
|
let savepointCounter = 0;
|
|
4
4
|
|
|
@@ -13,7 +13,7 @@ export function upsertSerializedReferenceLinks(existing, targetDocId, relation,
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export function persistSceneReferenceLink({ scenePath, syncDir, targetDocId, relation }) {
|
|
16
|
-
const { meta } =
|
|
16
|
+
const { sourceMeta: meta } = readSourceMeta(scenePath, syncDir, { writable: true });
|
|
17
17
|
const existingExplicit = [
|
|
18
18
|
...(Array.isArray(meta.reference_links) ? meta.reference_links : meta.reference_links ? [meta.reference_links] : []),
|
|
19
19
|
...(Array.isArray(meta.explicit_reference_links) ? meta.explicit_reference_links : meta.explicit_reference_links ? [meta.explicit_reference_links] : []),
|
|
@@ -59,6 +59,11 @@ export function upsertExplicitReferenceLinkRow(
|
|
|
59
59
|
insertStmt.run(sourceKind, sourceProjectId, sourceId, targetDocId, relation);
|
|
60
60
|
};
|
|
61
61
|
|
|
62
|
+
if (db.inTransaction) {
|
|
63
|
+
runUpsertBody();
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
62
67
|
if (typeof db.transaction === "function") {
|
|
63
68
|
db.transaction(runUpsertBody)();
|
|
64
69
|
return;
|
package/src/tools/search.js
CHANGED
|
@@ -2,7 +2,10 @@ import { z } from "zod";
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import matter from "gray-matter";
|
|
4
4
|
import { readMeta } from "../sync/sync.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
persistSceneReferenceLink,
|
|
7
|
+
upsertExplicitReferenceLinkRow,
|
|
8
|
+
} from "./reference-link-persistence.js";
|
|
6
9
|
import { resolveValidatedChapterFilter } from "../core/chapter-resolution.js";
|
|
7
10
|
import {
|
|
8
11
|
createToolActor,
|
|
@@ -606,7 +609,7 @@ export function registerSearchTools(s, {
|
|
|
606
609
|
// ---- get_place_sheet -----------------------------------------------------
|
|
607
610
|
s.tool(
|
|
608
611
|
"get_place_sheet",
|
|
609
|
-
"Get full place details
|
|
612
|
+
"Get full place details, including canonical sheet content plus retained sidecar associated_characters and tags as compatibility/review notes. Use connect_character_place_evidence for current scene-backed character/place authority. Response shape note: returns a structured envelope (`results`, `total_count`) with one result row.",
|
|
610
613
|
{
|
|
611
614
|
place_id: z.string().describe("The place_id to look up (e.g. 'place-harbor-district'). Use list_places to find valid IDs."),
|
|
612
615
|
},
|
|
@@ -1073,7 +1076,7 @@ export function registerSearchTools(s, {
|
|
|
1073
1076
|
// ---- suggest_scene_references --------------------------------------------
|
|
1074
1077
|
s.tool(
|
|
1075
1078
|
"suggest_scene_references",
|
|
1076
|
-
"Suggest reference documents for a scene by aggregating links from the scene's characters and places. Returns weighted candidates ranked by how many entities in the scene link to each reference. Excludes any explicit scene
|
|
1079
|
+
"Suggest reference documents for a scene by aggregating links from the scene's characters and places. Returns weighted candidates ranked by how many entities in the scene link to each reference. Excludes any explicit scene reference links already present. In apply mode, selected links commit to SQLite first, project backups refresh, and scene sidecar/frontmatter output is refreshed only as generated compatibility.",
|
|
1077
1080
|
{
|
|
1078
1081
|
scene_id: z.string().describe("Scene ID (e.g. 'sc-011-sebastian')."),
|
|
1079
1082
|
project_id: z.string().optional().describe("Optional project scope to disambiguate an ambiguous scene_id across projects."),
|
|
@@ -1235,40 +1238,13 @@ export function registerSearchTools(s, {
|
|
|
1235
1238
|
|
|
1236
1239
|
|
|
1237
1240
|
if (mode === "apply") {
|
|
1238
|
-
|
|
1239
|
-
return errorResponse("STALE_PATH", `Scene '${scene_id}' has no indexed file path. Run sync() to refresh.`, {
|
|
1240
|
-
scene_id,
|
|
1241
|
-
project_id: resolvedProjectId,
|
|
1242
|
-
});
|
|
1243
|
-
}
|
|
1244
|
-
|
|
1245
|
-
const toApply = selectApplyCandidates(enriched, selected_doc_ids, max_apply);
|
|
1246
|
-
|
|
1241
|
+
const toApply = selectApplyCandidates(enriched, selected_doc_ids, max_apply);
|
|
1247
1242
|
const appliedLinks = [];
|
|
1248
1243
|
const failedLinks = [];
|
|
1249
1244
|
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
scenePath: resolvedScene.file_path,
|
|
1254
|
-
syncDir: SYNC_DIR,
|
|
1255
|
-
targetDocId: candidate.doc_id,
|
|
1256
|
-
relation: candidate.relation,
|
|
1257
|
-
});
|
|
1258
|
-
} catch (err) {
|
|
1259
|
-
failedLinks.push({
|
|
1260
|
-
target_doc_id: candidate.doc_id,
|
|
1261
|
-
relation: candidate.relation,
|
|
1262
|
-
stage: "metadata",
|
|
1263
|
-
code: err?.code ?? "IO_ERROR",
|
|
1264
|
-
message: err?.code === "ENOENT"
|
|
1265
|
-
? `Scene file for '${scene_id}' not found at indexed path — run sync() to refresh.`
|
|
1266
|
-
: `Failed to persist scene reference link metadata: ${err.message}`,
|
|
1267
|
-
});
|
|
1268
|
-
continue;
|
|
1269
|
-
}
|
|
1270
|
-
|
|
1271
|
-
try {
|
|
1245
|
+
try {
|
|
1246
|
+
db.exec("BEGIN");
|
|
1247
|
+
for (const candidate of toApply) {
|
|
1272
1248
|
upsertExplicitReferenceLinkRow(db, {
|
|
1273
1249
|
sourceKind: "scene",
|
|
1274
1250
|
sourceProjectId: resolvedProjectId,
|
|
@@ -1276,7 +1252,15 @@ export function registerSearchTools(s, {
|
|
|
1276
1252
|
targetDocId: candidate.doc_id,
|
|
1277
1253
|
relation: candidate.relation,
|
|
1278
1254
|
});
|
|
1279
|
-
}
|
|
1255
|
+
}
|
|
1256
|
+
db.exec("COMMIT");
|
|
1257
|
+
} catch (err) {
|
|
1258
|
+
try {
|
|
1259
|
+
db.exec("ROLLBACK");
|
|
1260
|
+
} catch (rollbackErr) {
|
|
1261
|
+
void rollbackErr;
|
|
1262
|
+
}
|
|
1263
|
+
for (const candidate of toApply) {
|
|
1280
1264
|
failedLinks.push({
|
|
1281
1265
|
target_doc_id: candidate.doc_id,
|
|
1282
1266
|
relation: candidate.relation,
|
|
@@ -1284,17 +1268,20 @@ export function registerSearchTools(s, {
|
|
|
1284
1268
|
code: err?.code ?? "IO_ERROR",
|
|
1285
1269
|
message: `Failed to persist scene reference link index row: ${err.message}`,
|
|
1286
1270
|
});
|
|
1287
|
-
continue;
|
|
1288
1271
|
}
|
|
1272
|
+
}
|
|
1289
1273
|
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1274
|
+
if (failedLinks.length === 0) {
|
|
1275
|
+
for (const candidate of toApply) {
|
|
1276
|
+
appliedLinks.push({
|
|
1277
|
+
source_kind: "scene",
|
|
1278
|
+
source_project_id: resolvedProjectId,
|
|
1279
|
+
source_id: scene_id,
|
|
1280
|
+
target_doc_id: candidate.doc_id,
|
|
1281
|
+
relation: candidate.relation,
|
|
1282
|
+
origin: "explicit",
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1298
1285
|
}
|
|
1299
1286
|
|
|
1300
1287
|
const backupResult = appliedLinks.length
|
|
@@ -1323,6 +1310,47 @@ export function registerSearchTools(s, {
|
|
|
1323
1310
|
backup_warnings: [],
|
|
1324
1311
|
};
|
|
1325
1312
|
|
|
1313
|
+
const compatibilityDiagnostics = [];
|
|
1314
|
+
if (appliedLinks.length) {
|
|
1315
|
+
if (!resolvedScene.file_path) {
|
|
1316
|
+
compatibilityDiagnostics.push({
|
|
1317
|
+
code: "STALE_PATH",
|
|
1318
|
+
severity: "warning",
|
|
1319
|
+
message: `Canonical scene reference link was committed, but scene '${scene_id}' has no indexed file path for generated compatibility output.`,
|
|
1320
|
+
next_step: "Treat SQLite and project backup artifacts as current. Run sync, inspect the indexed scene path, then retry suggest_scene_references in apply mode if compatibility output is still needed.",
|
|
1321
|
+
details: {
|
|
1322
|
+
scene_id,
|
|
1323
|
+
project_id: resolvedProjectId,
|
|
1324
|
+
indexed_path: null,
|
|
1325
|
+
},
|
|
1326
|
+
});
|
|
1327
|
+
} else {
|
|
1328
|
+
for (const link of appliedLinks) {
|
|
1329
|
+
try {
|
|
1330
|
+
persistSceneReferenceLink({
|
|
1331
|
+
scenePath: resolvedScene.file_path,
|
|
1332
|
+
syncDir: SYNC_DIR,
|
|
1333
|
+
targetDocId: link.target_doc_id,
|
|
1334
|
+
relation: link.relation,
|
|
1335
|
+
});
|
|
1336
|
+
} catch (err) {
|
|
1337
|
+
compatibilityDiagnostics.push({
|
|
1338
|
+
code: err?.code ?? "COMPATIBILITY_OUTPUT_FAILED",
|
|
1339
|
+
severity: "warning",
|
|
1340
|
+
message: `Canonical scene reference link was committed, but generated compatibility metadata for scene '${scene_id}' could not be refreshed: ${err.message}`,
|
|
1341
|
+
next_step: "Treat SQLite and project backup artifacts as current. Run sync, inspect the indexed scene path, then retry suggest_scene_references in apply mode if compatibility output is still needed.",
|
|
1342
|
+
details: {
|
|
1343
|
+
scene_id,
|
|
1344
|
+
project_id: resolvedProjectId,
|
|
1345
|
+
target_doc_id: link.target_doc_id,
|
|
1346
|
+
indexed_path: resolvedScene.file_path,
|
|
1347
|
+
},
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1326
1354
|
return {
|
|
1327
1355
|
content: [{
|
|
1328
1356
|
type: "text",
|
|
@@ -1336,6 +1364,18 @@ export function registerSearchTools(s, {
|
|
|
1336
1364
|
applied_links: appliedLinks,
|
|
1337
1365
|
failed_count: failedLinks.length,
|
|
1338
1366
|
failed_links: failedLinks,
|
|
1367
|
+
mutation_order: [
|
|
1368
|
+
"validated_request",
|
|
1369
|
+
"sqlite_commit",
|
|
1370
|
+
"project_backup_refresh",
|
|
1371
|
+
"compatibility_output_refresh",
|
|
1372
|
+
],
|
|
1373
|
+
compatibility_output: {
|
|
1374
|
+
generated_transparency: true,
|
|
1375
|
+
mutation_surface: false,
|
|
1376
|
+
refreshed: compatibilityDiagnostics.length === 0,
|
|
1377
|
+
},
|
|
1378
|
+
compatibility_diagnostics: compatibilityDiagnostics,
|
|
1339
1379
|
operation_history: backupResult.operation_history,
|
|
1340
1380
|
backup_refresh: backupResult.backup_refresh,
|
|
1341
1381
|
backup_warnings: backupResult.backup_warnings,
|
package/src/tools/sync.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 { syncAll, writeMeta,
|
|
5
|
+
import { syncAll, writeMeta, readSourceMeta, indexSceneFile, normalizeSceneMetaForPath, isManagedStructureProject } from "../sync/sync.js";
|
|
6
6
|
import { importScrivenerSync, validateProjectId } from "../sync/importer.js";
|
|
7
7
|
import { runStructureDiagnostics } from "../structure/structure-diagnostics.js";
|
|
8
8
|
import {
|
|
@@ -676,7 +676,7 @@ export function registerSyncTools(s, {
|
|
|
676
676
|
|
|
677
677
|
s.tool(
|
|
678
678
|
"enrich_scene_characters_batch",
|
|
679
|
-
"Start an asynchronous
|
|
679
|
+
"Start an asynchronous prose-derived relationship repair job that infers scene character mentions and updates scene character links. Defaults to dry_run=true; apply mode retains scene sidecar characters as compatibility output, then syncs SQLite scene_characters and refreshes project backups for changed scenes.",
|
|
680
680
|
{
|
|
681
681
|
project_id: z.string().describe("Project ID (e.g. 'the-lamb' or 'universe-1/book-1-the-lamb')."),
|
|
682
682
|
scene_ids: z.array(z.string()).optional().describe("Optional allowlist of scene IDs to process before other filters are applied."),
|
|
@@ -684,7 +684,7 @@ export function registerSyncTools(s, {
|
|
|
684
684
|
chapter: z.number().int().optional().describe("Optional read-scope compatibility alias resolved through canonical chapter identity. Not a structural mutation target."),
|
|
685
685
|
chapter_id: z.string().optional().describe("Optional canonical chapter identifier."),
|
|
686
686
|
only_stale: z.boolean().optional().describe("If true, only process scenes currently marked metadata_stale."),
|
|
687
|
-
dry_run: z.boolean().optional().describe("If true (default), returns preview results without writing
|
|
687
|
+
dry_run: z.boolean().optional().describe("If true (default), returns preview results without writing compatibility metadata."),
|
|
688
688
|
replace_mode: z.enum(["merge", "replace"]).optional().describe("merge (default): add inferred IDs; replace: overwrite characters with inferred IDs."),
|
|
689
689
|
max_scenes: z.number().int().positive().optional().describe("Hard guardrail for resolved scene count (default: 200)."),
|
|
690
690
|
include_match_details: z.boolean().optional().describe("If true, include extra match diagnostics per scene."),
|
|
@@ -805,6 +805,18 @@ export function registerSyncTools(s, {
|
|
|
805
805
|
});
|
|
806
806
|
completedJob.result = {
|
|
807
807
|
...completedJob.result,
|
|
808
|
+
mutation_order: [
|
|
809
|
+
"prose_inference",
|
|
810
|
+
"compatibility_output_refresh",
|
|
811
|
+
"sqlite_sync_index",
|
|
812
|
+
"project_backup_refresh",
|
|
813
|
+
],
|
|
814
|
+
compatibility_output: {
|
|
815
|
+
role: "prose_derived_repair_output",
|
|
816
|
+
generated_transparency: true,
|
|
817
|
+
mutation_surface: false,
|
|
818
|
+
refreshed: true,
|
|
819
|
+
},
|
|
808
820
|
...backupMutationFields(backupResult),
|
|
809
821
|
};
|
|
810
822
|
}
|
|
@@ -951,20 +963,21 @@ export function registerSyncTools(s, {
|
|
|
951
963
|
try {
|
|
952
964
|
const raw = fs.readFileSync(scene.file_path, "utf8");
|
|
953
965
|
const { content: prose } = matter(raw);
|
|
954
|
-
const { meta } =
|
|
966
|
+
const { sourceMeta: meta } = readSourceMeta(scene.file_path, SYNC_DIR, { writable: true });
|
|
955
967
|
|
|
956
968
|
const inferredLogline = deriveLoglineFromProse(prose);
|
|
957
969
|
const inferredCharacters = inferCharacterIdsFromProse(db, prose, scene.project_id);
|
|
958
970
|
|
|
959
|
-
const
|
|
971
|
+
const sourceUpdatedMeta = {
|
|
960
972
|
...meta,
|
|
961
973
|
...(inferredLogline ? { logline: inferredLogline } : {}),
|
|
962
974
|
...((inferredCharacters.length > 0 || (meta.characters?.length ?? 0) > 0)
|
|
963
975
|
? { characters: inferredCharacters.length > 0 ? inferredCharacters : meta.characters }
|
|
964
976
|
: {}),
|
|
965
|
-
}
|
|
977
|
+
};
|
|
978
|
+
const updatedMeta = normalizeSceneMetaForPath(SYNC_DIR, scene.file_path, sourceUpdatedMeta).meta;
|
|
966
979
|
|
|
967
|
-
writeMeta(scene.file_path,
|
|
980
|
+
writeMeta(scene.file_path, sourceUpdatedMeta, { syncDir: SYNC_DIR });
|
|
968
981
|
indexSceneFile(db, SYNC_DIR, scene.file_path, updatedMeta, prose, {
|
|
969
982
|
managedStructure: isManagedStructureProject(db, scene.project_id),
|
|
970
983
|
});
|
|
@@ -63,6 +63,23 @@ export const WORKFLOW_CATALOGUE = [
|
|
|
63
63
|
{ tool: "get_arc", note: "Use when the thread question is really a character-progression question and character context is the better structural entry point." },
|
|
64
64
|
],
|
|
65
65
|
},
|
|
66
|
+
{
|
|
67
|
+
id: "relationship_alignment",
|
|
68
|
+
label: "Track and repair story relationships",
|
|
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
|
+
steps: [
|
|
71
|
+
{ tool: "find_scenes", note: "Identify scene_id and project_id from story context before recording relationships." },
|
|
72
|
+
{ tool: "track_thread_arc", note: "Use when a scene should carry a storyline, subplot, setup, escalation, reveal, reversal, payoff, or other thread beat." },
|
|
73
|
+
{ tool: "connect_character_place_evidence", note: "Use when a scene proves a character/place association; SQLite scene relationship indexes commit first and sidecar characters/places remain generated compatibility output." },
|
|
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
|
+
{ tool: "link_reference_evidence", note: "Use when scene, character, place, or reference evidence should point to a reference document; SQLite commits first and compatibility output is generated transparency." },
|
|
76
|
+
{ tool: "audit_relationship_metadata", note: "Use before repair work to review stale relationship indexes and sidecar-only compatibility notes without mutating canonical state." },
|
|
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
|
+
{ 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
|
+
{ 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." },
|
|
80
|
+
{ tool: "export_project_backup", note: "Prepare a deterministic recovery snapshot from SQLite canonical state; editing generated backup files does not mutate relationships." },
|
|
81
|
+
],
|
|
82
|
+
},
|
|
66
83
|
{
|
|
67
84
|
id: "parity_recovery",
|
|
68
85
|
label: "Recover metadata parity",
|
|
@@ -70,9 +87,11 @@ export const WORKFLOW_CATALOGUE = [
|
|
|
70
87
|
steps: [
|
|
71
88
|
{ tool: "sync", note: "Refresh the index and use the result as the main signal that material has changed or parity may need attention." },
|
|
72
89
|
{ tool: "diagnose_structure", note: "Use when sync warning summaries suggest chapter, epigraph, folder-derived, or generated-export drift that should be understood before repair." },
|
|
90
|
+
{ tool: "audit_relationship_metadata", note: "Use when parity questions involve stale relationship indexes, character/place sidecar notes, sidecar threads, or relationship recovery readiness." },
|
|
73
91
|
{ tool: "enrich_scene", note: "Use for lightweight opportunistic recovery when the current task is already touching a specific low-parity scene." },
|
|
74
92
|
{ tool: "enrich_scene_characters_batch", note: "Use when recovery scope is broad enough to justify focused catch-up work; prefer dry_run first." },
|
|
75
|
-
{ tool: "suggest_scene_references", note: "Use when low parity is specifically about missing scene-to-reference relationships." },
|
|
93
|
+
{ tool: "suggest_scene_references", note: "Use when low parity is specifically about missing scene-to-reference relationships; apply mode commits SQLite first and refreshes compatibility output as generated transparency." },
|
|
94
|
+
{ tool: "link_reference_evidence", note: "Use when the repair is a known evidence relationship rather than a suggestion-review workflow." },
|
|
76
95
|
],
|
|
77
96
|
},
|
|
78
97
|
{
|