@hanna84/mcp-writing 2.12.7 → 2.12.8

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,11 +4,21 @@ 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
+ #### [v2.12.8](https://github.com/hannasdev/mcp-writing.git
8
+ /compare/v2.12.7...v2.12.8)
9
+
10
+ - refactor(domains): move review, styleguide, and workflow modules into src [`#123`](https://github.com/hannasdev/mcp-writing.git
11
+ /pull/123)
12
+
7
13
  #### [v2.12.7](https://github.com/hannasdev/mcp-writing.git
8
14
  /compare/v2.12.6...v2.12.7)
9
15
 
16
+ > 28 April 2026
17
+
10
18
  - refactor(core): move db and git modules under src/core [`#122`](https://github.com/hannasdev/mcp-writing.git
11
19
  /pull/122)
20
+ - Release 2.12.7 [`4644e20`](https://github.com/hannasdev/mcp-writing.git
21
+ /commit/4644e20a0b63f508e7a9578c600596dd546b4653)
12
22
 
13
23
  #### [v2.12.6](https://github.com/hannasdev/mcp-writing.git
14
24
  /compare/v2.12.5...v2.12.6)
package/helpers.js CHANGED
@@ -9,7 +9,7 @@ import {
9
9
  renderPlaceSheetTemplate,
10
10
  renderCharacterArcTemplate,
11
11
  } from "./src/world/world-entity-templates.js";
12
- import { ReviewBundlePlanError } from "./review-bundles.js";
12
+ import { ReviewBundlePlanError } from "./src/review-bundles/review-bundles.js";
13
13
 
