@openvulcan/dsh-vulcancode 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +86 -2
- package/assets/openvulcan-icon.svg +33 -0
- package/cordis.patch.yml +17 -0
- package/node_modules/@openvulcan/dsh-decision-guard/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-decision-guard/README.md +14 -0
- package/node_modules/@openvulcan/dsh-decision-guard/assets/decision-guard-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.d.ts +40 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.js +150 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/client.js +2193 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.d.ts +90 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.js +108 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.d.ts +129 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/index.js +572 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.js +77 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.d.ts +158 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.js +340 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.d.ts +133 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/records.js +186 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/review.js +202 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.d.ts +62 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.js +155 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/dsh-decision-guard/lib/version.js +4 -0
- package/node_modules/@openvulcan/dsh-decision-guard/package.json +68 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/assets/account-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/client.js +565 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.js +194 -0
- package/node_modules/@openvulcan/dsh-vulcan-account/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/answers-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-interactive.md +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-standard.md +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.d.ts +258 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.js +698 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/client.js +671 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.js +70 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.d.ts +19 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.js +52 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.d.ts +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.js +9 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.d.ts +22 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.js +39 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.d.ts +20 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.js +17 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.d.ts +66 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.js +118 -0
- package/node_modules/@openvulcan/dsh-vulcan-answers/package.json +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/assets/luaskills-icon.svg +1 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/client.js +940 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.d.ts +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.js +51 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.d.ts +55 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.js +149 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.d.ts +49 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.js +116 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.js +276 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.js +2 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.d.ts +111 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.js +281 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.d.ts +26 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.js +87 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.d.ts +79 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.js +165 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.js +61 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.d.ts +59 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.js +107 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.d.ts +10 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.js +140 -0
- package/node_modules/@openvulcan/dsh-vulcan-luaskills/package.json +57 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/markdown-memory-guide.md +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-curator/SKILL.md +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/client.js +1090 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.js +240 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.d.ts +30 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.js +46 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.d.ts +23 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.js +25 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.d.ts +108 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.js +373 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.js +31 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.d.ts +196 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.js +204 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.d.ts +92 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.js +472 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.d.ts +126 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.js +553 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.d.ts +56 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.js +182 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.d.ts +13 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.js +24 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.d.ts +65 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.js +227 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.js +171 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.d.ts +72 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.js +250 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.d.ts +6 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.js +12 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.d.ts +8 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.js +38 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.d.ts +132 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.js +235 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.d.ts +14 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.js +27 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.d.ts +29 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.js +62 -0
- package/node_modules/@openvulcan/dsh-vulcan-memory/package.json +63 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/LICENSE +21 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/assets/style-icon.png +0 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/client.js +1061 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.d.ts +34 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.js +42 -0
- package/node_modules/@openvulcan/dsh-vulcan-style/package.json +51 -0
- package/node_modules/@openvulcan/openvulcan-account/LICENSE +21 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.d.ts +140 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/account.js +347 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.d.ts +1 -0
- package/node_modules/@openvulcan/openvulcan-account/lib/index.js +1 -0
- package/node_modules/@openvulcan/openvulcan-account/package.json +29 -0
- package/node_modules/@openvulcan/vulcan-decision/LICENSE +21 -0
- package/node_modules/@openvulcan/vulcan-decision/README.md +23 -0
- package/node_modules/@openvulcan/vulcan-decision/bin/vulcan-decision.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.d.ts +5 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cli.js +267 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.d.ts +211 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.js +594 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.d.ts +36 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/download.js +99 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.d.ts +37 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/engine.js +39 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.d.ts +105 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/hardware.js +207 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.d.ts +26 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/home.js +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.d.ts +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/index.js +17 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.d.ts +54 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.js +157 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.d.ts +126 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/models.js +261 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.d.ts +102 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.js +195 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.d.ts +61 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.js +123 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.d.ts +74 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/protocol.js +31 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.d.ts +15 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/render.js +33 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.d.ts +2 -0
- package/node_modules/@openvulcan/vulcan-decision/lib/version.js +4 -0
- package/node_modules/@openvulcan/vulcan-decision/package.json +40 -0
- package/node_modules/@openvulcan/vulcan-decision/python/vulcan_decision_worker.py +363 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cpu.json +627 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cuda.json +423 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cpu.json +1047 -0
- package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cu128.json +647 -0
- package/package.json +53 -4
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
/** Write operations applied atomically across the scope files (`operations.rs`). */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { MAX_FILE_BYTES, MAX_NODE_BODY_BYTES, MAX_NODE_LEVEL, MAX_OPERATIONS_PER_WRITE, MAX_TITLE_CHARS, byteLength } from './constants.js';
|
|
4
|
+
import { MemoryDocument, MemoryNode, isMetaValue, lines, nodeHeadingLevels, storedBody, titleRoundTrips, viewTitle } from './document.js';
|
|
5
|
+
import { MemoryError } from './errors.js';
|
|
6
|
+
import { REGISTRY_FILE, isRecord, randomHex } from './registry.js';
|
|
7
|
+
import { BASE_SECTION_KEY, SCOPES, isScope, presetSections } from './scope.js';
|
|
8
|
+
import { baseUsage, nearBudget, overBudget, renderUsageLine } from './snapshot.js';
|
|
9
|
+
import { boundProject, commitFiles, readOptional } from './store.js';
|
|
10
|
+
import { nowUtcMinute } from './timestamp.js';
|
|
11
|
+
import { isCjkCharacter } from './tokens.js';
|
|
12
|
+
/** The receipt as model-facing English text. */
|
|
13
|
+
export function renderWriteReceipt(receipt) {
|
|
14
|
+
const output = [`Applied ${receipt.operations.length} memory operation(s).`];
|
|
15
|
+
if (receipt.createdProject !== undefined)
|
|
16
|
+
output.push(`Created project memory \`${receipt.createdProject}\` for this project.`);
|
|
17
|
+
for (const operation of receipt.operations) {
|
|
18
|
+
let line = `- ${operation.action} ${operation.id} [${operation.scope}] ${operation.path.join(' › ')}`;
|
|
19
|
+
if (operation.revision !== '')
|
|
20
|
+
line += ` (revision ${operation.revision})`;
|
|
21
|
+
if (operation.newSection)
|
|
22
|
+
line += ' · new section';
|
|
23
|
+
if (operation.deletedChildren > 0)
|
|
24
|
+
line += ` · ${operation.deletedChildren} child node(s) deleted`;
|
|
25
|
+
output.push(line);
|
|
26
|
+
if (operation.duplicates.length > 0) {
|
|
27
|
+
const hints = operation.duplicates.map(hint => `${hint.id} [${hint.scope}] ${hint.path.join(' › ')}`).join('; ');
|
|
28
|
+
output.push(` possible duplicates (merge if they cover the same fact): ${hints}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
for (const usage of receipt.usage) {
|
|
32
|
+
output.push(renderUsageLine(usage));
|
|
33
|
+
if (overBudget(usage))
|
|
34
|
+
output.push(' Base is over budget: move less essential nodes from the base section to a catalog section.');
|
|
35
|
+
else if (nearBudget(usage))
|
|
36
|
+
output.push(' Base is nearly full: keep only rules that would cause mistakes if unknown.');
|
|
37
|
+
}
|
|
38
|
+
return output.join('\n');
|
|
39
|
+
}
|
|
40
|
+
const OPERATION_FIELDS = {
|
|
41
|
+
create: ['action', 'title', 'body', 'scope', 'parentId', 'section', 'newSection'],
|
|
42
|
+
replace: ['action', 'id', 'expectedRevision', 'body'],
|
|
43
|
+
append: ['action', 'id', 'expectedRevision', 'text'],
|
|
44
|
+
rename: ['action', 'id', 'expectedRevision', 'title'],
|
|
45
|
+
move: ['action', 'id', 'expectedRevision', 'scope', 'parentId', 'section', 'newSection'],
|
|
46
|
+
delete: ['action', 'id', 'expectedRevision', 'withChildren'],
|
|
47
|
+
};
|
|
48
|
+
/**
|
|
49
|
+
* Parse one operation the way VulcanCode's serde enum does: tagged by `action`, camelCase fields,
|
|
50
|
+
* unknown fields refused, optional fields may be absent or null. Messages describe the problem
|
|
51
|
+
* without the `Invalid arguments:` prefix the tool layer adds.
|
|
52
|
+
*/
|
|
53
|
+
export function parseWriteOperation(value) {
|
|
54
|
+
if (!isRecord(value))
|
|
55
|
+
throw MemoryError.invalid('each operation must be an object');
|
|
56
|
+
const action = value['action'];
|
|
57
|
+
if (typeof action !== 'string' || !Object.hasOwn(OPERATION_FIELDS, action)) {
|
|
58
|
+
throw MemoryError.invalid(`unknown variant ${JSON.stringify(action)}, expected one of create, replace, append, rename, move, delete`);
|
|
59
|
+
}
|
|
60
|
+
const kind = action;
|
|
61
|
+
const unknown = Object.keys(value).find(key => !OPERATION_FIELDS[kind].includes(key));
|
|
62
|
+
if (unknown !== undefined)
|
|
63
|
+
throw MemoryError.invalid(`unknown field \`${unknown}\` for action \`${kind}\``);
|
|
64
|
+
const required = (field) => {
|
|
65
|
+
const text = value[field];
|
|
66
|
+
if (typeof text !== 'string')
|
|
67
|
+
throw MemoryError.invalid(`\`${kind}\` needs the string field \`${field}\``);
|
|
68
|
+
return text;
|
|
69
|
+
};
|
|
70
|
+
const optional = (field) => {
|
|
71
|
+
const text = value[field];
|
|
72
|
+
if (text === undefined || text === null)
|
|
73
|
+
return undefined;
|
|
74
|
+
if (typeof text !== 'string')
|
|
75
|
+
throw MemoryError.invalid(`\`${field}\` must be a string`);
|
|
76
|
+
return text;
|
|
77
|
+
};
|
|
78
|
+
const flag = (field) => {
|
|
79
|
+
const bit = value[field];
|
|
80
|
+
if (bit === undefined)
|
|
81
|
+
return false;
|
|
82
|
+
if (typeof bit !== 'boolean')
|
|
83
|
+
throw MemoryError.invalid(`\`${field}\` must be a boolean`);
|
|
84
|
+
return bit;
|
|
85
|
+
};
|
|
86
|
+
const placement = () => {
|
|
87
|
+
const scope = value['scope'];
|
|
88
|
+
if (scope !== undefined && scope !== null && !isScope(scope))
|
|
89
|
+
throw MemoryError.invalid('`scope` must be user, space or project');
|
|
90
|
+
const parentId = optional('parentId');
|
|
91
|
+
const section = optional('section');
|
|
92
|
+
return {
|
|
93
|
+
...isScope(scope) ? { scope } : {},
|
|
94
|
+
...parentId === undefined ? {} : { parentId },
|
|
95
|
+
...section === undefined ? {} : { section },
|
|
96
|
+
newSection: flag('newSection'),
|
|
97
|
+
};
|
|
98
|
+
};
|
|
99
|
+
switch (kind) {
|
|
100
|
+
case 'create': return { action: kind, title: required('title'), body: optional('body') ?? '', ...placement() };
|
|
101
|
+
case 'replace': return { action: kind, id: required('id'), expectedRevision: required('expectedRevision'), body: required('body') };
|
|
102
|
+
case 'append': return { action: kind, id: required('id'), expectedRevision: required('expectedRevision'), text: required('text') };
|
|
103
|
+
case 'rename': return { action: kind, id: required('id'), expectedRevision: required('expectedRevision'), title: required('title') };
|
|
104
|
+
case 'move': return { action: kind, id: required('id'), expectedRevision: required('expectedRevision'), ...placement() };
|
|
105
|
+
case 'delete': return { action: kind, id: required('id'), expectedRevision: required('expectedRevision'), withChildren: flag('withChildren') };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** Apply operations in order, all or nothing; a failed commit restores originals or reports an incomplete rollback. */
|
|
109
|
+
export function writeMemory(store, context, author, budget, operations) {
|
|
110
|
+
if (operations.length === 0 || operations.length > MAX_OPERATIONS_PER_WRITE) {
|
|
111
|
+
throw MemoryError.invalid(`Provide 1 to ${MAX_OPERATIONS_PER_WRITE} operations.`);
|
|
112
|
+
}
|
|
113
|
+
const session = new WriteSession(store, context, author);
|
|
114
|
+
const receipts = operations.map((operation, index) => {
|
|
115
|
+
try {
|
|
116
|
+
return session.apply(operation);
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw error instanceof MemoryError ? MemoryError.operation(index + 1, error) : error;
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
return session.finish(receipts, budget);
|
|
123
|
+
}
|
|
124
|
+
/** Mutable state of one write call. */
|
|
125
|
+
class WriteSession {
|
|
126
|
+
store;
|
|
127
|
+
context;
|
|
128
|
+
author;
|
|
129
|
+
registry;
|
|
130
|
+
projectId;
|
|
131
|
+
createdProject;
|
|
132
|
+
scopes = new Map();
|
|
133
|
+
session;
|
|
134
|
+
/** Timestamp shared by every change of this call. */
|
|
135
|
+
now = nowUtcMinute();
|
|
136
|
+
constructor(store, context, author) {
|
|
137
|
+
this.store = store;
|
|
138
|
+
this.context = context;
|
|
139
|
+
this.author = author;
|
|
140
|
+
this.registry = store.registry();
|
|
141
|
+
this.projectId = boundProject(context, this.registry)?.id;
|
|
142
|
+
this.session = author.session !== undefined && isMetaValue(author.session) ? author.session : undefined;
|
|
143
|
+
}
|
|
144
|
+
/** Load a scope on first use, seeding preset sections into a missing or blank file. */
|
|
145
|
+
scope(scope) {
|
|
146
|
+
const loaded = this.scopes.get(scope);
|
|
147
|
+
if (loaded !== undefined)
|
|
148
|
+
return loaded;
|
|
149
|
+
const file = this.store.scopePath(scope, scope === 'project' ? this.ensureProject() : undefined);
|
|
150
|
+
const original = readOptional(file);
|
|
151
|
+
const blank = original === undefined || original.trim() === '';
|
|
152
|
+
const created = {
|
|
153
|
+
path: file,
|
|
154
|
+
original,
|
|
155
|
+
document: blank ? presetDocument(scope, this.now) : MemoryDocument.parse(original),
|
|
156
|
+
changed: blank,
|
|
157
|
+
};
|
|
158
|
+
this.scopes.set(scope, created);
|
|
159
|
+
return created;
|
|
160
|
+
}
|
|
161
|
+
/** The bound memory project, creating and binding one for the host project if needed. */
|
|
162
|
+
ensureProject() {
|
|
163
|
+
if (this.projectId !== undefined) {
|
|
164
|
+
// Users may still edit a project whose memory is off; only AI writes are refused.
|
|
165
|
+
if (this.author.source === 'ai' && this.registry.project(this.projectId)?.memoryEnabled === false) {
|
|
166
|
+
throw new MemoryError('project_unavailable', 'Memory is turned off for this project.');
|
|
167
|
+
}
|
|
168
|
+
return this.projectId;
|
|
169
|
+
}
|
|
170
|
+
const { hostProjectId, rootPath } = this.context;
|
|
171
|
+
if (hostProjectId === undefined || rootPath === undefined) {
|
|
172
|
+
throw new MemoryError('project_unavailable', 'This conversation has no project, so project memory is unavailable.');
|
|
173
|
+
}
|
|
174
|
+
const id = this.registry.createForHost(hostProjectId, rootPath, undefined, this.now);
|
|
175
|
+
this.projectId = id;
|
|
176
|
+
this.createdProject = id;
|
|
177
|
+
return id;
|
|
178
|
+
}
|
|
179
|
+
/** Views of every scope that is loaded or can be loaded without creating a project. */
|
|
180
|
+
allViews() {
|
|
181
|
+
const scopes = this.projectId === undefined ? ['user', 'space'] : ['user', 'space', 'project'];
|
|
182
|
+
return scopes.flatMap(scope => this.scope(scope).document.views(scope));
|
|
183
|
+
}
|
|
184
|
+
/** Find a node by id across scopes and check its revision. */
|
|
185
|
+
locate(id, expectedRevision) {
|
|
186
|
+
const view = this.allViews().find(candidate => candidate.id === id);
|
|
187
|
+
if (view === undefined)
|
|
188
|
+
throw MemoryError.notFound(id);
|
|
189
|
+
if (view.revision !== expectedRevision.trim())
|
|
190
|
+
throw MemoryError.revisionMismatch(id, expectedRevision, view.revision);
|
|
191
|
+
return view;
|
|
192
|
+
}
|
|
193
|
+
apply(operation) {
|
|
194
|
+
switch (operation.action) {
|
|
195
|
+
case 'create':
|
|
196
|
+
return this.create(operation.title, operation.body, operation);
|
|
197
|
+
case 'replace': {
|
|
198
|
+
const view = this.locate(operation.id, operation.expectedRevision);
|
|
199
|
+
validateBody(operation.body);
|
|
200
|
+
return this.updateNode(view, 'replace', (node, eol) => node.setBody(storedBody(operation.body, eol)));
|
|
201
|
+
}
|
|
202
|
+
case 'append': {
|
|
203
|
+
const view = this.locate(operation.id, operation.expectedRevision);
|
|
204
|
+
const text = operation.text.trim();
|
|
205
|
+
if (text === '')
|
|
206
|
+
throw MemoryError.invalid('Append text must not be empty.');
|
|
207
|
+
const combined = view.body === '' ? text : `${view.body}\n\n${text}`;
|
|
208
|
+
validateBody(combined);
|
|
209
|
+
return this.updateNode(view, 'append', (node, eol) => node.setBody(storedBody(combined, eol)));
|
|
210
|
+
}
|
|
211
|
+
case 'rename': {
|
|
212
|
+
const view = this.locate(operation.id, operation.expectedRevision);
|
|
213
|
+
const title = validateTitle(operation.title, view.level);
|
|
214
|
+
return this.updateNode(view, 'rename', node => node.setTitle(title));
|
|
215
|
+
}
|
|
216
|
+
case 'move':
|
|
217
|
+
return this.moveNode(operation.id, operation.expectedRevision, operation);
|
|
218
|
+
case 'delete':
|
|
219
|
+
return this.delete(operation.id, operation.expectedRevision, operation.withChildren);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
create(title, body, placement) {
|
|
223
|
+
const scope = placement.scope;
|
|
224
|
+
if (scope === undefined)
|
|
225
|
+
throw MemoryError.invalid('create requires `scope`.');
|
|
226
|
+
validateBody(body);
|
|
227
|
+
const { parent, level, sectionKey } = this.resolvePlacement(scope, placement, undefined);
|
|
228
|
+
const validTitle = validateTitle(title, level);
|
|
229
|
+
const duplicates = this.duplicates(scope, validTitle, body, undefined);
|
|
230
|
+
const id = this.freshId();
|
|
231
|
+
const meta = {
|
|
232
|
+
id,
|
|
233
|
+
...sectionKey === undefined ? {} : { section: sectionKey },
|
|
234
|
+
created: this.now,
|
|
235
|
+
updated: this.now,
|
|
236
|
+
...this.session === undefined ? {} : { session: this.session },
|
|
237
|
+
source: this.author.source,
|
|
238
|
+
};
|
|
239
|
+
const loaded = this.scope(scope);
|
|
240
|
+
loaded.document.append(parent, new MemoryNode(level, validTitle, meta, storedBody(body, loaded.document.eol)));
|
|
241
|
+
loaded.changed = true;
|
|
242
|
+
const view = this.viewById(scope, id);
|
|
243
|
+
return { action: 'create', id, scope, path: view.path, revision: view.revision, newSection: sectionKey !== undefined, deletedChildren: 0, duplicates };
|
|
244
|
+
}
|
|
245
|
+
moveNode(id, expectedRevision, placement) {
|
|
246
|
+
const view = this.locate(id, expectedRevision);
|
|
247
|
+
guardBaseSection(view, 'moved');
|
|
248
|
+
const targetScope = placement.scope ?? view.scope;
|
|
249
|
+
const source = this.scope(view.scope);
|
|
250
|
+
const node = source.document.remove(view.locator);
|
|
251
|
+
if (node === undefined)
|
|
252
|
+
throw MemoryError.notFound(id);
|
|
253
|
+
source.changed = true;
|
|
254
|
+
const { parent, level, sectionKey } = this.resolvePlacement(targetScope, placement, id);
|
|
255
|
+
const deepest = node.relevel(level);
|
|
256
|
+
if (deepest !== undefined) {
|
|
257
|
+
throw MemoryError.invalid(`Moving there would put nodes at heading level ${deepest}; memory nodes stop at H${MAX_NODE_LEVEL}.`);
|
|
258
|
+
}
|
|
259
|
+
const touched = this.touchedMeta(node.meta);
|
|
260
|
+
const { section: _previous, ...rest } = touched;
|
|
261
|
+
const meta = sectionKey === undefined ? rest : { ...rest, section: sectionKey };
|
|
262
|
+
node.setMeta(meta);
|
|
263
|
+
const target = this.scope(targetScope);
|
|
264
|
+
target.document.append(parent, node);
|
|
265
|
+
target.changed = true;
|
|
266
|
+
const moved = this.viewById(targetScope, meta.id);
|
|
267
|
+
return { action: 'move', id: meta.id, scope: targetScope, path: moved.path, revision: moved.revision, newSection: sectionKey !== undefined, deletedChildren: 0, duplicates: [] };
|
|
268
|
+
}
|
|
269
|
+
delete(id, expectedRevision, withChildren) {
|
|
270
|
+
const view = this.locate(id, expectedRevision);
|
|
271
|
+
guardBaseSection(view, 'deleted');
|
|
272
|
+
if (view.descendants > 0 && !withChildren) {
|
|
273
|
+
throw MemoryError.invalid(`Node \`${id}\` has ${view.descendants} child node(s); set withChildren to delete them too, or move them first.`);
|
|
274
|
+
}
|
|
275
|
+
const loaded = this.scope(view.scope);
|
|
276
|
+
if (loaded.document.remove(view.locator) === undefined)
|
|
277
|
+
throw MemoryError.notFound(id);
|
|
278
|
+
loaded.changed = true;
|
|
279
|
+
return { action: 'delete', id: view.id, scope: view.scope, path: view.path, revision: '', newSection: false, deletedChildren: view.descendants, duplicates: [] };
|
|
280
|
+
}
|
|
281
|
+
/** Apply an in-place change and refresh the node's metadata. */
|
|
282
|
+
updateNode(view, action, change) {
|
|
283
|
+
const meta = this.touchedMeta(view.meta);
|
|
284
|
+
const loaded = this.scope(view.scope);
|
|
285
|
+
const node = loaded.document.node(view.locator);
|
|
286
|
+
if (node === undefined)
|
|
287
|
+
throw MemoryError.notFound(view.id);
|
|
288
|
+
change(node, loaded.document.eol);
|
|
289
|
+
node.setMeta(meta);
|
|
290
|
+
loaded.changed = true;
|
|
291
|
+
const updated = this.viewById(view.scope, meta.id);
|
|
292
|
+
const duplicates = action === 'rename' ? this.duplicates(view.scope, viewTitle(updated), updated.body, meta.id) : [];
|
|
293
|
+
return { action, id: meta.id, scope: view.scope, path: updated.path, revision: updated.revision, newSection: false, deletedChildren: 0, duplicates };
|
|
294
|
+
}
|
|
295
|
+
/** Metadata for a written node: existing identity with a new update time, or a new identity. */
|
|
296
|
+
touchedMeta(existing) {
|
|
297
|
+
if (existing !== undefined) {
|
|
298
|
+
const session = this.session ?? existing.session;
|
|
299
|
+
const { session: _previous, ...rest } = existing;
|
|
300
|
+
return { ...rest, updated: this.now, ...session === undefined ? {} : { session }, source: this.author.source };
|
|
301
|
+
}
|
|
302
|
+
return { id: this.freshId(), created: this.now, updated: this.now, ...this.session === undefined ? {} : { session: this.session }, source: this.author.source };
|
|
303
|
+
}
|
|
304
|
+
/** Resolve a placement to its parent locator, heading level and new section key. */
|
|
305
|
+
resolvePlacement(scope, placement, movingId) {
|
|
306
|
+
const choices = [placement.parentId !== undefined, placement.section !== undefined, placement.newSection].filter(Boolean).length;
|
|
307
|
+
if (choices !== 1)
|
|
308
|
+
throw MemoryError.invalid('Give exactly one of `parentId`, `section` or `newSection`.');
|
|
309
|
+
const views = this.scope(scope).document.views(scope);
|
|
310
|
+
if (placement.parentId !== undefined) {
|
|
311
|
+
const parentId = placement.parentId;
|
|
312
|
+
const parent = views.find(view => view.id === parentId);
|
|
313
|
+
if (parent === undefined) {
|
|
314
|
+
throw MemoryError.invalid(movingId !== undefined
|
|
315
|
+
? `Parent \`${parentId}\` is not in the ${scope} scope, or it is inside the node being moved.`
|
|
316
|
+
: `Parent \`${parentId}\` is not in the ${scope} scope.`);
|
|
317
|
+
}
|
|
318
|
+
if (parent.level >= MAX_NODE_LEVEL) {
|
|
319
|
+
throw MemoryError.invalid(`Parent \`${parentId}\` is already H${MAX_NODE_LEVEL}; add the content to its body or choose a shallower parent.`);
|
|
320
|
+
}
|
|
321
|
+
return { parent: parent.locator, level: parent.level + 1, sectionKey: undefined };
|
|
322
|
+
}
|
|
323
|
+
if (placement.section !== undefined) {
|
|
324
|
+
const wanted = placement.section.trim();
|
|
325
|
+
const parent = views.find(view => view.level === 1 && view.section === wanted)
|
|
326
|
+
?? views.find(view => view.locator.length === 1 && view.level === 1 && view.path[0] !== undefined && asciiLower(view.path[0]) === asciiLower(wanted));
|
|
327
|
+
if (parent === undefined) {
|
|
328
|
+
const existing = views.filter(view => view.locator.length === 1 && view.level === 1).map(view => view.section ?? view.path[0]).join(', ');
|
|
329
|
+
throw MemoryError.invalid(`Section \`${wanted}\` does not exist in the ${scope} scope (existing: ${existing}). Use one of them, or set newSection to declare a new one.`);
|
|
330
|
+
}
|
|
331
|
+
return { parent: parent.locator, level: 2, sectionKey: undefined };
|
|
332
|
+
}
|
|
333
|
+
const taken = new Set(views.flatMap(view => view.section === undefined ? [] : [view.section]));
|
|
334
|
+
for (;;) {
|
|
335
|
+
const candidate = `s-${randomHex(6)}`;
|
|
336
|
+
if (!taken.has(candidate))
|
|
337
|
+
return { parent: undefined, level: 1, sectionKey: candidate };
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
/** A node id unused in every loaded scope. */
|
|
341
|
+
freshId() {
|
|
342
|
+
const used = new Set(this.allViews().map(view => view.id));
|
|
343
|
+
for (;;) {
|
|
344
|
+
const candidate = `m-${randomHex(8)}`;
|
|
345
|
+
if (!used.has(candidate))
|
|
346
|
+
return candidate;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
viewById(scope, id) {
|
|
350
|
+
const view = this.scope(scope).document.views(scope).find(candidate => candidate.id === id);
|
|
351
|
+
if (view === undefined)
|
|
352
|
+
throw MemoryError.notFound(id);
|
|
353
|
+
return view;
|
|
354
|
+
}
|
|
355
|
+
/** Similar nodes in the same scope, and in the space scope for project writes. */
|
|
356
|
+
duplicates(scope, title, body, exclude) {
|
|
357
|
+
const scored = [];
|
|
358
|
+
for (const candidateScope of scope === 'project' ? ['project', 'space'] : [scope]) {
|
|
359
|
+
for (const view of this.scope(candidateScope).document.views(candidateScope)) {
|
|
360
|
+
if (view.id === exclude || view.level === 1)
|
|
361
|
+
continue;
|
|
362
|
+
const score = similarity(title, body, viewTitle(view), view.body);
|
|
363
|
+
if (score >= DUPLICATE_THRESHOLD)
|
|
364
|
+
scored.push([score, view]);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
scored.sort((left, right) => right[0] - left[0]);
|
|
368
|
+
return scored.slice(0, 3).map(([, view]) => ({ id: view.id, scope: view.scope, path: view.path }));
|
|
369
|
+
}
|
|
370
|
+
/** Validate file sizes and commit every changed file together with the registry. */
|
|
371
|
+
finish(operations, budget) {
|
|
372
|
+
const files = [];
|
|
373
|
+
for (const scope of SCOPES) {
|
|
374
|
+
const loaded = this.scopes.get(scope);
|
|
375
|
+
if (loaded === undefined || !loaded.changed)
|
|
376
|
+
continue;
|
|
377
|
+
const text = loaded.document.render();
|
|
378
|
+
const bytes = byteLength(text);
|
|
379
|
+
if (bytes > MAX_FILE_BYTES) {
|
|
380
|
+
throw MemoryError.invalid(`\`${loaded.path}\` would grow to ${bytes} bytes, above the ${MAX_FILE_BYTES}-byte limit; merge or delete outdated nodes first.`);
|
|
381
|
+
}
|
|
382
|
+
files.push({ path: loaded.path, original: loaded.original, text });
|
|
383
|
+
}
|
|
384
|
+
if (this.createdProject !== undefined) {
|
|
385
|
+
const file = path.join(this.store.markdownDirectory, REGISTRY_FILE);
|
|
386
|
+
files.push({ path: file, original: readOptional(file), text: `${this.registry.toJson()}\n` });
|
|
387
|
+
}
|
|
388
|
+
commitFiles(files);
|
|
389
|
+
const touched = new Set(operations.map(operation => operation.scope));
|
|
390
|
+
const usage = SCOPES.filter(scope => touched.has(scope)).map(scope => baseUsage(scope, this.scope(scope).document, budget));
|
|
391
|
+
return { operations, usage, createdProject: this.createdProject };
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
/** Similarity at or above which a node is reported as a possible duplicate. */
|
|
395
|
+
const DUPLICATE_THRESHOLD = 0.6;
|
|
396
|
+
/** Score two nodes by title character bigrams and body word overlap. */
|
|
397
|
+
export function similarity(title, body, otherTitle, otherBody) {
|
|
398
|
+
const titles = jaccard(titleBigrams(title), titleBigrams(otherTitle));
|
|
399
|
+
const left = bodyTerms(body);
|
|
400
|
+
const right = bodyTerms(otherBody);
|
|
401
|
+
const bodies = left.size >= 8 && right.size >= 8 ? jaccard(left, right) * 0.8 : 0;
|
|
402
|
+
return Math.max(titles, bodies);
|
|
403
|
+
}
|
|
404
|
+
/** Lowercase character bigrams of a title, ignoring spaces and punctuation (Rust `char::is_alphanumeric`). */
|
|
405
|
+
function titleBigrams(title) {
|
|
406
|
+
const characters = [...title].filter(character => /[\p{Alphabetic}\p{N}]/u.test(character)).flatMap(character => [...character.toLowerCase()]);
|
|
407
|
+
if (characters.length === 1)
|
|
408
|
+
return new Set(characters);
|
|
409
|
+
const pairs = new Set();
|
|
410
|
+
for (let index = 0; index + 1 < characters.length; index += 1)
|
|
411
|
+
pairs.add(`${characters[index]}${characters[index + 1]}`);
|
|
412
|
+
return pairs;
|
|
413
|
+
}
|
|
414
|
+
/** Body terms: lowercase ASCII words of three or more letters and CJK bigrams. */
|
|
415
|
+
function bodyTerms(body) {
|
|
416
|
+
const terms = new Set();
|
|
417
|
+
for (const word of body.split(/[^A-Za-z0-9]/))
|
|
418
|
+
if (word.length >= 3)
|
|
419
|
+
terms.add(word.toLowerCase());
|
|
420
|
+
const cjk = [...body].filter(character => isCjkCharacter(character.codePointAt(0) ?? 0));
|
|
421
|
+
for (let index = 0; index + 1 < cjk.length; index += 1)
|
|
422
|
+
terms.add(`${cjk[index]}${cjk[index + 1]}`);
|
|
423
|
+
return terms;
|
|
424
|
+
}
|
|
425
|
+
function jaccard(left, right) {
|
|
426
|
+
let shared = 0;
|
|
427
|
+
for (const term of left)
|
|
428
|
+
if (right.has(term))
|
|
429
|
+
shared += 1;
|
|
430
|
+
const union = left.size + right.size - shared;
|
|
431
|
+
return union === 0 ? 0 : shared / union;
|
|
432
|
+
}
|
|
433
|
+
/** Rust `eq_ignore_ascii_case` folds only ASCII letters. */
|
|
434
|
+
function asciiLower(text) {
|
|
435
|
+
return text.replace(/[A-Z]/g, letter => letter.toLowerCase());
|
|
436
|
+
}
|
|
437
|
+
/** Refuse to move or delete the base section. */
|
|
438
|
+
function guardBaseSection(view, verb) {
|
|
439
|
+
if (view.level === 1 && view.locator.length === 1 && view.meta?.section === BASE_SECTION_KEY) {
|
|
440
|
+
throw MemoryError.invalid(`The base section cannot be ${verb}; change the nodes inside it instead.`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
/** Validate a title for a heading level and return it trimmed. */
|
|
444
|
+
function validateTitle(title, level) {
|
|
445
|
+
const trimmed = title.trim();
|
|
446
|
+
if (trimmed === '' || [...trimmed].length > MAX_TITLE_CHARS)
|
|
447
|
+
throw MemoryError.invalid(`A title must have 1 to ${MAX_TITLE_CHARS} characters.`);
|
|
448
|
+
if (trimmed.startsWith('#') || !titleRoundTrips(level, trimmed)) {
|
|
449
|
+
throw MemoryError.invalid(`Title \`${trimmed}\` must be a single line of plain heading text (no leading or trailing \`#\`).`);
|
|
450
|
+
}
|
|
451
|
+
return trimmed;
|
|
452
|
+
}
|
|
453
|
+
/** Validate a body: size, no node-forming headings and no metadata comments. */
|
|
454
|
+
function validateBody(body) {
|
|
455
|
+
if (byteLength(body) > MAX_NODE_BODY_BYTES)
|
|
456
|
+
throw MemoryError.invalid(`A node body can hold at most ${MAX_NODE_BODY_BYTES} bytes; split it into child nodes.`);
|
|
457
|
+
const [level] = nodeHeadingLevels(body);
|
|
458
|
+
if (level !== undefined) {
|
|
459
|
+
throw MemoryError.invalid(`The body contains an H${level} heading, which would create a separate node; create a child node instead, or use H5/H6 inside the body.`);
|
|
460
|
+
}
|
|
461
|
+
if (lines(body).some(line => line.trimStart().startsWith('<!-- mem'))) {
|
|
462
|
+
throw MemoryError.invalid('The body must not contain `<!-- mem` metadata comments; the host maintains them.');
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
/** A document holding the scope's preset sections. */
|
|
466
|
+
function presetDocument(scope, now) {
|
|
467
|
+
const document = MemoryDocument.empty();
|
|
468
|
+
for (const [key, title] of presetSections(scope)) {
|
|
469
|
+
document.append(undefined, new MemoryNode(1, title, { id: `m-${randomHex(8)}`, section: key, created: now, updated: now, source: 'user' }, '\n'));
|
|
470
|
+
}
|
|
471
|
+
return document;
|
|
472
|
+
}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Export packages and their node-level import (`package.rs`): export builds a versioned zip, import
|
|
3
|
+
* previews the merge of every scope against the local files and applies it atomically once every
|
|
4
|
+
* packaged project has a target and every conflict a resolution. The page moves the archive bytes
|
|
5
|
+
* through the browser, so these functions take and return bytes instead of native paths; the
|
|
6
|
+
* archive format is VulcanCode's, and packages move between the two in both directions.
|
|
7
|
+
*/
|
|
8
|
+
import { type MemoryScope } from './scope.js';
|
|
9
|
+
import { type MarkdownMemoryStore } from './store.js';
|
|
10
|
+
export interface ExportSelection {
|
|
11
|
+
readonly user: boolean;
|
|
12
|
+
readonly space: boolean;
|
|
13
|
+
readonly memoryProjectIds: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
export interface ExportedPackage {
|
|
16
|
+
/** Suggested download name, as VulcanCode names the file it writes. */
|
|
17
|
+
readonly fileName: string;
|
|
18
|
+
readonly bytes: Uint8Array;
|
|
19
|
+
readonly sha256: string;
|
|
20
|
+
}
|
|
21
|
+
/** One project recorded in a package manifest. */
|
|
22
|
+
export interface PackageProject {
|
|
23
|
+
/** Memory project id on the exporting machine; names the project entry. */
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly name: string;
|
|
26
|
+
/** Root paths the project was opened from, as a matching hint. */
|
|
27
|
+
readonly paths: readonly string[];
|
|
28
|
+
readonly firstCommit?: string;
|
|
29
|
+
}
|
|
30
|
+
export type ProjectTarget = {
|
|
31
|
+
readonly action: 'link';
|
|
32
|
+
readonly memoryProjectId: string;
|
|
33
|
+
} | {
|
|
34
|
+
readonly action: 'create';
|
|
35
|
+
} | {
|
|
36
|
+
readonly action: 'ignore';
|
|
37
|
+
};
|
|
38
|
+
export interface ProjectMapping {
|
|
39
|
+
readonly packageProjectId: string;
|
|
40
|
+
readonly target: ProjectTarget;
|
|
41
|
+
}
|
|
42
|
+
export type ConflictChoice = 'keep_local' | 'take_imported' | 'keep_both';
|
|
43
|
+
export interface ConflictResolution {
|
|
44
|
+
readonly scope: MemoryScope;
|
|
45
|
+
readonly packageProjectId?: string;
|
|
46
|
+
readonly nodeId: string;
|
|
47
|
+
readonly choice: ConflictChoice;
|
|
48
|
+
}
|
|
49
|
+
/** A local memory project suggested for a packaged project, with stable reason keys. */
|
|
50
|
+
export interface ProjectCandidate {
|
|
51
|
+
readonly memoryProjectId: string;
|
|
52
|
+
readonly name: string;
|
|
53
|
+
readonly reasons: readonly ('same_path' | 'same_first_commit' | 'same_directory_name')[];
|
|
54
|
+
}
|
|
55
|
+
export interface PackageProjectPreview {
|
|
56
|
+
readonly project: PackageProject;
|
|
57
|
+
/** Suggested local projects, best first; only hints, nothing is preselected. */
|
|
58
|
+
readonly candidates: readonly ProjectCandidate[];
|
|
59
|
+
}
|
|
60
|
+
export interface NodeSummary {
|
|
61
|
+
readonly id: string;
|
|
62
|
+
readonly path: readonly string[];
|
|
63
|
+
}
|
|
64
|
+
export interface NodeSide {
|
|
65
|
+
readonly title: string;
|
|
66
|
+
readonly body: string;
|
|
67
|
+
readonly updated: string | null;
|
|
68
|
+
}
|
|
69
|
+
export type NewerSide = 'local' | 'imported' | 'unknown';
|
|
70
|
+
/** A node present on both sides with different content. */
|
|
71
|
+
export interface NodeConflict {
|
|
72
|
+
/** Packaged node id, used by the resolution. */
|
|
73
|
+
readonly id: string;
|
|
74
|
+
/** Local title path. */
|
|
75
|
+
readonly path: readonly string[];
|
|
76
|
+
readonly local: NodeSide;
|
|
77
|
+
readonly imported: NodeSide;
|
|
78
|
+
/** Which side is newer; the suggested default. */
|
|
79
|
+
readonly newer: NewerSide;
|
|
80
|
+
}
|
|
81
|
+
export interface ScopeMergePreview {
|
|
82
|
+
readonly scope: MemoryScope;
|
|
83
|
+
readonly packageProjectId: string | null;
|
|
84
|
+
/** Local memory project merged into; `null` when a new project is created. */
|
|
85
|
+
readonly memoryProjectId: string | null;
|
|
86
|
+
readonly same: number;
|
|
87
|
+
readonly added: readonly NodeSummary[];
|
|
88
|
+
/** Added nodes whose title path already exists locally under another id; both are kept. */
|
|
89
|
+
readonly duplicates: readonly NodeSummary[];
|
|
90
|
+
readonly conflicts: readonly NodeConflict[];
|
|
91
|
+
/** Titles of the top-level sections the import creates. */
|
|
92
|
+
readonly newSections: readonly string[];
|
|
93
|
+
}
|
|
94
|
+
/** The preview of an import, bound to the exact archive bytes by their SHA-256. */
|
|
95
|
+
export interface ImportPreview {
|
|
96
|
+
readonly sha256: string;
|
|
97
|
+
readonly exportedAt: string;
|
|
98
|
+
readonly projects: readonly PackageProjectPreview[];
|
|
99
|
+
readonly scopes: readonly ScopeMergePreview[];
|
|
100
|
+
}
|
|
101
|
+
export interface ImportReport {
|
|
102
|
+
/** Directory holding the replaced files; `null` when no existing file changed. */
|
|
103
|
+
readonly backupDirectory: string | null;
|
|
104
|
+
/** Nodes added, including duplicates and kept-both copies. */
|
|
105
|
+
readonly added: number;
|
|
106
|
+
readonly replaced: number;
|
|
107
|
+
/** Identical nodes and conflicts that kept the local side. */
|
|
108
|
+
readonly unchanged: number;
|
|
109
|
+
readonly createdProjects: readonly string[];
|
|
110
|
+
}
|
|
111
|
+
/** Build an export package of the selected scopes. */
|
|
112
|
+
export declare function exportPackage(store: MarkdownMemoryStore, selection: ExportSelection): ExportedPackage;
|
|
113
|
+
/** Preview importing a package; mapped projects are compared with their targets, unmapped ones only listed. */
|
|
114
|
+
export declare function previewImport(store: MarkdownMemoryStore, archive: Uint8Array, name: string, mappings: readonly ProjectMapping[]): ImportPreview;
|
|
115
|
+
/**
|
|
116
|
+
* Apply a previewed import: the archive must still have the previewed SHA-256, every packaged
|
|
117
|
+
* project needs a mapping and every conflict a resolution. Replaced files are backed up first,
|
|
118
|
+
* then every file and the registry are committed as one batch.
|
|
119
|
+
*/
|
|
120
|
+
export declare function applyImport(store: MarkdownMemoryStore, archive: Uint8Array, name: string, expectedSha256: string, mappings: readonly ProjectMapping[], resolutions: readonly ConflictResolution[]): ImportReport;
|
|
121
|
+
/** The archive bytes: the manifest first, then the scope entries, deflated. */
|
|
122
|
+
export declare function writeArchive(manifest: string, entries: readonly (readonly [string, string])[]): Uint8Array;
|
|
123
|
+
/** Parse the page's project mappings, refusing unknown fields like serde's `deny_unknown_fields`. */
|
|
124
|
+
export declare function parseMappings(value: unknown): ProjectMapping[];
|
|
125
|
+
/** Parse the page's conflict resolutions. */
|
|
126
|
+
export declare function parseResolutions(value: unknown): ConflictResolution[];
|