@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.
Files changed (171) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -2
  3. package/assets/openvulcan-icon.svg +33 -0
  4. package/cordis.patch.yml +17 -0
  5. package/node_modules/@openvulcan/dsh-decision-guard/LICENSE +21 -0
  6. package/node_modules/@openvulcan/dsh-decision-guard/README.md +14 -0
  7. package/node_modules/@openvulcan/dsh-decision-guard/assets/decision-guard-icon.png +0 -0
  8. package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.d.ts +40 -0
  9. package/node_modules/@openvulcan/dsh-decision-guard/lib/calls.js +150 -0
  10. package/node_modules/@openvulcan/dsh-decision-guard/lib/client.js +2193 -0
  11. package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.d.ts +90 -0
  12. package/node_modules/@openvulcan/dsh-decision-guard/lib/decide.js +108 -0
  13. package/node_modules/@openvulcan/dsh-decision-guard/lib/index.d.ts +129 -0
  14. package/node_modules/@openvulcan/dsh-decision-guard/lib/index.js +572 -0
  15. package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.d.ts +22 -0
  16. package/node_modules/@openvulcan/dsh-decision-guard/lib/intent.js +77 -0
  17. package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.d.ts +158 -0
  18. package/node_modules/@openvulcan/dsh-decision-guard/lib/panel.js +340 -0
  19. package/node_modules/@openvulcan/dsh-decision-guard/lib/records.d.ts +133 -0
  20. package/node_modules/@openvulcan/dsh-decision-guard/lib/records.js +186 -0
  21. package/node_modules/@openvulcan/dsh-decision-guard/lib/review.d.ts +55 -0
  22. package/node_modules/@openvulcan/dsh-decision-guard/lib/review.js +202 -0
  23. package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.d.ts +62 -0
  24. package/node_modules/@openvulcan/dsh-decision-guard/lib/session-state.js +155 -0
  25. package/node_modules/@openvulcan/dsh-decision-guard/lib/version.d.ts +2 -0
  26. package/node_modules/@openvulcan/dsh-decision-guard/lib/version.js +4 -0
  27. package/node_modules/@openvulcan/dsh-decision-guard/package.json +68 -0
  28. package/node_modules/@openvulcan/dsh-vulcan-account/LICENSE +21 -0
  29. package/node_modules/@openvulcan/dsh-vulcan-account/assets/account-icon.png +0 -0
  30. package/node_modules/@openvulcan/dsh-vulcan-account/lib/client.js +565 -0
  31. package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.d.ts +92 -0
  32. package/node_modules/@openvulcan/dsh-vulcan-account/lib/index.js +194 -0
  33. package/node_modules/@openvulcan/dsh-vulcan-account/package.json +57 -0
  34. package/node_modules/@openvulcan/dsh-vulcan-answers/LICENSE +21 -0
  35. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/answers-icon.png +0 -0
  36. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-interactive.md +79 -0
  37. package/node_modules/@openvulcan/dsh-vulcan-answers/assets/prompts/answer-mode-standard.md +1 -0
  38. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.d.ts +258 -0
  39. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/answer-model.js +698 -0
  40. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/client.js +671 -0
  41. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.d.ts +29 -0
  42. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/fences.js +70 -0
  43. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.d.ts +19 -0
  44. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/index.js +52 -0
  45. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.d.ts +9 -0
  46. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/limits.js +9 -0
  47. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.d.ts +22 -0
  48. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/prompt.js +39 -0
  49. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.d.ts +20 -0
  50. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/reply-format.js +17 -0
  51. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.d.ts +66 -0
  52. package/node_modules/@openvulcan/dsh-vulcan-answers/lib/session-state.js +118 -0
  53. package/node_modules/@openvulcan/dsh-vulcan-answers/package.json +59 -0
  54. package/node_modules/@openvulcan/dsh-vulcan-luaskills/LICENSE +21 -0
  55. package/node_modules/@openvulcan/dsh-vulcan-luaskills/assets/luaskills-icon.svg +1 -0
  56. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/client.js +940 -0
  57. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.d.ts +46 -0
  58. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/constants.js +51 -0
  59. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.d.ts +55 -0
  60. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/engine.js +149 -0
  61. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.d.ts +49 -0
  62. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/hub.js +116 -0
  63. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.d.ts +26 -0
  64. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/index.js +276 -0
  65. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.d.ts +65 -0
  66. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/protocol.js +2 -0
  67. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.d.ts +111 -0
  68. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/runtime.js +281 -0
  69. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.d.ts +26 -0
  70. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/sdk-install.js +87 -0
  71. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.d.ts +79 -0
  72. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/settings.js +165 -0
  73. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.d.ts +34 -0
  74. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/skills.js +61 -0
  75. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.d.ts +59 -0
  76. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/tools.js +107 -0
  77. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.d.ts +10 -0
  78. package/node_modules/@openvulcan/dsh-vulcan-luaskills/lib/worker.js +140 -0
  79. package/node_modules/@openvulcan/dsh-vulcan-luaskills/package.json +57 -0
  80. package/node_modules/@openvulcan/dsh-vulcan-memory/LICENSE +21 -0
  81. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/markdown-memory-guide.md +21 -0
  82. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-curator/SKILL.md +31 -0
  83. package/node_modules/@openvulcan/dsh-vulcan-memory/assets/memory-icon.png +0 -0
  84. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/client.js +1090 -0
  85. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.d.ts +30 -0
  86. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/index.js +240 -0
  87. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.d.ts +30 -0
  88. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/inject.js +46 -0
  89. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.d.ts +23 -0
  90. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/constants.js +25 -0
  91. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.d.ts +108 -0
  92. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/document.js +373 -0
  93. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.d.ts +14 -0
  94. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/errors.js +31 -0
  95. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.d.ts +196 -0
  96. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/index.js +204 -0
  97. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.d.ts +92 -0
  98. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/operations.js +472 -0
  99. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.d.ts +126 -0
  100. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/package.js +553 -0
  101. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.d.ts +56 -0
  102. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/registry.js +182 -0
  103. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.d.ts +13 -0
  104. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/scope.js +24 -0
  105. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.d.ts +65 -0
  106. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/search.js +227 -0
  107. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.d.ts +34 -0
  108. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/snapshot.js +171 -0
  109. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.d.ts +72 -0
  110. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/store.js +250 -0
  111. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.d.ts +6 -0
  112. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/timestamp.js +12 -0
  113. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.d.ts +8 -0
  114. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/markdown/tokens.js +38 -0
  115. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.d.ts +132 -0
  116. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/page.js +235 -0
  117. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.d.ts +14 -0
  118. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/project.js +27 -0
  119. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.d.ts +29 -0
  120. package/node_modules/@openvulcan/dsh-vulcan-memory/lib/settings.js +62 -0
  121. package/node_modules/@openvulcan/dsh-vulcan-memory/package.json +63 -0
  122. package/node_modules/@openvulcan/dsh-vulcan-style/LICENSE +21 -0
  123. package/node_modules/@openvulcan/dsh-vulcan-style/assets/style-icon.png +0 -0
  124. package/node_modules/@openvulcan/dsh-vulcan-style/lib/client.js +1061 -0
  125. package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.d.ts +34 -0
  126. package/node_modules/@openvulcan/dsh-vulcan-style/lib/index.js +42 -0
  127. package/node_modules/@openvulcan/dsh-vulcan-style/package.json +51 -0
  128. package/node_modules/@openvulcan/openvulcan-account/LICENSE +21 -0
  129. package/node_modules/@openvulcan/openvulcan-account/lib/account.d.ts +140 -0
  130. package/node_modules/@openvulcan/openvulcan-account/lib/account.js +347 -0
  131. package/node_modules/@openvulcan/openvulcan-account/lib/index.d.ts +1 -0
  132. package/node_modules/@openvulcan/openvulcan-account/lib/index.js +1 -0
  133. package/node_modules/@openvulcan/openvulcan-account/package.json +29 -0
  134. package/node_modules/@openvulcan/vulcan-decision/LICENSE +21 -0
  135. package/node_modules/@openvulcan/vulcan-decision/README.md +23 -0
  136. package/node_modules/@openvulcan/vulcan-decision/bin/vulcan-decision.js +4 -0
  137. package/node_modules/@openvulcan/vulcan-decision/lib/cli.d.ts +5 -0
  138. package/node_modules/@openvulcan/vulcan-decision/lib/cli.js +267 -0
  139. package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.d.ts +211 -0
  140. package/node_modules/@openvulcan/vulcan-decision/lib/cloud/edge.js +594 -0
  141. package/node_modules/@openvulcan/vulcan-decision/lib/download.d.ts +36 -0
  142. package/node_modules/@openvulcan/vulcan-decision/lib/download.js +99 -0
  143. package/node_modules/@openvulcan/vulcan-decision/lib/engine.d.ts +37 -0
  144. package/node_modules/@openvulcan/vulcan-decision/lib/engine.js +39 -0
  145. package/node_modules/@openvulcan/vulcan-decision/lib/hardware.d.ts +105 -0
  146. package/node_modules/@openvulcan/vulcan-decision/lib/hardware.js +207 -0
  147. package/node_modules/@openvulcan/vulcan-decision/lib/home.d.ts +26 -0
  148. package/node_modules/@openvulcan/vulcan-decision/lib/home.js +54 -0
  149. package/node_modules/@openvulcan/vulcan-decision/lib/index.d.ts +17 -0
  150. package/node_modules/@openvulcan/vulcan-decision/lib/index.js +17 -0
  151. package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.d.ts +54 -0
  152. package/node_modules/@openvulcan/vulcan-decision/lib/local/engine.js +157 -0
  153. package/node_modules/@openvulcan/vulcan-decision/lib/local/models.d.ts +126 -0
  154. package/node_modules/@openvulcan/vulcan-decision/lib/local/models.js +261 -0
  155. package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.d.ts +102 -0
  156. package/node_modules/@openvulcan/vulcan-decision/lib/local/packs.js +195 -0
  157. package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.d.ts +61 -0
  158. package/node_modules/@openvulcan/vulcan-decision/lib/local/worker.js +123 -0
  159. package/node_modules/@openvulcan/vulcan-decision/lib/protocol.d.ts +74 -0
  160. package/node_modules/@openvulcan/vulcan-decision/lib/protocol.js +31 -0
  161. package/node_modules/@openvulcan/vulcan-decision/lib/render.d.ts +15 -0
  162. package/node_modules/@openvulcan/vulcan-decision/lib/render.js +33 -0
  163. package/node_modules/@openvulcan/vulcan-decision/lib/version.d.ts +2 -0
  164. package/node_modules/@openvulcan/vulcan-decision/lib/version.js +4 -0
  165. package/node_modules/@openvulcan/vulcan-decision/package.json +40 -0
  166. package/node_modules/@openvulcan/vulcan-decision/python/vulcan_decision_worker.py +363 -0
  167. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cpu.json +627 -0
  168. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/onnx-cuda.json +423 -0
  169. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cpu.json +1047 -0
  170. package/node_modules/@openvulcan/vulcan-decision/runtime-packs/torch-cu128.json +647 -0
  171. package/package.json +53 -4
