stego-cli 0.3.3 → 0.4.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.
Files changed (78) hide show
  1. package/README.md +10 -0
  2. package/dist/comments/comment-domain.js +919 -0
  3. package/dist/comments/comments-command.js +356 -64
  4. package/dist/metadata/metadata-command.js +127 -0
  5. package/dist/metadata/metadata-domain.js +209 -0
  6. package/dist/spine/spine-command.js +129 -0
  7. package/dist/spine/spine-domain.js +274 -0
  8. package/dist/stego-cli.js +205 -426
  9. package/package.json +3 -2
  10. package/projects/fiction-example/spine/characters/CHAR-AGNES.md +17 -0
  11. package/projects/fiction-example/spine/characters/CHAR-ETIENNE.md +17 -0
  12. package/projects/fiction-example/spine/characters/CHAR-MATTHAEUS.md +17 -0
  13. package/projects/fiction-example/spine/characters/CHAR-RAOUL.md +17 -0
  14. package/projects/fiction-example/spine/characters/_category.md +6 -0
  15. package/projects/fiction-example/spine/locations/LOC-CHARNEL.md +17 -0
  16. package/projects/fiction-example/spine/locations/LOC-COLLEGE.md +17 -0
  17. package/projects/fiction-example/spine/locations/LOC-HOTELDIEU.md +17 -0
  18. package/projects/fiction-example/spine/locations/LOC-QUAY.md +17 -0
  19. package/projects/fiction-example/spine/locations/_category.md +6 -0
  20. package/projects/fiction-example/spine/sources/SRC-CONJUNCTION.md +20 -0
  21. package/projects/fiction-example/spine/sources/SRC-GALEN.md +20 -0
  22. package/projects/fiction-example/spine/sources/SRC-WARD-DATA.md +20 -0
  23. package/projects/fiction-example/spine/sources/_category.md +6 -0
  24. package/projects/fiction-example/stego-project.json +1 -18
  25. package/projects/stego-docs/manuscript/500-project-configuration.md +3 -3
  26. package/projects/stego-docs/spine/commands/CMD-BUILD.md +11 -0
  27. package/projects/stego-docs/spine/commands/CMD-CHECK-STAGE.md +11 -0
  28. package/projects/stego-docs/spine/commands/CMD-EXPORT.md +11 -0
  29. package/projects/stego-docs/spine/commands/CMD-INIT.md +11 -0
  30. package/projects/stego-docs/spine/commands/CMD-LIST-PROJECTS.md +10 -0
  31. package/projects/stego-docs/spine/commands/CMD-NEW-PROJECT.md +10 -0
  32. package/projects/stego-docs/spine/commands/CMD-NEW.md +11 -0
  33. package/projects/stego-docs/spine/commands/CMD-VALIDATE.md +11 -0
  34. package/projects/stego-docs/spine/commands/_category.md +6 -0
  35. package/projects/stego-docs/spine/concepts/CON-COMPILE-STRUCTURE.md +9 -0
  36. package/projects/stego-docs/spine/concepts/CON-DIST.md +9 -0
  37. package/projects/stego-docs/spine/concepts/CON-MANUSCRIPT.md +9 -0
  38. package/projects/stego-docs/spine/concepts/CON-METADATA.md +9 -0
  39. package/projects/stego-docs/spine/concepts/CON-NOTES.md +9 -0
  40. package/projects/stego-docs/spine/concepts/CON-PROJECT.md +9 -0
  41. package/projects/stego-docs/spine/concepts/CON-SPINE-CATEGORY.md +11 -0
  42. package/projects/stego-docs/spine/concepts/CON-SPINE.md +9 -0
  43. package/projects/stego-docs/spine/concepts/CON-STAGE-GATE.md +10 -0
  44. package/projects/stego-docs/spine/concepts/CON-WORKSPACE.md +9 -0
  45. package/projects/stego-docs/spine/concepts/_category.md +6 -0
  46. package/projects/stego-docs/spine/configuration/CFG-ALLOWED-STATUSES.md +9 -0
  47. package/projects/stego-docs/spine/configuration/CFG-COMPILE-LEVELS.md +9 -0
  48. package/projects/stego-docs/spine/configuration/CFG-COMPILE-STRUCTURE.md +9 -0
  49. package/projects/stego-docs/spine/configuration/CFG-REQUIRED-METADATA.md +9 -0
  50. package/projects/stego-docs/spine/configuration/CFG-SPINE-CATEGORIES.md +9 -0
  51. package/projects/stego-docs/spine/configuration/CFG-STAGE-POLICIES.md +9 -0
  52. package/projects/stego-docs/spine/configuration/CFG-STEGO-CONFIG.md +9 -0
  53. package/projects/stego-docs/spine/configuration/CFG-STEGO-PROJECT.md +9 -0
  54. package/projects/stego-docs/spine/configuration/_category.md +6 -0
  55. package/projects/stego-docs/spine/integrations/INT-CSPELL.md +9 -0
  56. package/projects/stego-docs/spine/integrations/INT-MARKDOWNLINT.md +9 -0
  57. package/projects/stego-docs/spine/integrations/INT-PANDOC.md +9 -0
  58. package/projects/stego-docs/spine/integrations/INT-SAURUS-EXTENSION.md +9 -0
  59. package/projects/stego-docs/spine/integrations/INT-STEGO-EXTENSION.md +9 -0
  60. package/projects/stego-docs/spine/integrations/INT-VSCODE.md +9 -0
  61. package/projects/stego-docs/spine/integrations/_category.md +6 -0
  62. package/projects/stego-docs/spine/workflows/FLOW-BUILD-EXPORT.md +10 -0
  63. package/projects/stego-docs/spine/workflows/FLOW-DAILY-WRITING.md +10 -0
  64. package/projects/stego-docs/spine/workflows/FLOW-INIT-WORKSPACE.md +9 -0
  65. package/projects/stego-docs/spine/workflows/FLOW-NEW-PROJECT.md +10 -0
  66. package/projects/stego-docs/spine/workflows/FLOW-PROOF-RELEASE.md +10 -0
  67. package/projects/stego-docs/spine/workflows/FLOW-STAGE-PROMOTION.md +10 -0
  68. package/projects/stego-docs/spine/workflows/_category.md +6 -0
  69. package/projects/stego-docs/stego-project.json +1 -28
  70. package/dist/comments/add-comment.js +0 -382
  71. package/projects/fiction-example/spine/characters.md +0 -35
  72. package/projects/fiction-example/spine/locations.md +0 -37
  73. package/projects/fiction-example/spine/sources.md +0 -31
  74. package/projects/stego-docs/spine/commands.md +0 -71
  75. package/projects/stego-docs/spine/concepts.md +0 -72
  76. package/projects/stego-docs/spine/configuration.md +0 -57
  77. package/projects/stego-docs/spine/integrations.md +0 -43
  78. package/projects/stego-docs/spine/workflows.md +0 -48
