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.
- package/README.md +10 -0
- package/dist/comments/comment-domain.js +919 -0
- package/dist/comments/comments-command.js +356 -64
- package/dist/metadata/metadata-command.js +127 -0
- package/dist/metadata/metadata-domain.js +209 -0
- package/dist/spine/spine-command.js +129 -0
- package/dist/spine/spine-domain.js +274 -0
- package/dist/stego-cli.js +205 -426
- package/package.json +3 -2
- package/projects/fiction-example/spine/characters/CHAR-AGNES.md +17 -0
- package/projects/fiction-example/spine/characters/CHAR-ETIENNE.md +17 -0
- package/projects/fiction-example/spine/characters/CHAR-MATTHAEUS.md +17 -0
- package/projects/fiction-example/spine/characters/CHAR-RAOUL.md +17 -0
- package/projects/fiction-example/spine/characters/_category.md +6 -0
- package/projects/fiction-example/spine/locations/LOC-CHARNEL.md +17 -0
- package/projects/fiction-example/spine/locations/LOC-COLLEGE.md +17 -0
- package/projects/fiction-example/spine/locations/LOC-HOTELDIEU.md +17 -0
- package/projects/fiction-example/spine/locations/LOC-QUAY.md +17 -0
- package/projects/fiction-example/spine/locations/_category.md +6 -0
- package/projects/fiction-example/spine/sources/SRC-CONJUNCTION.md +20 -0
- package/projects/fiction-example/spine/sources/SRC-GALEN.md +20 -0
- package/projects/fiction-example/spine/sources/SRC-WARD-DATA.md +20 -0
- package/projects/fiction-example/spine/sources/_category.md +6 -0
- package/projects/fiction-example/stego-project.json +1 -18
- package/projects/stego-docs/manuscript/500-project-configuration.md +3 -3
- package/projects/stego-docs/spine/commands/CMD-BUILD.md +11 -0
- package/projects/stego-docs/spine/commands/CMD-CHECK-STAGE.md +11 -0
- package/projects/stego-docs/spine/commands/CMD-EXPORT.md +11 -0
- package/projects/stego-docs/spine/commands/CMD-INIT.md +11 -0
- package/projects/stego-docs/spine/commands/CMD-LIST-PROJECTS.md +10 -0
- package/projects/stego-docs/spine/commands/CMD-NEW-PROJECT.md +10 -0
- package/projects/stego-docs/spine/commands/CMD-NEW.md +11 -0
- package/projects/stego-docs/spine/commands/CMD-VALIDATE.md +11 -0
- package/projects/stego-docs/spine/commands/_category.md +6 -0
- package/projects/stego-docs/spine/concepts/CON-COMPILE-STRUCTURE.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-DIST.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-MANUSCRIPT.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-METADATA.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-NOTES.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-PROJECT.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-SPINE-CATEGORY.md +11 -0
- package/projects/stego-docs/spine/concepts/CON-SPINE.md +9 -0
- package/projects/stego-docs/spine/concepts/CON-STAGE-GATE.md +10 -0
- package/projects/stego-docs/spine/concepts/CON-WORKSPACE.md +9 -0
- package/projects/stego-docs/spine/concepts/_category.md +6 -0
- package/projects/stego-docs/spine/configuration/CFG-ALLOWED-STATUSES.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-COMPILE-LEVELS.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-COMPILE-STRUCTURE.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-REQUIRED-METADATA.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-SPINE-CATEGORIES.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-STAGE-POLICIES.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-STEGO-CONFIG.md +9 -0
- package/projects/stego-docs/spine/configuration/CFG-STEGO-PROJECT.md +9 -0
- package/projects/stego-docs/spine/configuration/_category.md +6 -0
- package/projects/stego-docs/spine/integrations/INT-CSPELL.md +9 -0
- package/projects/stego-docs/spine/integrations/INT-MARKDOWNLINT.md +9 -0
- package/projects/stego-docs/spine/integrations/INT-PANDOC.md +9 -0
- package/projects/stego-docs/spine/integrations/INT-SAURUS-EXTENSION.md +9 -0
- package/projects/stego-docs/spine/integrations/INT-STEGO-EXTENSION.md +9 -0
- package/projects/stego-docs/spine/integrations/INT-VSCODE.md +9 -0
- package/projects/stego-docs/spine/integrations/_category.md +6 -0
- package/projects/stego-docs/spine/workflows/FLOW-BUILD-EXPORT.md +10 -0
- package/projects/stego-docs/spine/workflows/FLOW-DAILY-WRITING.md +10 -0
- package/projects/stego-docs/spine/workflows/FLOW-INIT-WORKSPACE.md +9 -0
- package/projects/stego-docs/spine/workflows/FLOW-NEW-PROJECT.md +10 -0
- package/projects/stego-docs/spine/workflows/FLOW-PROOF-RELEASE.md +10 -0
- package/projects/stego-docs/spine/workflows/FLOW-STAGE-PROMOTION.md +10 -0
- package/projects/stego-docs/spine/workflows/_category.md +6 -0
- package/projects/stego-docs/stego-project.json +1 -28
- package/dist/comments/add-comment.js +0 -382
- package/projects/fiction-example/spine/characters.md +0 -35
- package/projects/fiction-example/spine/locations.md +0 -37
- package/projects/fiction-example/spine/sources.md +0 -31
- package/projects/stego-docs/spine/commands.md +0 -71
- package/projects/stego-docs/spine/concepts.md +0 -72
- package/projects/stego-docs/spine/configuration.md +0 -57
- package/projects/stego-docs/spine/integrations.md +0 -43
- 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,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.
|
|
@@ -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
|