@@ -0,0 +1,553 @@
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 { createHash } from 'node:crypto';
9
+ import { mkdirSync, writeFileSync } from 'node:fs';
10
+ import path from 'node:path';
11
+ import { unzipSync, zipSync } from 'fflate';
12
+ import { EXPORT_FORMAT_VERSION, MAX_FILE_BYTES, MAX_NODE_LEVEL, MAX_PACKAGE_BYTES, MAX_PACKAGE_PROJECTS, byteLength } from './constants.js';
13
+ import { MemoryDocument, storedBody, viewTitle } from './document.js';
14
+ import { MemoryError } from './errors.js';
15
+ import { REGISTRY_FILE, compare, directoryName, isRecord, randomHex } from './registry.js';
16
+ import { isScope } from './scope.js';
17
+ import { commitFiles, readOptional } from './store.js';
18
+ import { nowUtcMinute } from './timestamp.js';
19
+ const PACKAGE_FORMAT = 'vulcan-markdown-memory';
20
+ const MANIFEST_ENTRY = 'manifest.json';
21
+ const USER_ENTRY = 'user.md';
22
+ const SPACE_ENTRY = 'space.md';
23
+ const PROJECT_ENTRY_PREFIX = 'projects/';
24
+ /** Directory under the Markdown memory directory that keeps the files replaced by imports. */
25
+ const BACKUP_DIRECTORY = 'backups';
26
+ /** Build an export package of the selected scopes. */
27
+ export function exportPackage(store, selection) {
28
+ if (!selection.user && !selection.space && selection.memoryProjectIds.length === 0) {
29
+ throw MemoryError.invalid('Choose at least one scope or project to export.');
30
+ }
31
+ if (selection.memoryProjectIds.length > MAX_PACKAGE_PROJECTS) {
32
+ throw MemoryError.invalid(`A package can hold at most ${MAX_PACKAGE_PROJECTS} projects.`);
33
+ }
34
+ const registry = store.registry();
35
+ const projects = [];
36
+ const entries = [];
37
+ for (const id of [...new Set(selection.memoryProjectIds)].sort(compare)) {
38
+ const project = registry.project(id);
39
+ if (project === undefined)
40
+ throw MemoryError.invalid(`Memory project \`${id}\` does not exist.`);
41
+ projects.push({
42
+ id: project.id,
43
+ name: project.name,
44
+ paths: [...project.paths],
45
+ ...project.firstCommit === undefined ? {} : { firstCommit: project.firstCommit },
46
+ });
47
+ entries.push([`${PROJECT_ENTRY_PREFIX}${id}.md`, store.readFileText('project', id)]);
48
+ }
49
+ if (selection.user)
50
+ entries.push([USER_ENTRY, store.readFileText('user', undefined)]);
51
+ if (selection.space)
52
+ entries.push([SPACE_ENTRY, store.readFileText('space', undefined)]);
53
+ const now = nowUtcMinute();
54
+ const manifest = {
55
+ format: PACKAGE_FORMAT,
56
+ version: EXPORT_FORMAT_VERSION,
57
+ exportedAt: now,
58
+ user: selection.user,
59
+ space: selection.space,
60
+ projects,
61
+ };
62
+ const bytes = writeArchive(JSON.stringify(manifest, null, 2), entries);
63
+ return { fileName: `vulcan-memory-${fileStamp(now)}-${randomHex(6)}.zip`, bytes, sha256: sha256(bytes) };
64
+ }
65
+ /** Preview importing a package; mapped projects are compared with their targets, unmapped ones only listed. */
66
+ export function previewImport(store, archive, name, mappings) {
67
+ const pkg = readPackage(archive, name);
68
+ const registry = store.registry();
69
+ const table = mappingTable(pkg, registry, mappings, false);
70
+ const scopes = [];
71
+ for (const planned of plannedScopes(pkg, table)) {
72
+ const local = planned.scope === 'project' && planned.memoryProjectId === undefined
73
+ ? MemoryDocument.empty()
74
+ : store.load(planned.scope, planned.memoryProjectId);
75
+ const outcome = merge(local, MemoryDocument.parse(planned.text), planned.scope, () => undefined);
76
+ scopes.push({
77
+ scope: planned.scope,
78
+ packageProjectId: planned.packageProjectId ?? null,
79
+ memoryProjectId: planned.memoryProjectId ?? null,
80
+ same: outcome.same,
81
+ added: outcome.added,
82
+ duplicates: outcome.duplicates,
83
+ conflicts: outcome.conflicts,
84
+ newSections: outcome.newSections,
85
+ });
86
+ }
87
+ return {
88
+ sha256: pkg.sha256,
89
+ exportedAt: pkg.manifest.exportedAt,
90
+ projects: pkg.manifest.projects.map(project => ({ project, candidates: projectCandidates(project, registry) })),
91
+ scopes,
92
+ };
93
+ }
94
+ /**
95
+ * Apply a previewed import: the archive must still have the previewed SHA-256, every packaged
96
+ * project needs a mapping and every conflict a resolution. Replaced files are backed up first,
97
+ * then every file and the registry are committed as one batch.
98
+ */
99
+ export function applyImport(store, archive, name, expectedSha256, mappings, resolutions) {
100
+ const pkg = readPackage(archive, name);
101
+ if (pkg.sha256 !== expectedSha256)
102
+ throw MemoryError.invalid('The package changed since it was previewed; preview it again.');
103
+ const registry = store.registry();
104
+ const table = mappingTable(pkg, registry, mappings, true);
105
+ const choices = new Map();
106
+ for (const resolution of resolutions) {
107
+ const key = conflictKey(resolution.scope, resolution.packageProjectId, resolution.nodeId);
108
+ if (choices.has(key))
109
+ throw MemoryError.invalid(`Conflict \`${resolution.nodeId}\` is resolved more than once.`);
110
+ choices.set(key, resolution.choice);
111
+ }
112
+ const now = nowUtcMinute();
113
+ const documents = new Map();
114
+ let added = 0;
115
+ let replaced = 0;
116
+ let unchanged = 0;
117
+ const createdProjects = [];
118
+ let resolved = 0;
119
+ for (const planned of plannedScopes(pkg, table)) {
120
+ if (planned.scope === 'project' && planned.memoryProjectId === undefined) {
121
+ const project = pkg.manifest.projects.find(candidate => candidate.id === planned.packageProjectId);
122
+ if (project === undefined)
123
+ throw new Error('planned projects come from the manifest');
124
+ const id = registry.createUnbound(project.name, [...project.paths], project.firstCommit, now);
125
+ createdProjects.push(id);
126
+ planned.memoryProjectId = id;
127
+ }
128
+ const file = store.scopePath(planned.scope, planned.memoryProjectId);
129
+ let entry = documents.get(file);
130
+ if (entry === undefined) {
131
+ const original = readOptional(file);
132
+ entry = { original, document: original === undefined ? MemoryDocument.empty() : MemoryDocument.parse(original) };
133
+ documents.set(file, entry);
134
+ }
135
+ const outcome = merge(entry.document, MemoryDocument.parse(planned.text), planned.scope, nodeId => choices.get(conflictKey(planned.scope, planned.packageProjectId, nodeId)));
136
+ const missing = outcome.conflicts.find(conflict => !choices.has(conflictKey(planned.scope, planned.packageProjectId, conflict.id)));
137
+ if (missing !== undefined)
138
+ throw MemoryError.invalid(`Conflict \`${missing.id}\` (${missing.path.join(' > ')}) has no resolution.`);
139
+ resolved += outcome.conflicts.length;
140
+ added += outcome.added.length + outcome.keptBoth;
141
+ replaced += outcome.replaced;
142
+ unchanged += outcome.same + outcome.conflicts.length - outcome.replaced - outcome.keptBoth;
143
+ if (changed(outcome))
144
+ entry.document = outcome.document;
145
+ }
146
+ if (resolved !== choices.size)
147
+ throw MemoryError.invalid('A resolution names a conflict this import does not have; preview it again.');
148
+ const files = [];
149
+ for (const [file, { original, document }] of documents) {
150
+ const text = document.render();
151
+ if (original === text || (original === undefined && text === ''))
152
+ continue;
153
+ if (byteLength(text) > MAX_FILE_BYTES) {
154
+ throw MemoryError.invalid(`\`${file}\` would grow to ${byteLength(text)} bytes, above the ${MAX_FILE_BYTES}-byte limit.`);
155
+ }
156
+ files.push({ path: file, original, text });
157
+ }
158
+ if (createdProjects.length > 0) {
159
+ const file = path.join(store.markdownDirectory, REGISTRY_FILE);
160
+ files.push({ path: file, original: readOptional(file), text: `${registry.toJson()}\n` });
161
+ }
162
+ const backupDirectory = backUp(store, files, now);
163
+ commitFiles(files);
164
+ return { backupDirectory, added, replaced, unchanged, createdProjects };
165
+ }
166
+ /** Copy every existing file a commit replaces into a new backup directory. */
167
+ function backUp(store, files, now) {
168
+ const replacedFiles = files.filter(file => file.original !== undefined);
169
+ if (replacedFiles.length === 0)
170
+ return null;
171
+ const markdown = store.markdownDirectory;
172
+ const directory = path.join(markdown, BACKUP_DIRECTORY, `import-${fileStamp(now)}-${randomHex(6)}`);
173
+ for (const file of replacedFiles) {
174
+ const target = path.join(directory, path.relative(markdown, file.path));
175
+ try {
176
+ mkdirSync(path.dirname(target), { recursive: true });
177
+ writeFileSync(target, file.original ?? '');
178
+ }
179
+ catch (error) {
180
+ throw MemoryError.io(target, 'written', error);
181
+ }
182
+ }
183
+ return directory;
184
+ }
185
+ function changed(outcome) {
186
+ return outcome.added.length > 0 || outcome.replaced > 0 || outcome.keptBoth > 0;
187
+ }
188
+ /** Key of one conflict across all scopes of an import. */
189
+ function conflictKey(scope, packageProjectId, nodeId) {
190
+ return `${scope}\u001f${packageProjectId ?? ''}\u001f${nodeId}`;
191
+ }
192
+ /** Validate mappings against the package and the registry; `complete` requires one for every packaged project. */
193
+ function mappingTable(pkg, registry, mappings, complete) {
194
+ const table = new Map();
195
+ for (const mapping of mappings) {
196
+ if (!pkg.projects.has(mapping.packageProjectId))
197
+ throw MemoryError.invalid(`The package has no project \`${mapping.packageProjectId}\`.`);
198
+ if (mapping.target.action === 'link' && registry.project(mapping.target.memoryProjectId) === undefined) {
199
+ throw MemoryError.invalid(`Memory project \`${mapping.target.memoryProjectId}\` does not exist.`);
200
+ }
201
+ if (table.has(mapping.packageProjectId))
202
+ throw MemoryError.invalid(`Project \`${mapping.packageProjectId}\` is mapped more than once.`);
203
+ table.set(mapping.packageProjectId, mapping.target);
204
+ }
205
+ if (complete) {
206
+ const missing = [...pkg.projects.keys()].sort(compare).find(id => !table.has(id));
207
+ if (missing !== undefined)
208
+ throw MemoryError.invalid(`Choose where packaged project \`${missing}\` goes before importing.`);
209
+ }
210
+ return table;
211
+ }
212
+ /** The scope files an import touches: user, space, then every linked or created project. */
213
+ function plannedScopes(pkg, mappings) {
214
+ const planned = [];
215
+ for (const [scope, text] of [['user', pkg.user], ['space', pkg.space]]) {
216
+ if (text !== undefined)
217
+ planned.push({ scope, packageProjectId: undefined, memoryProjectId: undefined, text });
218
+ }
219
+ for (const project of pkg.manifest.projects) {
220
+ const target = mappings.get(project.id);
221
+ if (target === undefined || target.action === 'ignore')
222
+ continue;
223
+ const text = pkg.projects.get(project.id);
224
+ if (text === undefined)
225
+ throw new Error('every manifest project has a file');
226
+ planned.push({
227
+ scope: 'project',
228
+ packageProjectId: project.id,
229
+ memoryProjectId: target.action === 'link' ? target.memoryProjectId : undefined,
230
+ text,
231
+ });
232
+ }
233
+ return planned;
234
+ }
235
+ /** Rank local memory projects that look like a packaged project. */
236
+ function projectCandidates(project, registry) {
237
+ const names = new Set(project.paths.map(item => directoryName(item).toLowerCase()));
238
+ const candidates = [];
239
+ for (const local of registry.projects) {
240
+ const reasons = [];
241
+ if (local.paths.some(item => project.paths.includes(item)))
242
+ reasons.push('same_path');
243
+ if (project.firstCommit !== undefined && local.firstCommit === project.firstCommit)
244
+ reasons.push('same_first_commit');
245
+ if (local.paths.some(item => names.has(directoryName(item).toLowerCase())))
246
+ reasons.push('same_directory_name');
247
+ if (reasons.length > 0)
248
+ candidates.push({ memoryProjectId: local.id, name: local.name, reasons });
249
+ }
250
+ return candidates.sort((left, right) => right.reasons.length - left.reasons.length || compare(left.name, right.name));
251
+ }
252
+ /**
253
+ * Merge an imported document into a local one node by node. Top-level sections match by section
254
+ * key, other nodes by id; identical nodes are skipped, missing nodes are appended under their
255
+ * matched parent, and conflicts follow `choose` (unresolved conflicts keep the local side).
256
+ */
257
+ function merge(local, imported, scope, choose) {
258
+ const outcome = {
259
+ // Rendering reproduces the source bytes, so parsing them again is a deep copy.
260
+ document: MemoryDocument.parse(local.render()),
261
+ same: 0,
262
+ added: [],
263
+ duplicates: [],
264
+ conflicts: [],
265
+ newSections: [],
266
+ replaced: 0,
267
+ keptBoth: 0,
268
+ };
269
+ const eol = local.eol;
270
+ const importedViews = imported.views(scope);
271
+ const importedIds = new Map(importedViews.map(view => [view.locator.join('/'), view.id]));
272
+ // Imported node id → locator of its node in the merged document. Merging only appends, so
273
+ // recorded locators stay valid.
274
+ const placed = new Map();
275
+ for (const view of importedViews) {
276
+ const node = imported.node(view.locator);
277
+ if (node === undefined)
278
+ throw new Error('views come from this document');
279
+ const mergedViews = outcome.document.views(scope);
280
+ const found = matchingView(mergedViews, view);
281
+ if (found !== undefined) {
282
+ placed.set(view.id, found.locator);
283
+ const title = viewTitle(view);
284
+ const localTitle = viewTitle(found);
285
+ if (localTitle.trim() === title.trim() && found.body === view.body) {
286
+ outcome.same += 1;
287
+ continue;
288
+ }
289
+ const localUpdated = found.meta?.updated ?? null;
290
+ const importedUpdated = view.meta?.updated ?? null;
291
+ outcome.conflicts.push({
292
+ id: view.id,
293
+ path: found.path,
294
+ local: { title: localTitle, body: found.body, updated: localUpdated },
295
+ imported: { title, body: view.body, updated: importedUpdated },
296
+ newer: localUpdated !== null && importedUpdated !== null && importedUpdated > localUpdated ? 'imported'
297
+ : localUpdated !== null && importedUpdated !== null && localUpdated > importedUpdated ? 'local'
298
+ : 'unknown',
299
+ });
300
+ const choice = choose(view.id);
301
+ if (choice === 'take_imported') {
302
+ const target = outcome.document.node(found.locator);
303
+ if (target === undefined)
304
+ throw new Error('the match comes from this document');
305
+ target.setTitle(node.title);
306
+ target.setBody(storedBody(view.body, eol));
307
+ if (found.meta !== undefined && view.meta !== undefined && found.meta.id !== view.meta.id) {
308
+ // A section keeps its local identity and takes the imported time.
309
+ target.setMeta({ ...found.meta, updated: view.meta.updated });
310
+ }
311
+ else if (view.meta !== undefined) {
312
+ target.setMeta(view.meta);
313
+ }
314
+ outcome.replaced += 1;
315
+ }
316
+ else if (choice === 'keep_both') {
317
+ const copy = node.detached(found.level, eol);
318
+ if (copy.meta !== undefined) {
319
+ const { section: _section, ...meta } = copy.meta;
320
+ copy.setMeta({ ...meta, id: unusedNodeId(mergedViews) });
321
+ }
322
+ const parent = found.locator.slice(0, -1);
323
+ outcome.document.append(parent.length > 0 ? parent : undefined, copy);
324
+ outcome.keptBoth += 1;
325
+ }
326
+ continue;
327
+ }
328
+ // The node is new here: place it under the node its imported parent maps to.
329
+ const parentLocator = view.locator.slice(0, -1);
330
+ let parent;
331
+ let level = view.level;
332
+ if (parentLocator.length > 0) {
333
+ const parentId = importedIds.get(parentLocator.join('/'));
334
+ const target = parentId === undefined ? undefined : placed.get(parentId);
335
+ if (target === undefined)
336
+ throw MemoryError.invalid(`Packaged node \`${view.id}\` has no parent.`);
337
+ const importedParent = imported.node(parentLocator);
338
+ const mergedParent = outcome.document.node(target);
339
+ if (importedParent === undefined || mergedParent === undefined)
340
+ throw new Error('placed locators stay valid');
341
+ parent = target;
342
+ level = mergedParent.level + (view.level - importedParent.level);
343
+ }
344
+ if (level > MAX_NODE_LEVEL)
345
+ throw MemoryError.invalid(`Packaged node \`${view.id}\` would sit below heading level ${MAX_NODE_LEVEL}.`);
346
+ const summary = { id: view.id, path: view.path };
347
+ if (mergedViews.some(existing => samePath(existing.path, view.path)))
348
+ outcome.duplicates.push(summary);
349
+ if (parent === undefined)
350
+ outcome.newSections.push(viewTitle(view));
351
+ outcome.added.push(summary);
352
+ const siblings = parent === undefined ? outcome.document.roots.length : outcome.document.node(parent)?.children.length ?? 0;
353
+ outcome.document.append(parent, node.detached(level, eol));
354
+ placed.set(view.id, [...parent ?? [], siblings]);
355
+ }
356
+ return outcome;
357
+ }
358
+ /** The local node an imported node stands for: a top-level section by its key, any other node by id. */
359
+ function matchingView(views, imported) {
360
+ const section = (view) => (view.locator.length === 1 ? view.meta?.section : undefined);
361
+ const key = section(imported);
362
+ if (key !== undefined) {
363
+ const found = views.find(view => section(view) === key);
364
+ if (found !== undefined)
365
+ return found;
366
+ }
367
+ return views.find(view => view.id === imported.id);
368
+ }
369
+ function samePath(left, right) {
370
+ return left.length === right.length && left.every((title, index) => title === right[index]);
371
+ }
372
+ function unusedNodeId(views) {
373
+ for (;;) {
374
+ const candidate = `m-${randomHex(8)}`;
375
+ if (!views.some(view => view.id === candidate))
376
+ return candidate;
377
+ }
378
+ }
379
+ /** The archive bytes: the manifest first, then the scope entries, deflated. */
380
+ export function writeArchive(manifest, entries) {
381
+ const encoder = new TextEncoder();
382
+ const files = { [MANIFEST_ENTRY]: encoder.encode(manifest) };
383
+ for (const [name, text] of entries)
384
+ files[name] = encoder.encode(text);
385
+ return zipSync(files, { level: 6 });
386
+ }
387
+ /** Read and validate a package: format and version, a closed set of entries matching the manifest, bounded UTF-8 files. */
388
+ function readPackage(archive, name) {
389
+ if (archive.byteLength > MAX_PACKAGE_BYTES)
390
+ throw MemoryError.invalid(`A memory package can be at most ${MAX_PACKAGE_BYTES} bytes.`);
391
+ const digest = sha256(archive);
392
+ const invalid = (detail) => new MemoryError('unsupported_format', `\`${name}\` is not a Vulcan memory package: ${detail}`);
393
+ const seen = new Set();
394
+ let raw;
395
+ try {
396
+ raw = unzipSync(archive, {
397
+ filter(entry) {
398
+ if (seen.size >= MAX_PACKAGE_PROJECTS + 3)
399
+ throw invalid('it has too many entries');
400
+ if (entry.name.endsWith('/') || !enclosed(entry.name) || entry.originalSize > MAX_FILE_BYTES || seen.has(entry.name)) {
401
+ throw invalid(`entry \`${entry.name}\` is not allowed`);
402
+ }
403
+ seen.add(entry.name);
404
+ return true;
405
+ },
406
+ });
407
+ }
408
+ catch (error) {
409
+ throw error instanceof MemoryError ? error : invalid(error instanceof Error ? error.message : String(error));
410
+ }
411
+ const decoder = new TextDecoder('utf-8', { fatal: true });
412
+ const files = new Map();
413
+ for (const [entry, bytes] of Object.entries(raw)) {
414
+ if (bytes.byteLength > MAX_FILE_BYTES)
415
+ throw invalid(`entry \`${entry}\` is not allowed`);
416
+ try {
417
+ files.set(entry, decoder.decode(bytes));
418
+ }
419
+ catch {
420
+ throw invalid(`entry \`${entry}\` is not UTF-8 text`);
421
+ }
422
+ }
423
+ const manifestText = files.get(MANIFEST_ENTRY);
424
+ if (manifestText === undefined)
425
+ throw invalid('it has no manifest');
426
+ files.delete(MANIFEST_ENTRY);
427
+ let header;
428
+ try {
429
+ header = JSON.parse(manifestText);
430
+ }
431
+ catch (error) {
432
+ throw invalid(error instanceof Error ? error.message : String(error));
433
+ }
434
+ if (!isRecord(header) || header['format'] !== PACKAGE_FORMAT || header['version'] !== EXPORT_FORMAT_VERSION) {
435
+ throw new MemoryError('unsupported_format', `\`${name}\` must be a ${PACKAGE_FORMAT} package of format version ${EXPORT_FORMAT_VERSION}.`);
436
+ }
437
+ const manifest = parseManifest(header, invalid);
438
+ const user = files.get(USER_ENTRY);
439
+ const space = files.get(SPACE_ENTRY);
440
+ files.delete(USER_ENTRY);
441
+ files.delete(SPACE_ENTRY);
442
+ if ((user !== undefined) !== manifest.user || (space !== undefined) !== manifest.space)
443
+ throw invalid('its scope files do not match the manifest');
444
+ const projects = new Map();
445
+ for (const project of manifest.projects) {
446
+ const entry = `${PROJECT_ENTRY_PREFIX}${project.id}.md`;
447
+ const text = /^[A-Za-z0-9-]+$/.test(project.id) ? files.get(entry) : undefined;
448
+ if (text === undefined)
449
+ throw invalid(`project \`${project.id}\` has no file`);
450
+ files.delete(entry);
451
+ if (projects.has(project.id))
452
+ throw invalid(`project \`${project.id}\` appears twice`);
453
+ projects.set(project.id, text);
454
+ }
455
+ const extra = [...files.keys()].sort(compare)[0];
456
+ if (extra !== undefined)
457
+ throw invalid(`entry \`${extra}\` is not allowed`);
458
+ return { sha256: digest, manifest, user, space, projects };
459
+ }
460
+ /** Whether an entry name stays inside the archive root, as `ZipFile::enclosed_name` requires. */
461
+ function enclosed(name) {
462
+ if (name.includes('\0') || name.startsWith('/') || name.startsWith('\\') || /^[A-Za-z]:/.test(name))
463
+ return false;
464
+ let depth = 0;
465
+ for (const part of name.split(/[/\\]/)) {
466
+ if (part === '..')
467
+ depth -= 1;
468
+ else if (part !== '' && part !== '.')
469
+ depth += 1;
470
+ if (depth < 0)
471
+ return false;
472
+ }
473
+ return true;
474
+ }
475
+ function parseManifest(value, invalid) {
476
+ const fields = ['format', 'version', 'exportedAt', 'user', 'space', 'projects'];
477
+ const unknown = Object.keys(value).find(key => !fields.includes(key));
478
+ if (unknown !== undefined)
479
+ throw invalid(`unknown field \`${unknown}\``);
480
+ const { exportedAt, user, space, projects } = value;
481
+ if (typeof exportedAt !== 'string' || typeof user !== 'boolean' || typeof space !== 'boolean' || !Array.isArray(projects)) {
482
+ throw invalid('the manifest fields have the wrong type');
483
+ }
484
+ return {
485
+ format: PACKAGE_FORMAT,
486
+ version: EXPORT_FORMAT_VERSION,
487
+ exportedAt,
488
+ user,
489
+ space,
490
+ projects: projects.map((project) => {
491
+ if (!isRecord(project) || Object.keys(project).some(key => !['id', 'name', 'paths', 'firstCommit'].includes(key))) {
492
+ throw invalid('a manifest project has unknown fields');
493
+ }
494
+ const { id, name, paths, firstCommit } = project;
495
+ if (typeof id !== 'string' || typeof name !== 'string' || !Array.isArray(paths) || !paths.every(item => typeof item === 'string')
496
+ || (firstCommit !== undefined && firstCommit !== null && typeof firstCommit !== 'string')) {
497
+ throw invalid('a manifest project has fields of the wrong type');
498
+ }
499
+ return { id, name, paths, ...typeof firstCommit === 'string' ? { firstCommit } : {} };
500
+ }),
501
+ };
502
+ }
503
+ /** Parse the page's project mappings, refusing unknown fields like serde's `deny_unknown_fields`. */
504
+ export function parseMappings(value) {
505
+ if (value === undefined || value === null)
506
+ return [];
507
+ if (!Array.isArray(value))
508
+ throw MemoryError.invalid('`projects` must be an array.');
509
+ return value.map((item) => {
510
+ if (!isRecord(item) || typeof item['packageProjectId'] !== 'string' || !isRecord(item['target'])
511
+ || Object.keys(item).some(key => key !== 'packageProjectId' && key !== 'target')) {
512
+ throw MemoryError.invalid('Each project mapping needs `packageProjectId` and `target`.');
513
+ }
514
+ const target = item['target'];
515
+ const keys = Object.keys(target);
516
+ let parsed;
517
+ if (target['action'] === 'link' && typeof target['memoryProjectId'] === 'string' && keys.length === 2) {
518
+ parsed = { action: 'link', memoryProjectId: target['memoryProjectId'] };
519
+ }
520
+ else if ((target['action'] === 'create' || target['action'] === 'ignore') && keys.length === 1) {
521
+ parsed = { action: target['action'] };
522
+ }
523
+ else {
524
+ throw MemoryError.invalid('A project target is `link` with `memoryProjectId`, `create` or `ignore`.');
525
+ }
526
+ return { packageProjectId: item['packageProjectId'], target: parsed };
527
+ });
528
+ }
529
+ /** Parse the page's conflict resolutions. */
530
+ export function parseResolutions(value) {
531
+ if (value === undefined || value === null)
532
+ return [];
533
+ if (!Array.isArray(value))
534
+ throw MemoryError.invalid('`resolutions` must be an array.');
535
+ return value.map((item) => {
536
+ if (!isRecord(item) || Object.keys(item).some(key => !['scope', 'packageProjectId', 'nodeId', 'choice'].includes(key))) {
537
+ throw MemoryError.invalid('A resolution has unknown fields.');
538
+ }
539
+ const { scope, packageProjectId, nodeId, choice } = item;
540
+ if (!isScope(scope) || typeof nodeId !== 'string' || (choice !== 'keep_local' && choice !== 'take_imported' && choice !== 'keep_both')
541
+ || (packageProjectId !== undefined && packageProjectId !== null && typeof packageProjectId !== 'string')) {
542
+ throw MemoryError.invalid('A resolution needs `scope`, `nodeId` and a `choice` of keep_local, take_imported or keep_both.');
543
+ }
544
+ return { scope, ...typeof packageProjectId === 'string' ? { packageProjectId } : {}, nodeId, choice };
545
+ });
546
+ }
547
+ function sha256(bytes) {
548
+ return createHash('sha256').update(bytes).digest('hex');
549
+ }
550
+ /** `2026-10-20T15:59Z` → `20261020T1559Z`; the trailing `Z` keeps the stamp readable as UTC. */
551
+ function fileStamp(now) {
552
+ return now.replace(/[^A-Za-z0-9]/g, '');
553
+ }
@@ -0,0 +1,56 @@
1
+ /** Memory projects and their bindings to host projects (`registry.rs`); stored as `markdown/registry.json`. */
2
+ export interface MemoryProject {
3
+ /** Random id that names the project's memory file. */
4
+ id: string;
5
+ /** Display name, initially the directory name. */
6
+ name: string;
7
+ /** Host project ids currently bound to this memory project. */
8
+ hostProjectIds: string[];
9
+ /** Every root path the project was opened from, oldest first. */
10
+ paths: string[];
11
+ firstCommit?: string;
12
+ memoryEnabled: boolean;
13
+ created: string;
14
+ updated: string;
15
+ }
16
+ /** An orphaned memory project offered for linking, with the reasons it ranks where it does. */
17
+ export interface OrphanCandidate {
18
+ readonly project: MemoryProject;
19
+ readonly reasons: readonly ('same_directory_name' | 'same_first_commit')[];
20
+ }
21
+ export declare const REGISTRY_FILE = "registry.json";
22
+ export declare class MemoryRegistry {
23
+ readonly projects: MemoryProject[];
24
+ /** Host projects whose orphan prompt the user dismissed. */
25
+ ignoredHostProjectIds: string[];
26
+ constructor(projects?: MemoryProject[],
27
+ /** Host projects whose orphan prompt the user dismissed. */
28
+ ignoredHostProjectIds?: string[]);
29
+ /** Parse registry file text; a format other than version 1 is refused. */
30
+ static parse(text: string, path: string): MemoryRegistry;
31
+ /** The versioned file text, without the final newline. */
32
+ toJson(): string;
33
+ projectForHost(hostProjectId: string): MemoryProject | undefined;
34
+ project(id: string): MemoryProject | undefined;
35
+ private projectOrThrow;
36
+ /** Create a memory project bound to a host project and return its id. */
37
+ createForHost(hostProjectId: string, rootPath: string, firstCommit: string | undefined, now: string): string;
38
+ /** Create a memory project bound to no host project, such as one restored from an export package. */
39
+ createUnbound(name: string, paths: string[], firstCommit: string | undefined, now: string): string;
40
+ /** Bind a host project to an existing memory project and record the path. */
41
+ link(memoryProjectId: string, hostProjectId: string, rootPath: string, now: string): void;
42
+ ignoreHost(hostProjectId: string): void;
43
+ setEnabled(memoryProjectId: string, enabled: boolean, now: string): void;
44
+ rename(memoryProjectId: string, name: string, now: string): void;
45
+ /** Orphaned projects ranked for a host project, or nothing once it is bound or dismissed. */
46
+ orphanCandidates(hostProjectId: string, rootPath: string, firstCommit: string | undefined, pathExists: (path: string) => boolean): OrphanCandidate[];
47
+ private unbindHost;
48
+ private unusedProjectId;
49
+ }
50
+ /** The last path component, accepting both separators. */
51
+ export declare function directoryName(path: string): string;
52
+ /** Lowercase hex of the given length, like the prefix of a simple UUID. */
53
+ export declare function randomHex(length: number): string;
54
+ /** Ordinal string comparison, the order Rust's `str::cmp` gives. */
55
+ export declare function compare(left: string, right: string): number;
56
+ export declare function isRecord(value: unknown): value is Record<string, unknown>;