@@ -0,0 +1,9 @@
1
+ ---
2
+ label: Pandoc is used for optional export formats such as docx, pdf, and epub.
3
+ ---
4
+
5
+ # Pandoc is used for optional export formats such as docx, pdf, and epub.
6
+
7
+ - Pandoc is used for optional export formats such as docx, pdf, and epub.
8
+ - Related commands: CMD-EXPORT.
9
+ - Related workflows: FLOW-BUILD-EXPORT.
@@ -0,0 +1,9 @@
1
+ ---
2
+ label: The Saurus extension complements prose editing and research workflows in project folders.
3
+ ---
4
+
5
+ # The Saurus extension complements prose editing and research workflows in project folders.
6
+
7
+ - The Saurus extension complements prose editing and research workflows in project folders.
8
+ - Related integrations: INT-VSCODE.
9
+ - Related workflows: FLOW-DAILY-WRITING.
@@ -0,0 +1,9 @@
1
+ ---
2
+ label: The Stego VS Code extension is the official UI for Stego projects, including status controls, checks, and Spine Browser navigation.
3
+ ---
4
+
5
+ # The Stego VS Code extension is the official UI for Stego projects, including status controls, checks, and Spine Browser navigation.
6
+
7
+ - The Stego VS Code extension is the official UI for Stego projects, including status controls, checks, and Spine Browser navigation.
8
+ - Related concepts: CON-SPINE, CON-STAGE-GATE.
9
+ - Related workflows: FLOW-DAILY-WRITING, FLOW-STAGE-PROMOTION.
@@ -0,0 +1,9 @@
1
+ ---
2
+ label: VS Code is the primary editor environment for Stego projects.
3
+ ---
4
+
5
+ # VS Code is the primary editor environment for Stego projects.
6
+
7
+ - VS Code is the primary editor environment for Stego projects.
8
+ - Related workflows: FLOW-INIT-WORKSPACE, FLOW-DAILY-WRITING.
9
+ - Related commands: CMD-INIT.
@@ -0,0 +1,6 @@
1
+ ---
2
+ label: Integrations
3
+ ---
4
+
5
+ # Integrations
6
+
@@ -0,0 +1,10 @@
1
+ ---
2
+ label: Build a compiled markdown manuscript and export distribution formats.
3
+ ---
4
+
5
+ # Build a compiled markdown manuscript and export distribution formats.
6
+
7
+ - Build a compiled markdown manuscript and export distribution formats.
8
+ - Related commands: CMD-BUILD, CMD-EXPORT.
9
+ - Related concepts: CON-DIST, CON-COMPILE-STRUCTURE.
10
+ - Related integrations: INT-PANDOC.
@@ -0,0 +1,10 @@
1
+ ---
2
+ label: Open one project, write in manuscript files, validate, build, and commit progress.
3
+ ---
4
+
5
+ # Open one project, write in manuscript files, validate, build, and commit progress.
6
+
7
+ - Open one project, write in manuscript files, validate, build, and commit progress.
8
+ - Related commands: CMD-VALIDATE, CMD-BUILD.
9
+ - Related concepts: CON-MANUSCRIPT, CON-METADATA, CON-DIST.
10
+ - Related integrations: INT-VSCODE.
@@ -0,0 +1,9 @@
1
+ ---
2
+ label: Install the CLI, initialize a workspace, install local dev tools, and inspect scaffolded projects.
3
+ ---
4
+
5
+ # Install the CLI, initialize a workspace, install local dev tools, and inspect scaffolded projects.
6
+
7
+ - Install the CLI, initialize a workspace, install local dev tools, and inspect scaffolded projects.
8
+ - Related commands: CMD-INIT, CMD-LIST-PROJECTS.
9
+ - Related concepts: CON-WORKSPACE, CON-PROJECT.
@@ -0,0 +1,10 @@
1
+ ---
2
+ label: Create a new project, review generated folders, and configure project metadata rules.
3
+ ---
4
+
5
+ # Create a new project, review generated folders, and configure project metadata rules.
6
+
7
+ - Create a new project, review generated folders, and configure project metadata rules.
8
+ - Related commands: CMD-NEW-PROJECT, CMD-VALIDATE.
9
+ - Related concepts: CON-PROJECT, CON-MANUSCRIPT, CON-SPINE.
10
+ - Related configuration: CFG-STEGO-PROJECT.
@@ -0,0 +1,10 @@
1
+ ---
2
+ label: Run strict checks, build outputs, export artifacts, and archive release files.
3
+ ---
4
+
5
+ # Run strict checks, build outputs, export artifacts, and archive release files.
6
+
7
+ - Run strict checks, build outputs, export artifacts, and archive release files.
8
+ - Related commands: CMD-CHECK-STAGE, CMD-BUILD, CMD-EXPORT.
9
+ - Related concepts: CON-STAGE-GATE, CON-DIST.
10
+ - Related integrations: INT-MARKDOWNLINT, INT-CSPELL.
@@ -0,0 +1,10 @@
1
+ ---
2
+ label: Move files through statuses and verify readiness with stage checks.
3
+ ---
4
+
5
+ # Move files through statuses and verify readiness with stage checks.
6
+
7
+ - Move files through statuses and verify readiness with stage checks.
8
+ - Related commands: CMD-CHECK-STAGE, CMD-VALIDATE.
9
+ - Related concepts: CON-STAGE-GATE, CON-METADATA.
10
+ - Related configuration: CFG-STAGE-POLICIES, CFG-ALLOWED-STATUSES.
@@ -0,0 +1,6 @@
1
+ ---
2
+ label: Workflows
3
+ ---
4
+
5
+ # Workflows
6
+
@@ -19,32 +19,5 @@
19
19
  "pageBreak": "between-groups"
