@hanna84/mcp-writing 3.9.3 → 3.9.4

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.9.4](https://github.com/hannasdev/mcp-writing/compare/v3.9.3...v3.9.4)
8
+
9
+ - refactor(structure): centralize scene structure patches [`#200`](https://github.com/hannasdev/mcp-writing/pull/200)
10
+
7
11
  #### [v3.9.3](https://github.com/hannasdev/mcp-writing/compare/v3.9.2...v3.9.3)
8
12
 
13
+ > 17 May 2026
14
+
9
15
  - refactor(structure): extract target architecture M1 boundaries [`#199`](https://github.com/hannasdev/mcp-writing/pull/199)
16
+ - Release 3.9.3 [`55c87ea`](https://github.com/hannasdev/mcp-writing/commit/55c87ea405841153765144d32c57803b6139a103)
10
17
 
11
18
  #### [v3.9.2](https://github.com/hannasdev/mcp-writing/compare/v3.9.1...v3.9.2)
12
19
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "3.9.3",
3
+ "version": "3.9.4",
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",
@@ -109,24 +109,35 @@ export function inferChapterStructureFromPath(syncDir, filePath, meta = {}) {
109
109
  };
110
110
  }
111
111
 
112
- export function normalizeSceneMetaForPath(syncDir, filePath, meta = {}) {
112
+ export function buildSceneStructurePatch(syncDir, filePath, meta = {}, { chapter } = {}) {
113
113
  const derived = inferScenePositionFromPath(syncDir, filePath);
114
114
  const chapterStructure = inferChapterStructureFromPath(syncDir, filePath, meta);
115
- const normalized = { ...meta };
115
+ const patch = {};
116
116
 
117
- if (derived.part !== null) normalized.part = derived.part;
118
- if (derived.chapter !== null) normalized.chapter = derived.chapter;
117
+ if (derived.part !== null) patch.part = derived.part;
118
+ if (derived.chapter !== null) patch.chapter = derived.chapter;
119
119
  if (chapterStructure.chapter?.chapter_id) {
120
- normalized.chapter_id = chapterStructure.chapter.chapter_id;
121
- normalized.chapter = chapterStructure.chapter.sort_index;
122
- normalized.chapter_title = chapterStructure.chapter.title;
120
+ patch.chapter_id = chapterStructure.chapter.chapter_id;
121
+ patch.chapter = chapterStructure.chapter.sort_index;
122
+ patch.chapter_title = chapterStructure.chapter.title;
123
123
  }
124
124
  if (chapterStructure.role) {
125
- normalized.scene_role = chapterStructure.role;
125
+ patch.scene_role = chapterStructure.role;
126
+ }
127
+ if (chapter !== undefined) {
128
+ if (chapter === null) {
129
+ patch.chapter_id = null;
130
+ patch.chapter = null;
131
+ patch.chapter_title = null;
132
+ } else {
133
+ patch.chapter_id = chapter.chapter_id;
134
+ patch.chapter = chapter.sort_index;
135
+ patch.chapter_title = chapter.title ?? null;
136
+ }
126
137
  }
127
138
 
128
139
  return {
129
- meta: normalized,
140
+ patch,
130
141
  derived,
131
142
  chapterStructure,
132
143
  mismatches: {
@@ -135,3 +146,15 @@ export function normalizeSceneMetaForPath(syncDir, filePath, meta = {}) {
135
146
  },
136
147
  };
137
148
  }
149
+
150
+ export function applySceneStructurePatch(syncDir, filePath, meta = {}, options = {}) {
151
+ const plan = buildSceneStructurePatch(syncDir, filePath, meta, options);
152
+ return {
153
+ ...plan,
154
+ meta: { ...meta, ...plan.patch },
155
+ };
156
+ }
157
+
158
+ export function normalizeSceneMetaForPath(syncDir, filePath, meta = {}) {
159
+ return applySceneStructurePatch(syncDir, filePath, meta);
160
+ }
package/src/sync/sync.js CHANGED
@@ -3,6 +3,8 @@ import path from "node:path";
3
3
  import matter from "gray-matter";
4
4
  import yaml from "js-yaml";
5
5
  import {
6
+ applySceneStructurePatch,
7
+ buildSceneStructurePatch,
6
8
  inferChapterStructureFromPath,
7
9
  inferScenePositionFromPath,
8
10
  normalizeSceneMetaForPath,
@@ -16,6 +18,8 @@ import { indexCanonicalEpigraph } from "../structure/epigraph-indexing.js";
16
18
  const { load: parseYaml, dump: stringifyYaml } = yaml;
17
19
 
18
20
  export {
21
+ applySceneStructurePatch,
22
+ buildSceneStructurePatch,
19
23
  inferChapterStructureFromPath,
20
24
  inferScenePositionFromPath,
21
25
  normalizeSceneMetaForPath,
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import fs from "node:fs";
3
3
  import matter from "gray-matter";
4
- import { readMeta, writeMeta, indexSceneFile, normalizeSceneMetaForPath } from "../sync/sync.js";
4
+ import { readMeta, writeMeta, indexSceneFile, applySceneStructurePatch } from "../sync/sync.js";
5
5
  import { validateProjectId, validateUniverseId } from "../sync/importer.js";
6
6
  import { resolveValidatedChapterFilter } from "../core/chapter-resolution.js";
7
7
  import {
@@ -545,6 +545,7 @@ export function registerMetadataTools(s, {
545
545
  try {
546
546
  const { meta } = readMeta(scene.file_path, SYNC_DIR, { writable: true });
547
547
  const nextFields = { ...fields };
548
+ let chapter = undefined;
548
549
 
549
550
  if (fields.chapter_id === null && fields.chapter !== undefined) {
550
551
  return errorResponse(
@@ -559,8 +560,20 @@ export function registerMetadataTools(s, {
559
560
  }
560
561
 
561
562
  if (fields.chapter_id === null) {
562
- nextFields.chapter = null;
563
- nextFields.chapter_title = null;
563
+ const structurePlan = applySceneStructurePatch(SYNC_DIR, scene.file_path, meta);
564
+ if (structurePlan.derived.chapter !== null || structurePlan.chapterStructure.chapter?.chapter_id) {
565
+ return errorResponse(
566
+ "VALIDATION_ERROR",
567
+ "chapter_id cannot be cleared for a scene whose file path implies a chapter.",
568
+ {
569
+ project_id,
570
+ scene_id,
571
+ chapter_id: null,
572
+ path_chapter: structurePlan.chapterStructure.chapter?.chapter_id ?? structurePlan.derived.chapter,
573
+ }
574
+ );
575
+ }
576
+ chapter = null;
564
577
  } else if (fields.chapter_id !== undefined || fields.chapter !== undefined) {
565
578
  const resolvedChapterFilter = resolveValidatedChapterFilter(db, {
566
579
  projectId: project_id,
@@ -594,12 +607,10 @@ export function registerMetadataTools(s, {
594
607
  );
595
608
  }
596
609
 
597
- nextFields.chapter_id = resolvedChapter.chapter_id;
598
- nextFields.chapter = resolvedChapter.sort_index;
599
- nextFields.chapter_title = resolvedChapter.title ?? null;
610
+ chapter = resolvedChapter;
600
611
  }
601
612
 
602
- const updated = normalizeSceneMetaForPath(SYNC_DIR, scene.file_path, { ...meta, ...nextFields }).meta;
613
+ const updated = applySceneStructurePatch(SYNC_DIR, scene.file_path, { ...meta, ...nextFields }, { chapter }).meta;
603
614
  writeMeta(scene.file_path, updated);
604
615
 
605
616
  const { content: prose } = matter(fs.readFileSync(scene.file_path, "utf8"));