14
14
  export function deriveLoglineFromProse(prose) {
15
15
  const compact = prose.replace(/\s+/g, " ").trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hanna84/mcp-writing",
3
- "version": "2.12.7",
3
+ "version": "2.12.8",
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",
@@ -1,125 +1 @@
1
- function detectQuotationStyle(prose) {
2
- const counts = {
3
- double: (prose.match(/"[^"]{2,}"/g) ?? []).length,
4
- guillemets: (prose.match(/«[^»]{2,}»/g) ?? []).length,
5
- low9: (prose.match(/„[^“]{2,}“/g) ?? []).length,
6
- corner_brackets: (prose.match(/「[^」]{2,}」/g) ?? []).length,
7
- dialogue_dash_en: (prose.match(/^\s*–\s/mg) ?? []).length,
8
- dialogue_dash_em: (prose.match(/^\s*—\s/mg) ?? []).length,
9
- single: (prose.match(/'[^'\n]{2,}'/g) ?? []).length,
10
- };
11
-
12
- let best = null;
13
- let bestCount = 0;
14
- for (const [style, count] of Object.entries(counts)) {
15
- if (count > bestCount) {
16
- best = style;
17
- bestCount = count;
18
- }
19
- }
20
- return bestCount > 0 ? best : null;
21
- }
22
-
23
- function detectEmDashSpacing(prose) {
24
- const spaced = (prose.match(/\s—\s/g) ?? []).length;
25
- const closed = (prose.match(/\S—\S/g) ?? []).length;
26
- if (spaced === 0 && closed === 0) return null;
27
- return spaced >= closed ? "spaced" : "closed";
28
- }
29
-
30
- function detectSpellingVariant(prose) {
31
- const lower = prose.toLowerCase();
32
- const ukSignals = ["colour", "realise", "centre", "honour", "travelling"];
33
- const usSignals = ["color", "realize", "center", "honor", "traveling"];
34
-
35
- const countHits = (signals) => signals.reduce((sum, word) => {
36
- const re = new RegExp(`\\b${word}\\b`, "g");
37
- return sum + (lower.match(re) ?? []).length;
38
- }, 0);
39
-
40
- const uk = countHits(ukSignals);
41
- const us = countHits(usSignals);
42
- if (uk === 0 && us === 0) return null;
43
- return uk >= us ? "uk" : "us";
44
- }
45
-
46
- function detectTenseHint(prose) {
47
- const lower = prose.toLowerCase();
48
- const past = (lower.match(/\b(was|were|had|did)\b/g) ?? []).length;
49
- const present = (lower.match(/\b(is|are|has|do)\b/g) ?? []).length;
50
- if (past === 0 && present === 0) return null;
51
- return present >= past ? "present" : "past";
52
- }
53
-
54
- function mostCommonValue(values) {
55
- const counts = new Map();
56
- for (const value of values) {
57
- if (!value) continue;
58
- counts.set(value, (counts.get(value) ?? 0) + 1);
59
- }
60
- if (counts.size === 0) return null;
61
-
62
- let bestValue = null;
63
- let bestCount = 0;
64
- for (const [value, count] of counts.entries()) {
65
- if (count > bestCount) {
66
- bestValue = value;
67
- bestCount = count;
68
- }
69
- }
70
- return { value: bestValue, count: bestCount, total: values.filter(Boolean).length };
71
- }
72
-
73
- export function detectStyleguideSignals(prose) {
74
- return {
75
- quotation_style: detectQuotationStyle(prose),
76
- em_dash_spacing: detectEmDashSpacing(prose),
77
- spelling: detectSpellingVariant(prose),
78
- tense: detectTenseHint(prose),
79
- };
80
- }
81
-
82
- export function analyzeSceneStyleguideDrift({ prose, resolvedConfig }) {
83
- const observed = detectStyleguideSignals(prose);
84
- const drift = [];
85
-
86
- for (const field of ["quotation_style", "em_dash_spacing", "spelling", "tense"]) {
87
- const declared = resolvedConfig?.[field];
88
- const seen = observed[field];
89
- if (!declared || !seen) continue;
90
- if (declared !== seen) {
91
- drift.push({ field, declared, observed: seen });
92
- }
93
- }
94
-
95
- return { observed, drift };
96
- }
97
-
98
- export function suggestStyleguideUpdatesFromScenes({
99
- sceneAnalyses,
100
- resolvedConfig,
101
- minAgreement = 0.6,
102
- minEvidence = 3,
103
- }) {
104
- const suggestions = {};
105
-
106
- for (const field of ["quotation_style", "em_dash_spacing", "spelling", "tense"]) {
107
- const values = sceneAnalyses.map((scene) => scene.observed?.[field] ?? null);
108
- const common = mostCommonValue(values);
109
- if (!common) continue;
110
- if (common.total < minEvidence) continue;
111
-
112
- const agreement = common.total > 0 ? common.count / common.total : 0;
113
- const fieldThreshold = field === "tense" ? Math.max(minAgreement, 0.75) : minAgreement;
114
- if (agreement < fieldThreshold) continue;
115
- if (resolvedConfig?.[field] === common.value) continue;
116
-
117
- suggestions[field] = {
118
- suggested_value: common.value,
119
- agreement,
120
- based_on_scenes: common.total,
121
- };
122
- }
123
-
124
- return suggestions;
125
- }
1
+ export * from "./src/styleguide/prose-styleguide-drift.js";
@@ -1,125 +1 @@
1
- export const PROSE_STYLEGUIDE_SKILL_DIRNAME = "skills";
2
- export const PROSE_STYLEGUIDE_SKILL_BASENAME = "prose-styleguide.md";
3
-
4
- const LANGUAGE_LABELS = {
5
- english_us: "English (US)",
6
- english_uk: "English (UK)",
7
- english_au: "English (AU)",
8
- english_ca: "English (CA)",
9
- swedish: "Swedish",
10
- norwegian: "Norwegian",
11
- danish: "Danish",
12
- finnish: "Finnish",
13
- french: "French",
14
- italian: "Italian",
15
- russian: "Russian",
16
- portuguese_pt: "Portuguese (PT)",
17
- portuguese_br: "Portuguese (BR)",
18
- german: "German",
19
- dutch: "Dutch",
20
- polish: "Polish",
21
- czech: "Czech",
22
- hungarian: "Hungarian",
23
- spanish: "Spanish",
24
- irish: "Irish",
25
- japanese: "Japanese",
26
- korean: "Korean",
27
- chinese_traditional: "Chinese (Traditional)",
28
- chinese_simplified: "Chinese (Simplified)",
29
- };
30
-
31
- const CONFIG_RULE_RENDERERS = {
32
- language: (value) => `Primary writing language: ${LANGUAGE_LABELS[value] ?? value}.`,
33
- spelling: (value) => `Spelling variant: ${value.toUpperCase()}.`,
34
- quotation_style: (value) => {
35
- const labels = {
36
- double: "double quotes",
37
- single: "single quotes",
38
- guillemets: "guillemets (« »)",
39
- low9: "low-9 quotation marks",
40
- dialogue_dash_en: "Scandinavian en-dash dialogue",
41
- dialogue_dash_em: "Spanish/Irish em-dash dialogue",
42
- corner_brackets: "corner brackets (「 」)",
43
- };
44
- return `Dialogue quotation style: ${labels[value] ?? value}.`;
45
- },
46
- quotation_style_nested: (value) => `Nested quotation style: ${value}.`,
47
- em_dash_spacing: (value) => `Em dash spacing: ${value}.`,
48
- ellipsis_style: (value) => `Ellipsis style: ${value}.`,
49
- abbreviation_periods: (value) => `Abbreviation periods: ${value}.`,
50
- oxford_comma: (value) => `Oxford comma: ${value}.`,
51
- numbers: (value) => `Number formatting rule: ${value}.`,
52
- date_format: (value) => `Date format: ${value}.`,
53
- time_format: (value) => `Time format: ${value}.`,
54
- tense: (value) => `Default narrative tense: ${value}. Flag deviations as questions, not hard errors.`,
55
- pov: (value) => `Default POV: ${value}. Flag shifts as intentional-or-drift questions.`,
56
- dialogue_tags: (value) => `Dialogue tag policy: ${value}.`,
57
- sentence_fragments: (value) => `Sentence fragments policy: ${value}.`,
58
- };
59
-
60
- export function buildProseStyleguideSkill({ resolvedConfig, sources = [], projectId = null }) {
61
- if (!resolvedConfig || typeof resolvedConfig !== "object") {
62
- return {
63
- ok: false,
64
- error: {
65
- code: "INVALID_STYLEGUIDE_CONFIG",
66
- message: "Cannot generate prose-styleguide.md without a resolved config object.",
67
- },
68
- };
69
- }
70
-
71
- const injectedRules = [];
72
- for (const [field, renderRule] of Object.entries(CONFIG_RULE_RENDERERS)) {
73
- if (resolvedConfig[field] === undefined) continue;
74
- injectedRules.push(renderRule(resolvedConfig[field]));
75
- }
76
-
77
- const sourceLines = sources.length
78
- ? sources.map((source) => `- ${source.scope}: ${source.file_path}`)
79
- : ["- none"];
80
-
81
- const voiceNotes = typeof resolvedConfig.voice_notes === "string" && resolvedConfig.voice_notes.trim()
82
- ? resolvedConfig.voice_notes.trim().split("\n").map((line) => `> ${line}`).join("\n")
83
- : "> None provided.";
84
-
85
- const markdown = [
86
- "# Prose Styleguide",
87
- "",
88
- "## Standing Order",
89
- "Apply this styleguide by default for prose critique and edits. Preserve author voice over mechanical cleanup.",
90
- "",
91
- "## Resolved Scope",
92
- `- Project scope: ${projectId ?? "sync-root default"}`,
93
- ...sourceLines,
94
- "",
95
- "## Mechanical Conventions",
96
- "These are injected from prose-styleguide.config.yaml and should be applied consistently:",
97
- ...injectedRules.map((rule) => `- ${rule}`),
98
- "",
99
- "## Universal Craft Rules",
100
- "- Identify scene purpose before proposing changes.",
101
- "- Require transformation (emotional, relational, narrative, or thematic).",
102
- "- Prefer critique before rewrite.",
103
- "- Preserve cadence and specificity; avoid flattening voice.",
104
- "- Ask before normalizing intentional instability (flashbacks, POV drift, syntax breaks).",
105
- "",
106
- "## Review Posture",
107
- "- Prioritize structural issues, then convention drift, then line-level polish.",
108
- "- Treat convention drift as a question when intent may be deliberate.",
109
- "",
110
- "## Edit Posture",
111
- "- Do not shorten unless requested.",
112
- "- Apply conventions consistently while preserving tone.",
113
- "- Justify significant rewrites.",
114
- "",
115
- "## Voice Notes",
116
- voiceNotes,
117
- "",
118
- ].join("\n");
119
-
120
- return {
121
- ok: true,
122
- markdown,
123
- injected_rules: injectedRules,
124
- };
125
- }
1
+ export * from "./src/styleguide/prose-styleguide-skill.js";