20
20
  }
21
21
  ]
22
- },
23
- "spineCategories": [
24
- {
25
- "key": "commands",
26
- "prefix": "CMD",
27
- "notesFile": "commands.md"
28
- },
29
- {
30
- "key": "concepts",
31
- "prefix": "CON",
32
- "notesFile": "concepts.md"
33
- },
34
- {
35
- "key": "workflows",
36
- "prefix": "FLOW",
37
- "notesFile": "workflows.md"
38
- },
39
- {
40
- "key": "configuration",
41
- "prefix": "CFG",
42
- "notesFile": "configuration.md"
43
- },
44
- {
45
- "key": "integrations",
46
- "prefix": "INT",
47
- "notesFile": "integrations.md"
48
- }
49
- ]
22
+ }
50
23
  }
@@ -1,382 +0,0 @@
1
- import fs from "node:fs";
2
- import path from "node:path";
3
- import { CommentsCommandError } from "./errors.js";
4
- const COMMENT_DELIMITER_REGEX = /^<!--\s*comment:\s*(CMT-(\d{4,}))\s*-->\s*$/i;
5
- const LEGACY_COMMENT_HEADING_REGEX = /^###\s+(CMT-(\d{4,}))\s*$/i;
6
- const START_SENTINEL = "<!-- stego-comments:start -->";
7
- const END_SENTINEL = "<!-- stego-comments:end -->";
8
- /** Adds one stego comment entry to a manuscript and writes the updated file. */
9
- export function addCommentToManuscript(request) {
10
- const absolutePath = path.resolve(request.cwd, request.manuscriptPath);
11
- if (!fs.existsSync(absolutePath) || !fs.statSync(absolutePath).isFile()) {
12
- throw new CommentsCommandError("INVALID_USAGE", 2, `Manuscript file not found: ${request.manuscriptPath}`);
13
- }
14
- const raw = fs.readFileSync(absolutePath, "utf8");
15
- const lineEnding = raw.includes("\r\n") ? "\r\n" : "\n";
16
- const lines = raw.split(/\r?\n/);
17
- const sentinelState = resolveSentinelState(lines);
18
- const hasYamlFrontmatter = raw.startsWith(`---${lineEnding}`) || raw.startsWith("---\n");
19
- if (!sentinelState.hasSentinels && !hasYamlFrontmatter) {
20
- throw new CommentsCommandError("NOT_STEGO_MANUSCRIPT", 3, "File is not recognized as a Stego manuscript (missing frontmatter and comments sentinels).");
21
- }
22
- const commentId = getNextCommentId(lines, sentinelState);
23
- const now = new Date();
24
- const createdAt = now.toISOString();
25
- const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone || undefined;
26
- const timezoneOffsetMinutes = -now.getTimezoneOffset();
27
- const paragraphIndex = request.range
28
- ? resolveParagraphIndex(lines, lineEnding, sentinelState, request.range)
29
- : undefined;
30
- const excerpt = request.range
31
- ? extractExcerptForRange(lines, request.range)
32
- : undefined;
33
- const meta = buildMetaPayload({
34
- createdAt,
35
- timezone,
36
- timezoneOffsetMinutes,
37
- paragraphIndex,
38
- range: request.range,
39
- sourceMeta: request.sourceMeta
40
- });
41
- const meta64 = Buffer.from(JSON.stringify(meta), "utf8").toString("base64url");
42
- const entryLines = buildCommentEntryLines({
43
- commentId,
44
- createdAt,
45
- author: request.author,
46
- message: request.message,
47
- meta64,
48
- paragraphIndex,
49
- excerpt
50
- });
51
- const nextLines = applyCommentEntry(lines, sentinelState, entryLines);
52
- const nextContent = `${nextLines.join(lineEnding)}${lineEnding}`;
53
- try {
54
- fs.writeFileSync(absolutePath, nextContent, "utf8");
55
- }
56
- catch (error) {
57
- const message = error instanceof Error ? error.message : String(error);
58
- throw new CommentsCommandError("WRITE_FAILURE", 6, `Failed to update manuscript: ${message}`);
59
- }
60
- const result = {
61
- ok: true,
62
- manuscript: absolutePath,
63
- commentId,
64
- status: "open",
65
- anchor: request.range
66
- ? {
67
- type: "selection",
68
- excerptStartLine: request.range.startLine,
69
- excerptStartCol: request.range.startCol,
70
- excerptEndLine: request.range.endLine,
71
- excerptEndCol: request.range.endCol
72
- }
73
- : { type: "file" },
74
- createdAt
75
- };
76
- return result;
77
- }
78
- function buildMetaPayload(input) {
79
- const payload = {
80
- status: "open",
81
- created_at: input.createdAt,
82
- timezone_offset_minutes: input.timezoneOffsetMinutes
83
- };
84
- if (input.timezone) {
85
- payload.timezone = input.timezone;
86
- }
87
- if (input.paragraphIndex !== undefined) {
88
- payload.paragraph_index = input.paragraphIndex;
89
- }
90
- if (input.range) {
91
- payload.excerpt_start_line = input.range.startLine;
92
- payload.excerpt_start_col = input.range.startCol;
93
- payload.excerpt_end_line = input.range.endLine;
94
- payload.excerpt_end_col = input.range.endCol;
95
- }
96
- if (input.sourceMeta && Object.keys(input.sourceMeta).length > 0) {
97
- payload.signature = input.sourceMeta;
98
- }
99
- return payload;
100
- }
101
- function buildCommentEntryLines(input) {
102
- const safeAuthor = input.author.trim() || "Saurus";
103
- const normalizedMessageLines = input.message
104
- .replace(/\r\n/g, "\n")
105
- .split("\n")
106
- .map((line) => line.trimEnd());
107
- const displayTimestamp = formatHumanTimestamp(input.createdAt);
108
- const headerTimestamp = escapeThreadHeaderPart(displayTimestamp);
109
- const headerAuthor = escapeThreadHeaderPart(safeAuthor);
110
- const lines = [
111
- `<!-- comment: ${input.commentId} -->`,
112
- `<!-- meta64: ${input.meta64} -->`,
113
- `> _${headerTimestamp} — ${headerAuthor}_`,
114
- ">"
115
- ];
116
- if (input.paragraphIndex !== undefined && input.excerpt) {
117
- const truncatedExcerpt = input.excerpt.length > 100
118
- ? `${input.excerpt.slice(0, 100).trimEnd()}…`
119
- : input.excerpt;
120
- lines.push(`> > “${truncatedExcerpt}”`);
121
- lines.push(">");
122
- }
123
- lines.push(...normalizedMessageLines.map((line) => `> ${line}`));
124
- return lines;
125
- }
126
- function applyCommentEntry(lines, sentinelState, entryLines) {
127
- if (!sentinelState.hasSentinels) {
128
- const baseLines = trimTrailingBlankLines(lines);
129
- return [
130
- ...baseLines,
131
- "",
132
- START_SENTINEL,
133
- "",
134
- ...entryLines,
135
- "",
136
- END_SENTINEL
137
- ];
138
- }
139
- const startIndex = sentinelState.startIndex;
140
- const endIndex = sentinelState.endIndex;
141
- const block = lines.slice(startIndex + 1, endIndex);
142
- const trimmedBlock = trimOuterBlankLines(block);
143
- const nextBlock = trimmedBlock.length > 0
144
- ? [...trimmedBlock, "", ...entryLines]
145
- : [...entryLines];
146
- return [
147
- ...lines.slice(0, startIndex + 1),
148
- ...nextBlock,
149
- ...lines.slice(endIndex)
150
- ];
151
- }
152
- function resolveSentinelState(lines) {
153
- const startIndexes = findTrimmedLineIndexes(lines, START_SENTINEL);
154
- const endIndexes = findTrimmedLineIndexes(lines, END_SENTINEL);
155
- const hasAnySentinel = startIndexes.length > 0 || endIndexes.length > 0;
156
- if (!hasAnySentinel) {
157
- return {
158
- hasSentinels: false,
159
- startIndex: -1,
160
- endIndex: -1
161
- };
162
- }
163
- if (startIndexes.length !== 1 || endIndexes.length !== 1) {
164
- throw new CommentsCommandError("COMMENT_APPENDIX_INVALID", 5, `Expected exactly one '${START_SENTINEL}' and one '${END_SENTINEL}' marker.`);
165
- }
166
- const startIndex = startIndexes[0];
167
- const endIndex = endIndexes[0];
168
- if (endIndex <= startIndex) {
169
- throw new CommentsCommandError("COMMENT_APPENDIX_INVALID", 5, `'${END_SENTINEL}' must appear after '${START_SENTINEL}'.`);
170
- }
171
- return {
172
- hasSentinels: true,
173
- startIndex,
174
- endIndex
175
- };
176
- }
177
- function findTrimmedLineIndexes(lines, target) {
178
- const indexes = [];
179
- for (let index = 0; index < lines.length; index += 1) {
180
- if (lines[index].trim() === target) {
181
- indexes.push(index);
182
- }
183
- }
184
- return indexes;
185
- }
186
- function getNextCommentId(lines, sentinelState) {
187
- const candidateLines = sentinelState.hasSentinels
188
- ? lines.slice(sentinelState.startIndex + 1, sentinelState.endIndex)
189
- : lines;
190
- let maxId = 0;
191
- for (const line of candidateLines) {
192
- const trimmed = line.trim();
193
- const match = trimmed.match(COMMENT_DELIMITER_REGEX) ?? trimmed.match(LEGACY_COMMENT_HEADING_REGEX);
194
- if (!match || !match[2]) {
195
- continue;
196
- }
197
- const numeric = Number.parseInt(match[2], 10);
198
- if (Number.isFinite(numeric) && numeric > maxId) {
199
- maxId = numeric;
200
- }
201
- }
202
- const next = maxId + 1;
203
- return `CMT-${String(next).padStart(4, "0")}`;
204
- }
205
- function resolveParagraphIndex(lines, lineEnding, sentinelState, range) {
206
- const markdownWithoutComments = stripCommentsAppendix(lines, sentinelState, lineEnding);
207
- const { body, lineOffset } = splitFrontmatterForAnchors(markdownWithoutComments);
208
- const paragraphs = extractParagraphs(body).map((paragraph) => ({
209
- ...paragraph,
210
- startLine: paragraph.startLine + lineOffset,
211
- endLine: paragraph.endLine + lineOffset
212
- }));
213
- const matched = findParagraphForLine(paragraphs, range.startLine) ?? findPreviousParagraphForLine(paragraphs, range.startLine);
214
- return matched?.index;
215
- }
216
- function extractExcerptForRange(lines, range) {
217
- const startLineIndex = range.startLine - 1;
218
- const endLineIndex = range.endLine - 1;
219
- if (startLineIndex < 0 || endLineIndex < 0 || startLineIndex >= lines.length || endLineIndex >= lines.length) {
220
- return undefined;
221
- }
222
- if (startLineIndex > endLineIndex || (startLineIndex === endLineIndex && range.startCol >= range.endCol)) {
223
- return undefined;
224
- }
225
- const startLineText = lines[startLineIndex] ?? "";
226
- const endLineText = lines[endLineIndex] ?? "";
227
- const safeStartCol = clamp(range.startCol, 0, startLineText.length);
228
- const safeEndCol = clamp(range.endCol, 0, endLineText.length);
229
- let selected = "";
230
- if (startLineIndex === endLineIndex) {
231
- if (safeStartCol >= safeEndCol) {
232
- return undefined;
233
- }
234
- selected = startLineText.slice(safeStartCol, safeEndCol);
235
- }
236
- else {
237
- const segments = [];
238
- segments.push(startLineText.slice(safeStartCol));
239
- for (let lineIndex = startLineIndex + 1; lineIndex < endLineIndex; lineIndex += 1) {
240
- segments.push(lines[lineIndex] ?? "");
241
- }
242
- segments.push(endLineText.slice(0, safeEndCol));
243
- selected = segments.join("\n");
244
- }
245
- const compact = compactExcerpt(selected);
246
- return compact || undefined;
247
- }
248
- function clamp(value, min, max) {
249
- if (value < min) {
250
- return min;
251
- }
252
- if (value > max) {
253
- return max;
254
- }
255
- return value;
256
- }
257
- function compactExcerpt(value, max = 180) {
258
- const compact = value.replace(/\s+/g, " ").trim();
259
- if (compact.length <= max) {
260
- return compact;
261
- }
262
- return `${compact.slice(0, max - 1)}…`;
263
- }
264
- function stripCommentsAppendix(lines, sentinelState, lineEnding) {
265
- if (!sentinelState.hasSentinels) {
266
- return lines.join(lineEnding);
267
- }
268
- let removeStart = sentinelState.startIndex;
269
- if (removeStart > 0 && lines[removeStart - 1].trim().length === 0) {
270
- removeStart -= 1;
271
- }
272
- const keptLines = [...lines.slice(0, removeStart), ...lines.slice(sentinelState.endIndex + 1)];
273
- while (keptLines.length > 0 && keptLines[keptLines.length - 1].trim().length === 0) {
274
- keptLines.pop();
275
- }
276
- return keptLines.join(lineEnding);
277
- }
278
- function splitFrontmatterForAnchors(markdownText) {
279
- const frontmatterMatch = markdownText.match(/^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/);
280
- if (!frontmatterMatch) {
281
- return {
282
- body: markdownText,
283
- lineOffset: 0
284
- };
285
- }
286
- const consumed = frontmatterMatch[0];
287
- return {
288
- body: markdownText.slice(consumed.length),
289
- lineOffset: countLineBreaks(consumed)
290
- };
291
- }
292
- function countLineBreaks(value) {
293
- const matches = value.match(/\r?\n/g);
294
- return matches ? matches.length : 0;
295
- }
296
- function extractParagraphs(markdownText) {
297
- const lines = markdownText.split(/\r?\n/);
298
- const paragraphs = [];
299
- let currentStart = -1;
300
- const currentLines = [];
301
- const flush = (endIndex) => {
302
- if (currentStart < 0 || currentLines.length === 0) {
303
- return;
304
- }
305
- const joined = currentLines.join(" ").replace(/\s+/g, " ").trim();
306
- if (joined.length > 0) {
307
- paragraphs.push({
308
- index: paragraphs.length,
309
- startLine: currentStart + 1,
310
- endLine: endIndex + 1,
311
- text: joined
312
- });
313
- }
314
- currentStart = -1;
315
- currentLines.length = 0;
316
- };
317
- for (let index = 0; index < lines.length; index += 1) {
318
- const trimmed = lines[index].trim();
319
- if (!trimmed) {
320
- flush(index - 1);
321
- continue;
322
- }
323
- if (currentStart < 0) {
324
- currentStart = index;
325
- }
326
- currentLines.push(trimmed);
327
- }
328
- flush(lines.length - 1);
329
- return paragraphs;
330
- }
331
- function findParagraphForLine(paragraphs, lineNumber) {
332
- return paragraphs.find((paragraph) => lineNumber >= paragraph.startLine && lineNumber <= paragraph.endLine);
333
- }
334
- function findPreviousParagraphForLine(paragraphs, lineNumber) {
335
- for (let index = paragraphs.length - 1; index >= 0; index -= 1) {
336
- if (paragraphs[index].endLine < lineNumber) {
337
- return paragraphs[index];
338
- }
339
- }
340
- return undefined;
341
- }
342
- function formatHumanTimestamp(raw) {
343
- if (!/^\d{4}-\d{2}-\d{2}T/.test(raw)) {
344
- return raw;
345
- }
346
- const date = new Date(raw);
347
- if (Number.isNaN(date.getTime())) {
348
- return raw;
349
- }
350
- const months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
351
- const month = months[date.getUTCMonth()];
352
- const day = date.getUTCDate();
353
- const year = date.getUTCFullYear();
354
- const hours24 = date.getUTCHours();
355
- const hours = hours24 % 12 || 12;
356
- const minutes = String(date.getUTCMinutes()).padStart(2, "0");
357
- const ampm = hours24 < 12 ? "AM" : "PM";
358
- return `${month} ${day}, ${year}, ${hours}:${minutes} ${ampm}`;
359
- }
360
- function escapeThreadHeaderPart(value) {
361
- return value
362
- .replace(/\\/g, "\\\\")
363
- .replace(/_/g, "\\_");
364
- }
365
- function trimTrailingBlankLines(lines) {
366
- const copy = [...lines];
367
- while (copy.length > 0 && copy[copy.length - 1].trim().length === 0) {
368
- copy.pop();
369
- }
370
- return copy;
371
- }
372
- function trimOuterBlankLines(lines) {
373
- let start = 0;
374
- let end = lines.length;
375
- while (start < end && lines[start].trim().length === 0) {
376
- start += 1;
377
- }
378
- while (end > start && lines[end - 1].trim().length === 0) {
379
- end -= 1;
380
- }
381
- return lines.slice(start, end);
382
- }
@@ -1,35 +0,0 @@
1
- # Characters
2
-
3
- ## CHAR-MATTHAEUS
4
- label: Magister Matthaeus de Rota
5
-
6
- - Magister Matthaeus de Rota
7
- - Role: Scholar of medicine and astrology at the University of Paris
8
- - Disposition: Methodical, sincere, incapable of leaving an observation outside his system; treats coherence as a moral obligation
9
- - Sources: Works primarily from the conjunction theory (SRC-CONJUNCTION) and Galenic tradition (SRC-GALEN); consults a prohibited astrological quire privately for its method of reasoning from hidden correspondences
10
- - Arc: Builds a total explanation of the {{pestilence}} and dies at peace inside it; the question is whether his peace is understanding or its most sophisticated substitute
11
-
12
- ## CHAR-ETIENNE
13
- label: Etienne of Saint-Marcel
14
-
15
- - Etienne of Saint-Marcel
16
- - Role: Young cleric-scribe attached to Matthaeus (CHAR-MATTHAEUS)
17
- - Disposition: Careful, loyal, doctrinally cautious; sees everything his master does and says nothing about the parts that trouble him
18
- - Function: The story's witness; records the system without fully believing it, but also without offering an alternative
19
-
20
- ## CHAR-AGNES
21
- label: Agnes the apothecary
22
-
23
- - Agnes the apothecary
24
- - Role: Compounder and practical healer near the Petit Pont; supplies Hôtel-Dieu (LOC-HOTELDIEU)
25
- - Disposition: Empirical, blunt, uninterested in frameworks that don't predict which street sickens next
26
- - Sources: Her own ward observations (SRC-WARD-DATA); she has no name for her method, which is part of its strength and its institutional weakness
27
- - Function: The counterweight; asks whether a system that can absorb any evidence without changing is a system or a habit
28
-
29
- ## CHAR-RAOUL
30
- label: Canon Raoul de Villiers
31
-
32
- - Canon Raoul de Villiers
33
- - Role: Cathedral canon and royal intermediary overseeing the inquiry
34
- - Disposition: Pragmatic, literate, concerned with language fit for governance; judges arguments by their civic effects, not their truth
35
- - Relationship: Commissioned Matthaeus (CHAR-MATTHAEUS) and will approve or suppress his conclusions; represents the institution's need for a single authorized account
@@ -1,37 +0,0 @@
1
- # Locations
2
-
3
- ## LOC-COLLEGE
4
- label: College chamber near the Rue Saint-Jacques
5
-
6
- - College chamber near the Rue Saint-Jacques
7
- - Study and disputation room
8
- - Reference: [University of Paris](https://en.wikipedia.org/wiki/University_of_Paris), [Rue Saint-Jacques](https://en.wikipedia.org/wiki/Rue_Saint-Jacques_(Paris))
9
- - Stone-walled, poorly lit, dominated by a long table covered in manuscripts; the room where Matthaeus (CHAR-MATTHAEUS) and Etienne (CHAR-ETIENNE) compile the memorandum
10
- - Significance: The space where the system is built; physically separated from the wards and streets where the plague actually operates
11
-
12
- ## LOC-HOTELDIEU
13
- label: "Hôtel-Dieu"
14
-
15
- - Hôtel-Dieu
16
- - Hospital ward near Notre-Dame
17
- - Reference: [Hôtel-Dieu de Paris](https://en.wikipedia.org/wiki/H%C3%B4tel-Dieu_de_Paris)
18
- - Crowded, loud, smelling of vinegar and linen; priests read offices while surgeons open buboes; Agnes (CHAR-AGNES) manages the supply lines
19
- - Significance: Where data is collected; the distance between this room and the college chamber is the distance between observation and explanation
20
-
21
- ## LOC-QUAY
22
- label: Grain quay on the Seine
23
-
24
- - Grain quay on the Seine
25
- - River unloading zone
26
- - Reference: [Port de la Grève](https://en.wikipedia.org/wiki/Place_de_Gr%C3%A8ve) (medieval commercial riverfront)
27
- - Torn sackcloth, heavy foot traffic, commercial contact between upstream suppliers and the city's markets
28
- - Significance: Agnes's observations (SRC-WARD-DATA) trace infection along this route; it is the strongest evidence for material transmission and the hardest for the celestial framework (SRC-CONJUNCTION) to accommodate
29
-
30
- ## LOC-CHARNEL
31
- label: Charnel precinct at the cemetery edge
32
-
33
- - Charnel precinct at the cemetery edge
34
- - Overflow burial ground
35
- - Reference: [Holy Innocents' Cemetery](https://en.wikipedia.org/wiki/Saints_Innocents%27_Cemetery)
36
- - Where the dead go when the dead outnumber the rites; compressed ritual, civic exhaustion
37
- - Significance: The place that makes the need for an explanation urgent — not because understanding will stop the dying, but because the alternative is dying without a frame
@@ -1,31 +0,0 @@
1
- # Sources
2
-
3
- ## SRC-CONJUNCTION
4
- label: The Great Conjunction theory
5
-
6
- - The Great Conjunction theory
7
- - Tradition: University astrology ([Paris faculty opinion of 1348](https://en.wikipedia.org/wiki/Black_Death#Medical_knowledge))
8
- - Reference: [Great Conjunction](https://en.wikipedia.org/wiki/Great_conjunction)
9
- - Claim: The conjunction of Saturn, Jupiter, and Mars in Aquarius corrupted the atmosphere universally; local outbreaks are secondary effects of a celestial primary cause
10
- - Used by: Matthaeus (CHAR-MATTHAEUS) as the apex of his causal hierarchy; Raoul (CHAR-RAOUL) as the politically safest framing
11
- - Limitation: Explains everything at once, which means it predicts nothing in particular
12
-
13
- ## SRC-GALEN
14
- label: Galenic humoral medicine
15
-
16
- - Galenic humoral medicine
17
- - Tradition: Greco-Arabic medical inheritance
18
- - Reference: [Humorism](https://en.wikipedia.org/wiki/Humorism), [Galen](https://en.wikipedia.org/wiki/Galen)
19
- - Claim: Individual susceptibility depends on bodily complexion (balance of humors); treatment involves restoring balance through diet, purgation, and environment
20
- - Used by: Matthaeus (CHAR-MATTHAEUS) as the middle layer of his framework — celestial disposition selects who is vulnerable, humoral constitution determines who actually sickens
21
- - Limitation: Cannot explain why entire neighborhoods fall at once regardless of individual constitution
22
-
23
- ## SRC-WARD-DATA
24
- label: "Agnes's ward observations"
25
-
26
- - Agnes's ward observations
27
- - Tradition: None; empirical record-keeping without institutional backing
28
- - Reference: [Miasma theory](https://en.wikipedia.org/wiki/Miasma_theory) (the contemporary framework Agnes's data implicitly challenges)
29
- - Claim: Infection follows grain traffic and commercial contact; neighborhoods tied to the quay (LOC-QUAY) sicken first, then adjacent streets, then the rest
30
- - Used by: Agnes (CHAR-AGNES) as practical knowledge; Matthaeus (CHAR-MATTHAEUS) acknowledges the pattern but classifies it as a secondary distributing cause, subordinate to celestial disposition
31
- - Limitation: No theoretical framework to explain why the pattern holds; accurate but institutionally weightless