ocean-brain 0.5.0 → 0.6.1
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/dist/mcp.js +660 -31
- package/package.json +2 -1
- package/server/client/dist/assets/{Calendar-BU9XgX2N.js → Calendar-BJHLksEZ.js} +1 -1
- package/server/client/dist/assets/{Callout-C6tVCxOT.js → Callout-wFx9oCas.js} +1 -1
- package/server/client/dist/assets/Graph-9piERaNR.js +1 -0
- package/server/client/dist/assets/{Image-DxWhlIDG.js → Image-DK0lnaK2.js} +1 -1
- package/server/client/dist/assets/{Image.es-BjGK7y6Y.js → Image.es-Be7par_j.js} +1 -1
- package/server/client/dist/assets/Note-CC9ybxXL.js +21 -0
- package/server/client/dist/assets/{Plus.es-EcmtS_uw.js → Plus.es-BKm7uDYu.js} +1 -1
- package/server/client/dist/assets/Reminders-DRxA8v98.js +1 -0
- package/server/client/dist/assets/Search-BunSQrqv.js +1 -0
- package/server/client/dist/assets/{SurfaceCard-CO0oVxeL.js → SurfaceCard-FhlC8VSR.js} +1 -1
- package/server/client/dist/assets/Tag-z_crJ5JP.js +1 -0
- package/server/client/dist/assets/TagNotes-D7wIJ4jy.js +1 -0
- package/server/client/dist/assets/{Trash.es-DmwfSy3l.js → Trash.es-ByMVwx0d.js} +1 -1
- package/server/client/dist/assets/ViewNotes-Duo9XmcJ.js +1 -0
- package/server/client/dist/assets/Views-D4Z_O9Kd.js +1 -0
- package/server/client/dist/assets/{image.api-DeF9ck7g.js → image.api-B5Qi-Txv.js} +1 -1
- package/server/client/dist/assets/{index-BZUGWdOU.css → index-Cd81UxEh.css} +1 -1
- package/server/client/dist/assets/index-cHJQCo8V.js +1 -0
- package/server/client/dist/assets/index-nYj7BL3a.js +327 -0
- package/server/client/dist/assets/manage-image-detail-DcZggB1M.js +1 -0
- package/server/client/dist/assets/manage-image-glbtBibn.js +1 -0
- package/server/client/dist/assets/{mcp-Bh743HBz.js → mcp-BBUpQdS9.js} +1 -1
- package/server/client/dist/assets/{note-core-Z6kqulGB.js → note-core-DMZcZ4yV.js} +53 -53
- package/server/client/dist/assets/{note-vendor-i1M2FSI2.js → note-vendor-uuJO2MTD.js} +1 -1
- package/server/client/dist/assets/{placeholder-XQqo1Fvr.js → placeholder-B_qb_qkc.js} +5 -5
- package/server/client/dist/assets/properties-CiyQv6Tb.js +1 -0
- package/server/client/dist/assets/trash-CQnj4Rmv.js +1 -0
- package/server/client/dist/assets/{useReminderMutate-D21PurIS.js → useReminderMutate-CSKliBcc.js} +1 -1
- package/server/client/dist/assets/view-dashboard-B5fKgvk_.js +99 -0
- package/server/client/dist/index.html +4 -4
- package/server/dist/features/note/graphql/note.field.resolver.js +2 -0
- package/server/dist/features/note/graphql/note.field.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.mutation.resolver.js +119 -0
- package/server/dist/features/note/graphql/note.mutation.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.query.resolver.js +14 -0
- package/server/dist/features/note/graphql/note.query.resolver.js.map +1 -1
- package/server/dist/features/note/graphql/note.type-defs.js +100 -0
- package/server/dist/features/note/graphql/note.type-defs.js.map +1 -1
- package/server/dist/features/note/http/mcp.js +394 -1
- package/server/dist/features/note/http/mcp.js.map +1 -1
- package/server/dist/features/note/services/markdown-intent-write.js +495 -0
- package/server/dist/features/note/services/markdown-intent-write.js.map +1 -0
- package/server/dist/features/note/services/markdown-patch.js +601 -0
- package/server/dist/features/note/services/markdown-patch.js.map +1 -0
- package/server/dist/features/note/services/properties.js +874 -0
- package/server/dist/features/note/services/properties.js.map +1 -0
- package/server/dist/features/note/services/snapshot.js +94 -12
- package/server/dist/features/note/services/snapshot.js.map +1 -1
- package/server/dist/features/note/services/trash.js +70 -2
- package/server/dist/features/note/services/trash.js.map +1 -1
- package/server/dist/features/note/services/write.js +21 -7
- package/server/dist/features/note/services/write.js.map +1 -1
- package/server/dist/features/view/graphql/view.query.resolver.js +31 -1
- package/server/dist/features/view/graphql/view.query.resolver.js.map +1 -1
- package/server/dist/features/view/graphql/view.type-defs.js +57 -1
- package/server/dist/features/view/graphql/view.type-defs.js.map +1 -1
- package/server/dist/features/view/services/workspace.js +367 -43
- package/server/dist/features/view/services/workspace.js.map +1 -1
- package/server/dist/modules/blocknote.js +367 -7
- package/server/dist/modules/blocknote.js.map +1 -1
- package/server/dist/routes/mcp.js +21 -1
- package/server/dist/routes/mcp.js.map +1 -1
- package/server/prisma/migrations/20260527073000_0017_note_properties/migration.sql +105 -0
- package/server/prisma/migrations/20260531073000_0018_view_query/migration.sql +3 -0
- package/server/prisma/schema.prisma +226 -130
- package/server/client/dist/assets/Graph-Dond2hGK.js +0 -1
- package/server/client/dist/assets/Note-Cr6GtXUM.js +0 -21
- package/server/client/dist/assets/Reminders-BlLLT_x0.js +0 -1
- package/server/client/dist/assets/Search-CrZ0ap8x.js +0 -1
- package/server/client/dist/assets/Tag-vws5AfNW.js +0 -1
- package/server/client/dist/assets/TagNotes-Fmf1Kgim.js +0 -1
- package/server/client/dist/assets/ViewNotes-BALN7IfH.js +0 -1
- package/server/client/dist/assets/Views-Ccc_Ml5U.js +0 -1
- package/server/client/dist/assets/index-BSVE2cGd.js +0 -1
- package/server/client/dist/assets/index-BvKBTjQq.js +0 -246
- package/server/client/dist/assets/manage-image-IoTXaMBP.js +0 -1
- package/server/client/dist/assets/manage-image-detail-BRTXwOsh.js +0 -1
- package/server/client/dist/assets/trash-hiJIg6-c.js +0 -1
- package/server/client/dist/assets/view-dashboard-BuaZ4MvI.js +0 -90
|
@@ -0,0 +1,601 @@
|
|
|
1
|
+
import crypto from "crypto";
|
|
2
|
+
const HASH_ALGORITHM = "sha256";
|
|
3
|
+
const EXCERPT_CONTEXT_CHARS = 80;
|
|
4
|
+
const DIFF_CONTEXT_LINES = 3;
|
|
5
|
+
const calculateMarkdownSha256 = (value) => {
|
|
6
|
+
return crypto.createHash(HASH_ALGORITHM).update(value, "utf8").digest("hex");
|
|
7
|
+
};
|
|
8
|
+
const countNewlines = (value) => {
|
|
9
|
+
let count = 0;
|
|
10
|
+
for (const character of value) {
|
|
11
|
+
if (character === "\n") {
|
|
12
|
+
count += 1;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
return count;
|
|
16
|
+
};
|
|
17
|
+
const getLineRange = (markdown, match) => {
|
|
18
|
+
const lineStart = countNewlines(markdown.slice(0, match.start)) + 1;
|
|
19
|
+
const lineEnd = lineStart + countNewlines(match.text);
|
|
20
|
+
return {
|
|
21
|
+
lineStart,
|
|
22
|
+
lineEnd
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
const buildExcerpt = (markdown, start, end) => {
|
|
26
|
+
const excerpt = markdown.slice(start, end).trim();
|
|
27
|
+
return excerpt.length > 0 ? excerpt : void 0;
|
|
28
|
+
};
|
|
29
|
+
const toPatchMatch = (markdown, match, totalMatches) => {
|
|
30
|
+
const { lineStart, lineEnd } = getLineRange(markdown, match);
|
|
31
|
+
const beforeStart = Math.max(0, match.start - EXCERPT_CONTEXT_CHARS);
|
|
32
|
+
const afterEnd = Math.min(markdown.length, match.end + EXCERPT_CONTEXT_CHARS);
|
|
33
|
+
const beforeText = markdown.slice(beforeStart, match.start);
|
|
34
|
+
const afterText = markdown.slice(match.end, afterEnd);
|
|
35
|
+
return {
|
|
36
|
+
matchIndex: match.matchIndex,
|
|
37
|
+
lineStart,
|
|
38
|
+
lineEnd,
|
|
39
|
+
text: match.text,
|
|
40
|
+
beforeExcerpt: buildExcerpt(markdown, beforeStart, match.start),
|
|
41
|
+
afterExcerpt: buildExcerpt(markdown, match.end, afterEnd),
|
|
42
|
+
matchSha256: calculateMarkdownSha256(match.text),
|
|
43
|
+
surroundingHash: calculateMarkdownSha256(JSON.stringify({ beforeText, targetText: match.text, afterText })),
|
|
44
|
+
...match.matchIndex === 0 && totalMatches > 1 ? { positionHint: "first" } : {},
|
|
45
|
+
...match.matchIndex === totalMatches - 1 && totalMatches > 1 ? { positionHint: "last" } : {}
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
const findExactTextMatches = (markdown, text) => {
|
|
49
|
+
const matches = [];
|
|
50
|
+
let searchStart = 0;
|
|
51
|
+
while (searchStart <= markdown.length) {
|
|
52
|
+
const start = markdown.indexOf(text, searchStart);
|
|
53
|
+
if (start === -1) {
|
|
54
|
+
break;
|
|
55
|
+
}
|
|
56
|
+
matches.push({
|
|
57
|
+
matchIndex: matches.length,
|
|
58
|
+
start,
|
|
59
|
+
end: start + text.length,
|
|
60
|
+
text
|
|
61
|
+
});
|
|
62
|
+
searchStart = start + text.length;
|
|
63
|
+
}
|
|
64
|
+
return matches;
|
|
65
|
+
};
|
|
66
|
+
const splitLines = (value) => {
|
|
67
|
+
return value.split("\n");
|
|
68
|
+
};
|
|
69
|
+
const findSharedLineEdges = (beforeLines, afterLines) => {
|
|
70
|
+
let prefix = 0;
|
|
71
|
+
const maxPrefix = Math.min(beforeLines.length, afterLines.length);
|
|
72
|
+
while (prefix < maxPrefix && beforeLines[prefix] === afterLines[prefix]) {
|
|
73
|
+
prefix += 1;
|
|
74
|
+
}
|
|
75
|
+
let suffix = 0;
|
|
76
|
+
const maxSuffix = Math.min(beforeLines.length - prefix, afterLines.length - prefix);
|
|
77
|
+
while (suffix < maxSuffix && beforeLines[beforeLines.length - 1 - suffix] === afterLines[afterLines.length - 1 - suffix]) {
|
|
78
|
+
suffix += 1;
|
|
79
|
+
}
|
|
80
|
+
return {
|
|
81
|
+
prefix,
|
|
82
|
+
suffix
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const formatDiffRange = (lineIndex, lineCount) => {
|
|
86
|
+
const lineNumber = lineIndex + 1;
|
|
87
|
+
return lineCount === 1 ? String(lineNumber) : `${lineNumber},${lineCount}`;
|
|
88
|
+
};
|
|
89
|
+
const createUnifiedMarkdownDiff = (beforeMarkdown, afterMarkdown, contextLines = DIFF_CONTEXT_LINES) => {
|
|
90
|
+
const beforeLines = splitLines(beforeMarkdown);
|
|
91
|
+
const afterLines = splitLines(afterMarkdown);
|
|
92
|
+
const { prefix, suffix } = findSharedLineEdges(beforeLines, afterLines);
|
|
93
|
+
const changedBeforeStart = prefix;
|
|
94
|
+
const changedBeforeEnd = beforeLines.length - suffix;
|
|
95
|
+
const changedAfterStart = prefix;
|
|
96
|
+
const changedAfterEnd = afterLines.length - suffix;
|
|
97
|
+
const displayBeforeStart = contextLines === null ? 0 : Math.max(0, changedBeforeStart - contextLines);
|
|
98
|
+
const displayBeforeEnd = contextLines === null ? beforeLines.length : Math.min(beforeLines.length, changedBeforeEnd + contextLines);
|
|
99
|
+
const displayAfterStart = contextLines === null ? 0 : Math.max(0, changedAfterStart - contextLines);
|
|
100
|
+
const displayAfterEnd = contextLines === null ? afterLines.length : Math.min(afterLines.length, changedAfterEnd + contextLines);
|
|
101
|
+
const diffLines = [
|
|
102
|
+
"--- before.md",
|
|
103
|
+
"+++ after.md",
|
|
104
|
+
`@@ -${formatDiffRange(displayBeforeStart, displayBeforeEnd - displayBeforeStart)} +${formatDiffRange(displayAfterStart, displayAfterEnd - displayAfterStart)} @@`
|
|
105
|
+
];
|
|
106
|
+
for (let index = displayBeforeStart; index < changedBeforeStart; index += 1) {
|
|
107
|
+
diffLines.push(` ${beforeLines[index]}`);
|
|
108
|
+
}
|
|
109
|
+
for (let index = changedBeforeStart; index < changedBeforeEnd; index += 1) {
|
|
110
|
+
diffLines.push(`-${beforeLines[index]}`);
|
|
111
|
+
}
|
|
112
|
+
for (let index = changedAfterStart; index < changedAfterEnd; index += 1) {
|
|
113
|
+
diffLines.push(`+${afterLines[index]}`);
|
|
114
|
+
}
|
|
115
|
+
for (let index = changedBeforeEnd; index < displayBeforeEnd; index += 1) {
|
|
116
|
+
diffLines.push(` ${beforeLines[index]}`);
|
|
117
|
+
}
|
|
118
|
+
return diffLines.join("\n");
|
|
119
|
+
};
|
|
120
|
+
const countChangedLines = (beforeMarkdown, afterMarkdown) => {
|
|
121
|
+
const beforeLines = splitLines(beforeMarkdown);
|
|
122
|
+
const afterLines = splitLines(afterMarkdown);
|
|
123
|
+
const { prefix, suffix } = findSharedLineEdges(beforeLines, afterLines);
|
|
124
|
+
const removedLineCount = beforeLines.length - prefix - suffix;
|
|
125
|
+
const addedLineCount = afterLines.length - prefix - suffix;
|
|
126
|
+
return Math.max(removedLineCount, addedLineCount);
|
|
127
|
+
};
|
|
128
|
+
const isMarkdownTokenWhitespace = (character) => /\s/.test(character);
|
|
129
|
+
const countExplicitTagTokens = (value) => {
|
|
130
|
+
let count = 0;
|
|
131
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
132
|
+
if (value[index] !== "[" || value[index + 1] !== "@" && value[index + 1] !== "#") {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
let cursor = index + 2;
|
|
136
|
+
let isValid = cursor < value.length;
|
|
137
|
+
while (cursor < value.length && value[cursor] !== "]") {
|
|
138
|
+
if (value[cursor] === "[" || isMarkdownTokenWhitespace(value[cursor])) {
|
|
139
|
+
isValid = false;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
cursor += 1;
|
|
143
|
+
}
|
|
144
|
+
if (isValid && cursor < value.length && cursor > index + 2) {
|
|
145
|
+
count += 1;
|
|
146
|
+
index = cursor;
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
if (!isValid) {
|
|
150
|
+
index = Math.max(index, cursor - 1);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return count;
|
|
154
|
+
};
|
|
155
|
+
const countMarkdownReferenceTokens = (value) => {
|
|
156
|
+
let count = 0;
|
|
157
|
+
let cursor = 0;
|
|
158
|
+
while (cursor < value.length) {
|
|
159
|
+
const start = value.indexOf("[[", cursor);
|
|
160
|
+
if (start === -1) {
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
const end = value.indexOf("]]", start + 2);
|
|
164
|
+
if (end === -1) {
|
|
165
|
+
break;
|
|
166
|
+
}
|
|
167
|
+
if (end > start + 2) {
|
|
168
|
+
count += 1;
|
|
169
|
+
}
|
|
170
|
+
cursor = end + 2;
|
|
171
|
+
}
|
|
172
|
+
return count;
|
|
173
|
+
};
|
|
174
|
+
const parseVersionTimestamp = (value) => {
|
|
175
|
+
const timestamp = /^\d+$/.test(value) ? Number(value) : Date.parse(value);
|
|
176
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
177
|
+
};
|
|
178
|
+
const noteVersionsMatch = (expectedUpdatedAt, currentUpdatedAt) => {
|
|
179
|
+
const expectedTimestamp = parseVersionTimestamp(expectedUpdatedAt);
|
|
180
|
+
const currentTimestamp = parseVersionTimestamp(currentUpdatedAt);
|
|
181
|
+
return expectedTimestamp !== null && currentTimestamp !== null && expectedTimestamp === currentTimestamp;
|
|
182
|
+
};
|
|
183
|
+
const getPreservationFailure = (beforeMarkdown, afterMarkdown, policy) => {
|
|
184
|
+
const beforeTagCount = countExplicitTagTokens(beforeMarkdown);
|
|
185
|
+
const afterTagCount = countExplicitTagTokens(afterMarkdown);
|
|
186
|
+
if (policy?.preserveTags === true && afterTagCount < beforeTagCount) {
|
|
187
|
+
return {
|
|
188
|
+
status: "failed",
|
|
189
|
+
reason: "TAG_TOKEN_DECREASED",
|
|
190
|
+
message: "The patch would reduce explicit tag tokens."
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
const beforeReferenceCount = countMarkdownReferenceTokens(beforeMarkdown);
|
|
194
|
+
const afterReferenceCount = countMarkdownReferenceTokens(afterMarkdown);
|
|
195
|
+
if (policy?.preserveReferences === true && afterReferenceCount < beforeReferenceCount) {
|
|
196
|
+
return {
|
|
197
|
+
status: "failed",
|
|
198
|
+
reason: "REFERENCE_TOKEN_DECREASED",
|
|
199
|
+
message: "The patch would reduce note reference tokens."
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
return null;
|
|
203
|
+
};
|
|
204
|
+
const collectWarnings = (beforeMarkdown, afterMarkdown, policy) => {
|
|
205
|
+
const warnings = [];
|
|
206
|
+
const beforeTagCount = countExplicitTagTokens(beforeMarkdown);
|
|
207
|
+
const afterTagCount = countExplicitTagTokens(afterMarkdown);
|
|
208
|
+
const shouldWarnTags = policy?.preserveTags === void 0 || policy.preserveTags === "warn";
|
|
209
|
+
if (shouldWarnTags && afterTagCount < beforeTagCount) {
|
|
210
|
+
warnings.push("TAG_TOKEN_COUNT_DECREASED");
|
|
211
|
+
}
|
|
212
|
+
const beforeReferenceCount = countMarkdownReferenceTokens(beforeMarkdown);
|
|
213
|
+
const afterReferenceCount = countMarkdownReferenceTokens(afterMarkdown);
|
|
214
|
+
const shouldWarnReferences = policy?.preserveReferences === void 0 || policy.preserveReferences === "warn";
|
|
215
|
+
if (shouldWarnReferences && afterReferenceCount < beforeReferenceCount) {
|
|
216
|
+
warnings.push("REFERENCE_TOKEN_COUNT_DECREASED");
|
|
217
|
+
}
|
|
218
|
+
return warnings;
|
|
219
|
+
};
|
|
220
|
+
const validateBaseline = (note, expectedUpdatedAt, baseMarkdownSha256) => {
|
|
221
|
+
const beforeMarkdownSha256 = calculateMarkdownSha256(note.markdown);
|
|
222
|
+
if (!expectedUpdatedAt && !baseMarkdownSha256) {
|
|
223
|
+
return {
|
|
224
|
+
status: "failed",
|
|
225
|
+
reason: "MISSING_BASELINE",
|
|
226
|
+
message: "expectedUpdatedAt or baseMarkdownSha256 is required for markdown writes."
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
if (expectedUpdatedAt && !noteVersionsMatch(expectedUpdatedAt, note.updatedAt) || baseMarkdownSha256 && baseMarkdownSha256 !== beforeMarkdownSha256) {
|
|
230
|
+
return {
|
|
231
|
+
status: "failed",
|
|
232
|
+
reason: "BASELINE_MISMATCH",
|
|
233
|
+
message: "The markdown write baseline does not match the current note."
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return null;
|
|
237
|
+
};
|
|
238
|
+
const validateChangeLimits = (beforeMarkdown, afterMarkdown, changedCharCount, policy) => {
|
|
239
|
+
const changedLineCount = countChangedLines(beforeMarkdown, afterMarkdown);
|
|
240
|
+
if (policy?.maxChangedLines !== void 0 && changedLineCount > policy.maxChangedLines || policy?.maxChangedChars !== void 0 && changedCharCount > policy.maxChangedChars) {
|
|
241
|
+
return {
|
|
242
|
+
status: "failed",
|
|
243
|
+
reason: "CHANGE_LIMIT_EXCEEDED",
|
|
244
|
+
message: "The markdown write exceeds the configured change limit."
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
};
|
|
249
|
+
const createChangePlan = ({
|
|
250
|
+
note,
|
|
251
|
+
afterMarkdown,
|
|
252
|
+
changedCharCount,
|
|
253
|
+
policy,
|
|
254
|
+
diffContextLines = DIFF_CONTEXT_LINES,
|
|
255
|
+
match,
|
|
256
|
+
placement
|
|
257
|
+
}) => {
|
|
258
|
+
if (!policy?.allowNoop && afterMarkdown === note.markdown) {
|
|
259
|
+
return {
|
|
260
|
+
status: "failed",
|
|
261
|
+
reason: "NOOP",
|
|
262
|
+
message: "The markdown write would not change the note."
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const preservationFailure = getPreservationFailure(note.markdown, afterMarkdown, policy);
|
|
266
|
+
if (preservationFailure) {
|
|
267
|
+
return preservationFailure;
|
|
268
|
+
}
|
|
269
|
+
const limitFailure = validateChangeLimits(note.markdown, afterMarkdown, changedCharCount, policy);
|
|
270
|
+
if (limitFailure) {
|
|
271
|
+
return limitFailure;
|
|
272
|
+
}
|
|
273
|
+
return {
|
|
274
|
+
status: "dry_run",
|
|
275
|
+
note: {
|
|
276
|
+
id: note.id,
|
|
277
|
+
title: note.title,
|
|
278
|
+
updatedAt: note.updatedAt
|
|
279
|
+
},
|
|
280
|
+
...match ? { match } : {},
|
|
281
|
+
...placement ? { placement } : {},
|
|
282
|
+
proposed: {
|
|
283
|
+
changedLineCount: countChangedLines(note.markdown, afterMarkdown),
|
|
284
|
+
changedCharCount,
|
|
285
|
+
beforeMarkdownSha256: calculateMarkdownSha256(note.markdown),
|
|
286
|
+
afterMarkdownSha256: calculateMarkdownSha256(afterMarkdown),
|
|
287
|
+
diff: createUnifiedMarkdownDiff(note.markdown, afterMarkdown, diffContextLines)
|
|
288
|
+
},
|
|
289
|
+
warnings: collectWarnings(note.markdown, afterMarkdown, policy),
|
|
290
|
+
afterMarkdown
|
|
291
|
+
};
|
|
292
|
+
};
|
|
293
|
+
const matchHasAnchors = (markdown, match, selector) => {
|
|
294
|
+
if (selector.before !== void 0 && !markdown.slice(0, match.start).endsWith(selector.before)) {
|
|
295
|
+
return false;
|
|
296
|
+
}
|
|
297
|
+
if (selector.after !== void 0 && !markdown.slice(match.end).startsWith(selector.after)) {
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
return true;
|
|
301
|
+
};
|
|
302
|
+
const selectExactTextMatch = (markdown, selector) => {
|
|
303
|
+
if (selector.text.length === 0) {
|
|
304
|
+
return {
|
|
305
|
+
status: "failed",
|
|
306
|
+
reason: "EMPTY_TARGET",
|
|
307
|
+
message: "Exact text selector must not be empty."
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
const rawMatches = findExactTextMatches(markdown, selector.text);
|
|
311
|
+
if (rawMatches.length === 0) {
|
|
312
|
+
return {
|
|
313
|
+
status: "failed",
|
|
314
|
+
reason: "TARGET_NOT_FOUND",
|
|
315
|
+
message: "No exact text match was found in the current note."
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
const anchoredMatches = rawMatches.filter((match2) => matchHasAnchors(markdown, match2, selector));
|
|
319
|
+
if (anchoredMatches.length === 0) {
|
|
320
|
+
return {
|
|
321
|
+
status: "failed",
|
|
322
|
+
reason: "ANCHOR_MISMATCH",
|
|
323
|
+
message: "Exact text matches were found, but before/after anchors did not match."
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
if (anchoredMatches.length > 1) {
|
|
327
|
+
return {
|
|
328
|
+
status: "needs_disambiguation",
|
|
329
|
+
reason: "TARGET_AMBIGUOUS",
|
|
330
|
+
matches: anchoredMatches.map((match2) => toPatchMatch(markdown, match2, rawMatches.length))
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
const match = anchoredMatches[0];
|
|
334
|
+
return {
|
|
335
|
+
match,
|
|
336
|
+
patchMatch: toPatchMatch(markdown, match, rawMatches.length)
|
|
337
|
+
};
|
|
338
|
+
};
|
|
339
|
+
const selectMatchCandidate = (markdown, selector) => {
|
|
340
|
+
if (selector.text.length === 0) {
|
|
341
|
+
return {
|
|
342
|
+
status: "failed",
|
|
343
|
+
reason: "EMPTY_TARGET",
|
|
344
|
+
message: "Match candidate selector text must not be empty."
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
const rawMatches = findExactTextMatches(markdown, selector.text);
|
|
348
|
+
if (rawMatches.length === 0) {
|
|
349
|
+
return {
|
|
350
|
+
status: "failed",
|
|
351
|
+
reason: "TARGET_NOT_FOUND",
|
|
352
|
+
message: "No match candidate text was found in the current note."
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
const match = rawMatches.find((candidate) => candidate.matchIndex === selector.matchIndex);
|
|
356
|
+
if (!match) {
|
|
357
|
+
return {
|
|
358
|
+
status: "failed",
|
|
359
|
+
reason: "CANDIDATE_MISMATCH",
|
|
360
|
+
message: "The selected match index does not exist in the current note."
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
const patchMatch = toPatchMatch(markdown, match, rawMatches.length);
|
|
364
|
+
const positionMatches = selector.positionHint === void 0 || patchMatch.positionHint === selector.positionHint;
|
|
365
|
+
if (patchMatch.lineStart !== selector.lineStart || patchMatch.matchSha256 !== selector.matchSha256 || patchMatch.surroundingHash !== selector.surroundingHash || !positionMatches) {
|
|
366
|
+
return {
|
|
367
|
+
status: "failed",
|
|
368
|
+
reason: "CANDIDATE_MISMATCH",
|
|
369
|
+
message: "The selected match candidate no longer matches the current note context."
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
return {
|
|
373
|
+
match,
|
|
374
|
+
patchMatch
|
|
375
|
+
};
|
|
376
|
+
};
|
|
377
|
+
const applyPatchOperation = (markdown, match, operation) => {
|
|
378
|
+
if (operation.type === "replace") {
|
|
379
|
+
return {
|
|
380
|
+
markdown: `${markdown.slice(0, match.start)}${operation.replacement}${markdown.slice(match.end)}`,
|
|
381
|
+
changedCharCount: match.text.length + operation.replacement.length
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
if (operation.type === "insert_before") {
|
|
385
|
+
return {
|
|
386
|
+
markdown: `${markdown.slice(0, match.start)}${operation.insertion}${markdown.slice(match.start)}`,
|
|
387
|
+
changedCharCount: operation.insertion.length
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
markdown: `${markdown.slice(0, match.end)}${operation.insertion}${markdown.slice(match.end)}`,
|
|
392
|
+
changedCharCount: operation.insertion.length
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
const buildMarkdownPatchPlan = (input) => {
|
|
396
|
+
const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
|
|
397
|
+
if (baselineFailure) {
|
|
398
|
+
return baselineFailure;
|
|
399
|
+
}
|
|
400
|
+
const selected = input.selector.type === "exact_text" ? selectExactTextMatch(input.note.markdown, input.selector) : selectMatchCandidate(input.note.markdown, input.selector);
|
|
401
|
+
if ("status" in selected) {
|
|
402
|
+
return selected;
|
|
403
|
+
}
|
|
404
|
+
const patched = applyPatchOperation(input.note.markdown, selected.match, input.operation);
|
|
405
|
+
const plan = createChangePlan({
|
|
406
|
+
note: input.note,
|
|
407
|
+
afterMarkdown: patched.markdown,
|
|
408
|
+
changedCharCount: patched.changedCharCount,
|
|
409
|
+
policy: input.policy,
|
|
410
|
+
match: {
|
|
411
|
+
count: 1,
|
|
412
|
+
lineStart: selected.patchMatch.lineStart,
|
|
413
|
+
lineEnd: selected.patchMatch.lineEnd,
|
|
414
|
+
selectorType: input.selector.type,
|
|
415
|
+
matchedTextSha256: selected.patchMatch.matchSha256,
|
|
416
|
+
surroundingHash: selected.patchMatch.surroundingHash
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return plan;
|
|
420
|
+
};
|
|
421
|
+
const parseHeadingLine = (line) => {
|
|
422
|
+
const match = /^(#{1,6})\s+(.+?)\s*#*\s*$/.exec(line);
|
|
423
|
+
if (!match) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
return {
|
|
427
|
+
level: match[1].length,
|
|
428
|
+
heading: match[2].trim()
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
const getLineStartOffsets = (lines) => {
|
|
432
|
+
const offsets = [];
|
|
433
|
+
let offset = 0;
|
|
434
|
+
for (const line of lines) {
|
|
435
|
+
offsets.push(offset);
|
|
436
|
+
offset += line.length + 1;
|
|
437
|
+
}
|
|
438
|
+
return offsets;
|
|
439
|
+
};
|
|
440
|
+
const findHeadingMatches = (markdown, placement) => {
|
|
441
|
+
const lines = splitLines(markdown);
|
|
442
|
+
const offsets = getLineStartOffsets(lines);
|
|
443
|
+
const headingLines = lines.map((line, index) => {
|
|
444
|
+
const heading = parseHeadingLine(line);
|
|
445
|
+
if (!heading) {
|
|
446
|
+
return null;
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
...heading,
|
|
450
|
+
lineIndex: index,
|
|
451
|
+
start: offsets[index]
|
|
452
|
+
};
|
|
453
|
+
}).filter((heading) => heading !== null);
|
|
454
|
+
const matches = headingLines.filter((heading) => {
|
|
455
|
+
return heading.heading === placement.heading && (placement.level === void 0 || heading.level === placement.level);
|
|
456
|
+
});
|
|
457
|
+
return matches.map((match) => {
|
|
458
|
+
const nextHeading = headingLines.find(
|
|
459
|
+
(heading) => heading.lineIndex > match.lineIndex && heading.level <= match.level
|
|
460
|
+
);
|
|
461
|
+
return {
|
|
462
|
+
...match,
|
|
463
|
+
nextSectionStart: nextHeading?.start ?? markdown.length
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
};
|
|
467
|
+
const appendWithSeparator = (prefix, insertion, separator) => {
|
|
468
|
+
if (prefix.length === 0) {
|
|
469
|
+
return insertion;
|
|
470
|
+
}
|
|
471
|
+
return `${prefix}${prefix.endsWith(separator) ? "" : separator}${insertion}`;
|
|
472
|
+
};
|
|
473
|
+
const applyAppendPlacement = (markdown, insertion, placement, separator) => {
|
|
474
|
+
if (insertion.length === 0) {
|
|
475
|
+
return {
|
|
476
|
+
status: "failed",
|
|
477
|
+
reason: "EMPTY_INSERTION",
|
|
478
|
+
message: "Append insertion must not be empty."
|
|
479
|
+
};
|
|
480
|
+
}
|
|
481
|
+
if (placement.type === "end") {
|
|
482
|
+
return {
|
|
483
|
+
markdown: appendWithSeparator(markdown, insertion, separator),
|
|
484
|
+
placement
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
if (placement.level !== void 0 && (!Number.isInteger(placement.level) || placement.level < 1 || placement.level > 6)) {
|
|
488
|
+
return {
|
|
489
|
+
status: "failed",
|
|
490
|
+
reason: "INVALID_HEADING_LEVEL",
|
|
491
|
+
message: "Heading level must be an integer from 1 to 6."
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
const headingMatches = findHeadingMatches(markdown, placement);
|
|
495
|
+
if (headingMatches.length === 0) {
|
|
496
|
+
return {
|
|
497
|
+
status: "failed",
|
|
498
|
+
reason: "HEADING_NOT_FOUND",
|
|
499
|
+
message: "The requested heading was not found."
|
|
500
|
+
};
|
|
501
|
+
}
|
|
502
|
+
if (headingMatches.length > 1) {
|
|
503
|
+
return {
|
|
504
|
+
status: "failed",
|
|
505
|
+
reason: "HEADING_AMBIGUOUS",
|
|
506
|
+
message: "The requested heading appears more than once."
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
const [headingMatch] = headingMatches;
|
|
510
|
+
const prefix = markdown.slice(0, headingMatch.nextSectionStart);
|
|
511
|
+
const suffix = markdown.slice(headingMatch.nextSectionStart);
|
|
512
|
+
const prefixWithInsertion = appendWithSeparator(prefix, insertion, separator);
|
|
513
|
+
const nextMarkdown = suffix.length === 0 ? prefixWithInsertion : `${prefixWithInsertion}${prefixWithInsertion.endsWith(separator) ? "" : separator}${suffix}`;
|
|
514
|
+
return {
|
|
515
|
+
markdown: nextMarkdown,
|
|
516
|
+
placement
|
|
517
|
+
};
|
|
518
|
+
};
|
|
519
|
+
const buildMarkdownAppendPlan = (input) => {
|
|
520
|
+
const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
|
|
521
|
+
if (baselineFailure) {
|
|
522
|
+
return baselineFailure;
|
|
523
|
+
}
|
|
524
|
+
const placement = input.placement ?? { type: "end" };
|
|
525
|
+
const separator = input.separator ?? "\n\n";
|
|
526
|
+
const appended = applyAppendPlacement(input.note.markdown, input.insertion, placement, separator);
|
|
527
|
+
if ("status" in appended) {
|
|
528
|
+
return appended;
|
|
529
|
+
}
|
|
530
|
+
return createChangePlan({
|
|
531
|
+
note: input.note,
|
|
532
|
+
afterMarkdown: appended.markdown,
|
|
533
|
+
changedCharCount: input.insertion.length,
|
|
534
|
+
policy: input.policy,
|
|
535
|
+
placement: appended.placement
|
|
536
|
+
});
|
|
537
|
+
};
|
|
538
|
+
const buildMarkdownReplacePlan = (input) => {
|
|
539
|
+
const baselineFailure = validateBaseline(input.note, input.expectedUpdatedAt, input.baseMarkdownSha256);
|
|
540
|
+
if (baselineFailure) {
|
|
541
|
+
return baselineFailure;
|
|
542
|
+
}
|
|
543
|
+
if (input.replacement.length === 0) {
|
|
544
|
+
return {
|
|
545
|
+
status: "failed",
|
|
546
|
+
reason: "EMPTY_REPLACEMENT",
|
|
547
|
+
message: "Replacement markdown must not be empty."
|
|
548
|
+
};
|
|
549
|
+
}
|
|
550
|
+
return createChangePlan({
|
|
551
|
+
note: input.note,
|
|
552
|
+
afterMarkdown: input.replacement,
|
|
553
|
+
changedCharCount: input.note.markdown.length + input.replacement.length,
|
|
554
|
+
policy: input.policy,
|
|
555
|
+
diffContextLines: null
|
|
556
|
+
});
|
|
557
|
+
};
|
|
558
|
+
const toPreviewResult = (plan) => {
|
|
559
|
+
return {
|
|
560
|
+
status: "dry_run",
|
|
561
|
+
note: plan.note,
|
|
562
|
+
...plan.match ? { match: plan.match } : {},
|
|
563
|
+
...plan.placement ? { placement: plan.placement } : {},
|
|
564
|
+
proposed: plan.proposed,
|
|
565
|
+
warnings: plan.warnings
|
|
566
|
+
};
|
|
567
|
+
};
|
|
568
|
+
const previewMarkdownPatch = (input) => {
|
|
569
|
+
const plan = buildMarkdownPatchPlan(input);
|
|
570
|
+
if (plan.status !== "dry_run") {
|
|
571
|
+
return plan;
|
|
572
|
+
}
|
|
573
|
+
return toPreviewResult(plan);
|
|
574
|
+
};
|
|
575
|
+
const previewMarkdownAppend = (input) => {
|
|
576
|
+
const plan = buildMarkdownAppendPlan(input);
|
|
577
|
+
if (plan.status !== "dry_run") {
|
|
578
|
+
return plan;
|
|
579
|
+
}
|
|
580
|
+
return toPreviewResult(plan);
|
|
581
|
+
};
|
|
582
|
+
const previewMarkdownReplace = (input) => {
|
|
583
|
+
const plan = buildMarkdownReplacePlan(input);
|
|
584
|
+
if (plan.status !== "dry_run") {
|
|
585
|
+
return plan;
|
|
586
|
+
}
|
|
587
|
+
return toPreviewResult(plan);
|
|
588
|
+
};
|
|
589
|
+
export {
|
|
590
|
+
buildMarkdownAppendPlan,
|
|
591
|
+
buildMarkdownPatchPlan,
|
|
592
|
+
buildMarkdownReplacePlan,
|
|
593
|
+
calculateMarkdownSha256,
|
|
594
|
+
countExplicitTagTokens,
|
|
595
|
+
countMarkdownReferenceTokens,
|
|
596
|
+
createUnifiedMarkdownDiff,
|
|
597
|
+
previewMarkdownAppend,
|
|
598
|
+
previewMarkdownPatch,
|
|
599
|
+
previewMarkdownReplace
|
|
600
|
+
};
|
|
601
|
+
//# sourceMappingURL=markdown-patch.js.map
|