@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 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.22.0](https://github.com/hannasdev/mcp-writing/compare/v3.21.3...v3.22.0)
8
+
9
+ - feat(metadata): add outcome relationship workflows [`#225`](https://github.com/hannasdev/mcp-writing/pull/225)
10
+
11
+ #### [v3.21.3](https://github.com/hannasdev/mcp-writing/compare/v3.21.2...v3.21.3)
12
+
13
+ > 28 May 2026
14
+
15
+ - fix(metadata): preserve sidecar structure fields on generic writes [`#224`](https://github.com/hannasdev/mcp-writing/pull/224)
16
+ - Release 3.21.3 [`91bc41f`](https://github.com/hannasdev/mcp-writing/commit/91bc41f98e6acf0f46cf93f329e1ecebad2e2305)
17
+
7
18
  #### [v3.21.2](https://github.com/hannasdev/mcp-writing/compare/v3.21.1...v3.21.2)
8
19
 
20
+ > 28 May 2026
21
+
9
22
  - docs: accept architecture snapshot milestone [`#223`](https://github.com/hannasdev/mcp-writing/pull/223)
23
+ - Release 3.21.2 [`7f0e627`](https://github.com/hannasdev/mcp-writing/commit/7f0e62741059ad487577e7295ea75564fd64d33a)
10
24
 
11
25
  #### [v3.21.1](https://github.com/hannasdev/mcp-writing/compare/v3.21.0...v3.21.1)
12
26
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.21.2",
3
+ "version": "3.22.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",
@@ -2,7 +2,7 @@
2
2
  import path from "node:path";
3
3
  import { openDb } from "../core/db.js";
4
4
  import { buildCharacterNormalizationContext, normalizeSceneCharacters } from "../sync/scene-character-normalization.js";
5
- import { normalizeSceneMetaForPath, readMeta, syncAll, writeMeta } from "../sync/sync.js";
5
+ import { readMeta, readSourceMeta, syncAll, writeMeta } from "../sync/sync.js";
6
6
 
7
7
  function readRequiredValue(argv, index, option) {
8
8
  const value = argv[index + 1];
@@ -138,11 +138,11 @@ function runNormalization({ syncDir, projectId, write, limit }) {
138
138
  if (!normalized.changed) continue;
139
139
 
140
140
  if (write) {
141
- const updatedMeta = normalizeSceneMetaForPath(syncDir, scene.file_path, {
142
- ...meta,
141
+ const { sourceMeta } = readSourceMeta(scene.file_path, syncDir, { writable: true });
142
+ writeMeta(scene.file_path, {
143
+ ...sourceMeta,
143
144
  characters: normalized.after,
144
- }).meta;
145
- writeMeta(scene.file_path, updatedMeta, { syncDir });
145
+ }, { syncDir });
146
146
  }
147
147
 
148
148
  changed.push({
@@ -1,7 +1,7 @@
1
1
  import fs from "node:fs";
2
2
  import matter from "gray-matter";
3
3
  import { buildCharacterNormalizationContext, escapeRegex, resolveCharacterReference } from "./scene-character-normalization.js";
4
- import { normalizeSceneMetaForPath, readMeta, writeMeta } from "./sync.js";
4
+ import { readSourceMeta, writeMeta } from "./sync.js";
5
5
 
6
6
  function normalizeCharacterRows(rows) {
7
7
  return buildCharacterNormalizationContext(rows);
@@ -138,7 +138,7 @@ export async function runSceneCharacterBatch({ syncDir, args, onProgress, should
138
138
  try {
139
139
  const raw = fs.readFileSync(scene.file_path, "utf8");
140
140
  const { content: prose } = matter(raw);
141
- const { meta } = readMeta(scene.file_path, syncDir, { writable: !dry_run });
141
+ const { sourceMeta: meta } = readSourceMeta(scene.file_path, syncDir, { writable: !dry_run });
142
142
 
143
143
  const before_characters = [...new Set((meta.characters ?? []).map(String).filter(Boolean))];
144
144
  const normalized_before_characters = [...new Set(
@@ -169,10 +169,10 @@ export async function runSceneCharacterBatch({ syncDir, args, onProgress, should
169
169
  const changed = added.length > 0 || removed.length > 0;
170
170
 
171
171
  if (!dry_run && changed) {
172
- const updatedMeta = normalizeSceneMetaForPath(syncDir, scene.file_path, {
172
+ const updatedMeta = {
173
173
  ...meta,
174
174
  characters: after_characters,
175
- }).meta;
175
+ };
176
176
 
177
177
  writeMeta(scene.file_path, updatedMeta, { syncDir });
178
178
  }
@@ -234,6 +234,15 @@ export async function runSceneCharacterBatch({ syncDir, args, onProgress, should
234
234
  cancelled: Boolean(typeof shouldCancel === "function" && shouldCancel() && processed_scenes < targetScenes.length),
235
235
  project_id,
236
236
  dry_run: Boolean(dry_run),
237
+ relationship_authority: {
238
+ canonical_owner: "SQLite scene_characters",
239
+ compatibility_source: "scene sidecar characters",
240
+ compatibility_mutation_surface: false,
241
+ apply_order: dry_run ? "preview_only" : "compatibility_output_then_sync_index",
242
+ note: dry_run
243
+ ? "Dry run only reviews prose-derived character repairs."
244
+ : "M4 retains this batch repair as a prose-derived compatibility path; completion syncs the SQLite relationship index and refreshes backups for changed scenes.",
245
+ },
237
246
  total_scenes: targetScenes.length,
238
247
  processed_scenes,
239
248
  scenes_changed,
package/src/sync/sync.js CHANGED
@@ -494,11 +494,12 @@ export function parseFile(filePath) {
494
494
  }
495
495
 
496
496
  /**
497
- * Read metadata for a scene file. Priority: sidecar > frontmatter.
498
- * If sidecar doesn't exist but frontmatter does, auto-generates the sidecar.
499
- * Returns { meta, sidecarGenerated }.
497
+ * Read source metadata for a scene file. Priority: sidecar > frontmatter.
498
+ * Does not apply path-derived structural normalization or auto-generate sidecars.
499
+ * Generic metadata writers use this so non-structural updates do not mirror
500
+ * path-derived structure fields into sidecars.
500
501
  */
501
- export function readMeta(filePath, syncDir, { writable = false } = {}) {
502
+ export function readSourceMeta(filePath, syncDir, { writable = false } = {}) {
502
503
  const sidecar = sidecarPath(filePath);
503
504
  const syncDirAbs = path.resolve(syncDir);
504
505
 
@@ -517,26 +518,45 @@ export function readMeta(filePath, syncDir, { writable = false } = {}) {
517
518
  }
518
519
  const raw = fs.readFileSync(sidecar, "utf8");
519
520
  const parsed = parseYaml(raw) ?? {};
520
- return { ...normalizeSceneMetaForPath(syncDir, filePath, parsed), sourceMeta: parsed, sidecarGenerated: false };
521
+ return { meta: parsed, sourceMeta: parsed, source: "sidecar", sidecarGenerated: false };
521
522
  }
522
523
 
523
524
  // Fall back to frontmatter
524
525
  const { data: frontmatter } = parseFile(filePath);
525
526
  if (!Object.keys(frontmatter).length) {
526
- return { ...normalizeSceneMetaForPath(syncDir, filePath, {}), sourceMeta: {}, sidecarGenerated: false };
527
+ return { meta: {}, sourceMeta: {}, source: "empty", sidecarGenerated: false };
528
+ }
529
+
530
+ return { meta: frontmatter, sourceMeta: frontmatter, source: "frontmatter", sidecarGenerated: false };
531
+ }
532
+
533
+ /**
534
+ * Read metadata for a scene file. Priority: sidecar > frontmatter.
535
+ * If sidecar doesn't exist but frontmatter does, auto-generates the sidecar.
536
+ * Returns { meta, sidecarGenerated }.
537
+ */
538
+ export function readMeta(filePath, syncDir, { writable = false } = {}) {
539
+ const sourceRead = readSourceMeta(filePath, syncDir, { writable });
540
+
541
+ if (sourceRead.source === "sidecar" || sourceRead.source === "empty") {
542
+ return {
543
+ ...normalizeSceneMetaForPath(syncDir, filePath, sourceRead.sourceMeta),
544
+ sourceMeta: sourceRead.sourceMeta,
545
+ sidecarGenerated: false,
546
+ };
527
547
  }
528
548
 
529
- const normalized = normalizeSceneMetaForPath(syncDir, filePath, frontmatter);
549
+ const normalized = normalizeSceneMetaForPath(syncDir, filePath, sourceRead.sourceMeta);
530
550
 
531
551
  // Auto-migrate: write sidecar from frontmatter (only if writable)
532
552
  if (writable) {
533
553
  try {
534
554
  writeMeta(filePath, normalized.meta, { syncDir });
535
- return { ...normalized, sourceMeta: frontmatter, sidecarGenerated: true };
555
+ return { ...normalized, sourceMeta: sourceRead.sourceMeta, sidecarGenerated: true };
536
556
  } catch { /* empty */ }
537
557
  }
538
558
 
539
- return { ...normalized, sourceMeta: frontmatter, sidecarGenerated: false };
559
+ return { ...normalized, sourceMeta: sourceRead.sourceMeta, sidecarGenerated: false };
540
560
  }
541
561
 
542
562
  /**