@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,171 @@
1
+ /** Injection snapshot: full-text parts, catalogs and their budget usage (`snapshot.rs`). */
2
+ import { BUDGET_WARNING_PERCENT } from './constants.js';
3
+ import { lines, viewTitle } from './document.js';
4
+ import { BASE_SECTION_KEY, hasBaseSection } from './scope.js';
5
+ import { estimateTokens } from './tokens.js';
6
+ const PART_LABELS = {
7
+ user: 'user profile',
8
+ spaceBase: 'space base',
9
+ projectBase: 'project base',
10
+ spaceCatalog: 'space catalog',
11
+ projectCatalog: 'project catalog',
12
+ };
13
+ /** Whether content needs more than the budget. */
14
+ export function overBudget(usage) {
15
+ return usage.tokens > usage.budget;
16
+ }
17
+ /** Whether usage reached the warning ratio without exceeding the budget. */
18
+ export function nearBudget(usage) {
19
+ return !overBudget(usage) && usage.budget > 0 && usage.tokens * 100 >= usage.budget * BUDGET_WARNING_PERCENT;
20
+ }
21
+ /** One usage line for a write receipt. */
22
+ export function renderUsageLine(usage) {
23
+ const percent = usage.budget === 0 ? 0 : Math.floor(usage.tokens * 100 / usage.budget);
24
+ return `Injected ${PART_LABELS[usage.part]}: ${usage.tokens}/${usage.budget} tokens (${percent}%).`;
25
+ }
26
+ /** Usage of the full-text part of a scope, for write receipts. */
27
+ export function baseUsage(scope, document, budget) {
28
+ return fitFullText(fullTextEntries(document.views(scope), scope), fullTextPart(scope), budget).usage;
29
+ }
30
+ /** The injection snapshot of every visible scope. */
31
+ export function buildInjectionSnapshot(documents, budget) {
32
+ const sections = [];
33
+ const usage = [];
34
+ const scopes = [['user', documents.user, 'user'], ['space', documents.space, 'space']];
35
+ if (documents.project !== undefined)
36
+ scopes.push(['project', documents.project.document, `project ${documents.project.project.name}`]);
37
+ for (const [scope, document, label] of scopes) {
38
+ const views = document.views(scope);
39
+ const entries = fullTextEntries(views, scope);
40
+ if (entries.length > 0) {
41
+ const fitted = fitFullText(entries, fullTextPart(scope), budget);
42
+ sections.push(`${hasBaseSection(scope) ? `[${label} base]` : `[${label}]`}\n${fitted.text}`);
43
+ usage.push(fitted.usage);
44
+ }
45
+ if (hasBaseSection(scope)) {
46
+ const catalog = fitCatalog(views, scope === 'space' ? 'spaceCatalog' : 'projectCatalog', budget);
47
+ if (catalog !== undefined) {
48
+ const updated = latestUpdate(views);
49
+ sections.push(`[${label} catalog${updated === undefined ? '' : ` · updated ${updated}`}]\n${catalog.text}`);
50
+ usage.push(catalog.usage);
51
+ }
52
+ }
53
+ }
54
+ if (sections.length === 0)
55
+ return { content: '', usage };
56
+ return {
57
+ content: `On conflict, project memory overrides space memory, which overrides the user profile. Catalog lines list titles only; read a node with memory_search before relying on it.\n\n${sections.join('\n\n')}`,
58
+ usage,
59
+ };
60
+ }
61
+ function fullTextPart(scope) {
62
+ switch (scope) {
63
+ case 'user': return 'user';
64
+ case 'space': return 'spaceBase';
65
+ case 'project': return 'projectBase';
66
+ }
67
+ }
68
+ /** `[title path, rendered entry]` for every non-empty node injected in full. */
69
+ function fullTextEntries(views, scope) {
70
+ const base = hasBaseSection(scope);
71
+ return views
72
+ .filter(view => view.body !== '')
73
+ .filter(view => !base || view.section === BASE_SECTION_KEY)
74
+ .map(view => {
75
+ const path = base ? view.path.slice(1).join(' › ') : view.path.join(' › ');
76
+ const label = path === '' ? view.path.join(' › ') : path;
77
+ const entry = view.body.includes('\n')
78
+ ? `- ${label}:\n${lines(view.body).map(line => line === '' ? '' : ` ${line}`).join('\n')}`
79
+ : `- ${label}: ${view.body}`;
80
+ return [label, entry];
81
+ });
82
+ }
83
+ /** Keep whole entries until the budget is reached and list the omitted titles. */
84
+ function fitFullText(entries, part, budget) {
85
+ const limit = budget[part];
86
+ const total = estimateTokens(entries.map(([, entry]) => entry).join('\n'));
87
+ const kept = [];
88
+ const omitted = [];
89
+ let used = 0;
90
+ for (const [label, entry] of entries) {
91
+ const cost = estimateTokens(entry) + 1;
92
+ if (omitted.length === 0 && used + cost <= limit) {
93
+ used += cost;
94
+ kept.push(entry);
95
+ }
96
+ else {
97
+ omitted.push(label);
98
+ }
99
+ }
100
+ let text = kept.join('\n');
101
+ if (omitted.length > 0) {
102
+ if (text !== '')
103
+ text += '\n';
104
+ text += `(over budget; not shown, read with memory_search: ${omitted.join(' · ')})`;
105
+ }
106
+ return { text, usage: { part, tokens: total, budget: limit, omitted } };
107
+ }
108
+ /** The catalog at the deepest level that fits, degrading H3 → H2 → section counts. */
109
+ function fitCatalog(views, part, budget) {
110
+ const roots = views
111
+ .filter(view => view.locator.length === 1)
112
+ .filter(view => view.section !== BASE_SECTION_KEY)
113
+ .filter(view => view.descendants > 0 || view.body !== '');
114
+ if (roots.length === 0)
115
+ return undefined;
116
+ const limit = budget[part];
117
+ const full = renderCatalog(views, roots, 3);
118
+ const tokens = estimateTokens(full);
119
+ for (const [depth, dropped] of [[3, undefined], [2, 'H3'], [1, 'H2 and H3']]) {
120
+ let text = depth === 3 ? full : renderCatalog(views, roots, depth);
121
+ if (estimateTokens(text) <= limit) {
122
+ if (dropped !== undefined)
123
+ text += '\n(catalog shortened; list it with memory_outline)';
124
+ return { text, usage: { part, tokens, budget: limit, omitted: dropped === undefined ? [] : [dropped] } };
125
+ }
126
+ }
127
+ const output = [];
128
+ let used = 0;
129
+ for (const root of roots) {
130
+ const line = catalogLine(views, root, 1);
131
+ const cost = estimateTokens(line) + 1;
132
+ if (used + cost > limit)
133
+ break;
134
+ used += cost;
135
+ output.push(line);
136
+ }
137
+ output.push(`(catalog shortened, ${roots.length - output.length} more section(s); list them with memory_outline)`);
138
+ return { text: output.join('\n'), usage: { part, tokens, budget: limit, omitted: ['sections'] } };
139
+ }
140
+ function renderCatalog(views, roots, depth) {
141
+ return roots.map(root => catalogLine(views, root, depth)).join('\n');
142
+ }
143
+ /** One catalog line: `Section(count) › child [grandchildren] · child`. */
144
+ function catalogLine(views, root, depth) {
145
+ const line = `${viewTitle(root)}(${root.descendants})`;
146
+ if (depth < 2)
147
+ return line;
148
+ const children = directChildren(views, root);
149
+ if (children.length === 0)
150
+ return line;
151
+ const rendered = children.map(child => {
152
+ if (depth < 3)
153
+ return viewTitle(child);
154
+ const grandchildren = directChildren(views, child).map(viewTitle);
155
+ return grandchildren.length === 0 ? viewTitle(child) : `${viewTitle(child)} [${grandchildren.join(', ')}]`;
156
+ });
157
+ return `${line} › ${rendered.join(' · ')}`;
158
+ }
159
+ function directChildren(views, parent) {
160
+ return views.filter(view => view.locator.length === parent.locator.length + 1 && parent.locator.every((index, position) => view.locator[position] === index));
161
+ }
162
+ /** The date part of the newest update time in a scope. */
163
+ function latestUpdate(views) {
164
+ let latest;
165
+ for (const view of views) {
166
+ const updated = view.meta?.updated;
167
+ if (updated !== undefined && (latest === undefined || updated > latest))
168
+ latest = updated;
169
+ }
170
+ return latest === undefined ? undefined : [...latest].slice(0, 10).join('');
171
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * File layout, project resolution and atomic commits of the memory store (`store.rs`).
3
+ *
4
+ * Every function here is synchronous, so within one process two writes can never interleave and
5
+ * no lock object is needed; the commit still rechecks each file against the text it was read
6
+ * from, which catches edits made by other processes or by hand.
7
+ */
8
+ import { MemoryDocument } from './document.js';
9
+ import { MemoryRegistry, type MemoryProject } from './registry.js';
10
+ import type { MemoryScope } from './scope.js';
11
+ export declare const MARKDOWN_DIRECTORY = "markdown";
12
+ /** The project a request runs in, as known to the host. */
13
+ export interface ProjectContext {
14
+ /** Host project id derived from the project root. */
15
+ readonly hostProjectId?: string;
16
+ /** Project root path. */
17
+ readonly rootPath?: string;
18
+ /** Explicit memory project chosen in the memory page, overriding the host binding. */
19
+ readonly memoryProjectId?: string;
20
+ }
21
+ /** The memory project a context addresses: the explicit choice or the host binding. */
22
+ export declare function boundProject(context: ProjectContext, registry: MemoryRegistry): MemoryProject | undefined;
23
+ /** The documents of every scope visible to one request. */
24
+ export interface ScopeDocuments {
25
+ readonly user: MemoryDocument;
26
+ readonly space: MemoryDocument;
27
+ /** Bound memory project and its document, when the host project is bound. */
28
+ readonly project?: {
29
+ readonly project: MemoryProject;
30
+ readonly document: MemoryDocument;
31
+ };
32
+ }
33
+ export declare function scopeDocument(documents: ScopeDocuments, scope: MemoryScope): MemoryDocument | undefined;
34
+ /** One file change prepared for an atomic commit. */
35
+ export interface PreparedFile {
36
+ readonly path: string;
37
+ /** Exact text read before the change; `undefined` when the file did not exist. */
38
+ readonly original: string | undefined;
39
+ readonly text: string;
40
+ }
41
+ /** Handle to the memory files under `<dshHome>/vulcan-memory`; nothing is created on disk until a write. */
42
+ export declare class MarkdownMemoryStore {
43
+ readonly memoryDirectory: string;
44
+ constructor(memoryDirectory: string);
45
+ get markdownDirectory(): string;
46
+ /** The file path of a scope; the project scope needs a memory project id. */
47
+ scopePath(scope: MemoryScope, memoryProjectId: string | undefined): string;
48
+ /** The registry; a missing file is empty. */
49
+ registry(): MemoryRegistry;
50
+ /** Change the registry and save it when the change succeeds. */
51
+ updateRegistry<T>(change: (registry: MemoryRegistry) => T): T;
52
+ /** One scope document; a missing file is an empty document. */
53
+ load(scope: MemoryScope, memoryProjectId: string | undefined): MemoryDocument;
54
+ /** Every scope visible to a project context. */
55
+ loadScopes(context: ProjectContext): ScopeDocuments;
56
+ /** The exact text of one scope file for whole-file editing. */
57
+ readFileText(scope: MemoryScope, memoryProjectId: string | undefined): string;
58
+ /** Replace one whole scope file when it still holds the text the editor started from. */
59
+ replaceFileText(scope: MemoryScope, memoryProjectId: string | undefined, expected: string, text: string): void;
60
+ }
61
+ /** A file's exact text, or `undefined` when it does not exist. */
62
+ export declare function readOptional(file: string): string | undefined;
63
+ /** Write text through a sibling temporary file and an atomic rename. */
64
+ export declare function writeAtomically(file: string, text: string): void;
65
+ /** Retry `operation` only for transient Windows replacement failures. */
66
+ export declare function retryFileMutation(operation: () => void): void;
67
+ /** Atomically replace `destination` with `temporary`, retrying transient Windows sharing failures. */
68
+ export declare function replaceFile(temporary: string, destination: string): void;
69
+ /** Commit `files`; a failure restores completed replacements or reports preserved recovery copies. */
70
+ export declare function commitFiles(files: readonly PreparedFile[]): void;
71
+ /** The real commit and rollback algorithm, with a replacement operation tests can fault. */
72
+ export declare function commitFilesWith(files: readonly PreparedFile[], replace: (temporary: string, destination: string) => void): void;
@@ -0,0 +1,250 @@
1
+ /**
2
+ * File layout, project resolution and atomic commits of the memory store (`store.rs`).
3
+ *
4
+ * Every function here is synchronous, so within one process two writes can never interleave and
5
+ * no lock object is needed; the commit still rechecks each file against the text it was read
6
+ * from, which catches edits made by other processes or by hand.
7
+ */
8
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
9
+ import path from 'node:path';
10
+ import { MAX_FILE_BYTES, byteLength } from './constants.js';
11
+ import { MemoryDocument } from './document.js';
12
+ import { MemoryError } from './errors.js';
13
+ import { MemoryRegistry, REGISTRY_FILE, randomHex } from './registry.js';
14
+ /** Attempts allowed for Windows sharing violations during replacement or rollback. */
15
+ const WINDOWS_MUTATION_ATTEMPTS = 5;
16
+ /** Delay between Windows sharing-violation attempts; never applied to other errors or platforms. */
17
+ const WINDOWS_MUTATION_RETRY_MS = 25;
18
+ /** Node's codes for Windows os errors 5 (access denied), 32 (sharing) and 33 (lock violation). */
19
+ const WINDOWS_TRANSIENT_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
20
+ export const MARKDOWN_DIRECTORY = 'markdown';
21
+ /** The memory project a context addresses: the explicit choice or the host binding. */
22
+ export function boundProject(context, registry) {
23
+ if (context.memoryProjectId !== undefined) {
24
+ const project = registry.project(context.memoryProjectId);
25
+ if (project === undefined)
26
+ throw new MemoryError('project_unavailable', `Memory project \`${context.memoryProjectId}\` does not exist.`);
27
+ return project;
28
+ }
29
+ return context.hostProjectId === undefined ? undefined : registry.projectForHost(context.hostProjectId);
30
+ }
31
+ export function scopeDocument(documents, scope) {
32
+ switch (scope) {
33
+ case 'user': return documents.user;
34
+ case 'space': return documents.space;
35
+ case 'project': return documents.project?.document;
36
+ }
37
+ }
38
+ /** Handle to the memory files under `<dshHome>/vulcan-memory`; nothing is created on disk until a write. */
39
+ export class MarkdownMemoryStore {
40
+ memoryDirectory;
41
+ constructor(memoryDirectory) {
42
+ this.memoryDirectory = memoryDirectory;
43
+ }
44
+ get markdownDirectory() {
45
+ return path.join(this.memoryDirectory, MARKDOWN_DIRECTORY);
46
+ }
47
+ /** The file path of a scope; the project scope needs a memory project id. */
48
+ scopePath(scope, memoryProjectId) {
49
+ const directory = this.markdownDirectory;
50
+ switch (scope) {
51
+ case 'user': return path.join(directory, 'user.md');
52
+ case 'space': return path.join(directory, 'space.md');
53
+ case 'project': {
54
+ if (memoryProjectId === undefined) {
55
+ throw new MemoryError('project_unavailable', 'This conversation has no project, so project memory is unavailable.');
56
+ }
57
+ if (!/^[A-Za-z0-9-]+$/.test(memoryProjectId))
58
+ throw MemoryError.invalid(`\`${memoryProjectId}\` is not a memory project id.`);
59
+ return path.join(directory, 'projects', `${memoryProjectId}.md`);
60
+ }
61
+ }
62
+ }
63
+ /** The registry; a missing file is empty. */
64
+ registry() {
65
+ const file = path.join(this.markdownDirectory, REGISTRY_FILE);
66
+ const text = readOptional(file);
67
+ return text === undefined ? new MemoryRegistry() : MemoryRegistry.parse(text, file);
68
+ }
69
+ /** Change the registry and save it when the change succeeds. */
70
+ updateRegistry(change) {
71
+ const registry = this.registry();
72
+ const value = change(registry);
73
+ writeAtomically(path.join(this.markdownDirectory, REGISTRY_FILE), `${registry.toJson()}\n`);
74
+ return value;
75
+ }
76
+ /** One scope document; a missing file is an empty document. */
77
+ load(scope, memoryProjectId) {
78
+ const text = readOptional(this.scopePath(scope, memoryProjectId));
79
+ return text === undefined ? MemoryDocument.empty() : MemoryDocument.parse(text);
80
+ }
81
+ /** Every scope visible to a project context. */
82
+ loadScopes(context) {
83
+ const project = boundProject(context, this.registry());
84
+ return {
85
+ user: this.load('user', undefined),
86
+ space: this.load('space', undefined),
87
+ ...project === undefined ? {} : { project: { project, document: this.load('project', project.id) } },
88
+ };
89
+ }
90
+ /** The exact text of one scope file for whole-file editing. */
91
+ readFileText(scope, memoryProjectId) {
92
+ return readOptional(this.scopePath(scope, memoryProjectId)) ?? '';
93
+ }
94
+ /** Replace one whole scope file when it still holds the text the editor started from. */
95
+ replaceFileText(scope, memoryProjectId, expected, text) {
96
+ if (byteLength(text) > MAX_FILE_BYTES)
97
+ throw MemoryError.invalid(`A memory file can hold at most ${MAX_FILE_BYTES} bytes.`);
98
+ const file = this.scopePath(scope, memoryProjectId);
99
+ const original = readOptional(file);
100
+ if ((original ?? '') !== expected)
101
+ throw MemoryError.concurrent(file);
102
+ commitFiles([{ path: file, original, text }]);
103
+ }
104
+ }
105
+ /** A file's exact text, or `undefined` when it does not exist. */
106
+ export function readOptional(file) {
107
+ try {
108
+ return readFileSync(file, 'utf8');
109
+ }
110
+ catch (error) {
111
+ if (error.code === 'ENOENT')
112
+ return undefined;
113
+ throw MemoryError.io(file, 'read', error);
114
+ }
115
+ }
116
+ /** Write text through a sibling temporary file and an atomic rename. */
117
+ export function writeAtomically(file, text) {
118
+ const temporary = writeTemporary(file, text);
119
+ try {
120
+ replaceFile(temporary, file);
121
+ }
122
+ catch (error) {
123
+ rmSync(temporary, { force: true });
124
+ throw MemoryError.io(file, 'replaced', error);
125
+ }
126
+ }
127
+ function writeTemporary(file, text) {
128
+ const parent = path.dirname(file);
129
+ try {
130
+ mkdirSync(parent, { recursive: true });
131
+ }
132
+ catch (error) {
133
+ throw MemoryError.io(parent, 'created', error);
134
+ }
135
+ const temporary = path.join(parent, `.${path.basename(file)}.${randomHex(32)}.tmp`);
136
+ try {
137
+ writeFileSync(temporary, text, 'utf8');
138
+ }
139
+ catch (error) {
140
+ rmSync(temporary, { force: true });
141
+ throw MemoryError.io(temporary, 'written', error);
142
+ }
143
+ return temporary;
144
+ }
145
+ /** Retry `operation` only for transient Windows replacement failures. */
146
+ export function retryFileMutation(operation) {
147
+ const windows = process.platform === 'win32';
148
+ const attempts = windows ? WINDOWS_MUTATION_ATTEMPTS : 1;
149
+ for (let attempt = 0;; attempt += 1) {
150
+ try {
151
+ operation();
152
+ return;
153
+ }
154
+ catch (error) {
155
+ const code = error.code;
156
+ if (!windows || code === undefined || !WINDOWS_TRANSIENT_CODES.has(code) || attempt + 1 >= attempts)
157
+ throw error;
158
+ sleepSync(WINDOWS_MUTATION_RETRY_MS);
159
+ }
160
+ }
161
+ }
162
+ function sleepSync(milliseconds) {
163
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
164
+ }
165
+ /** Atomically replace `destination` with `temporary`, retrying transient Windows sharing failures. */
166
+ export function replaceFile(temporary, destination) {
167
+ retryFileMutation(() => renameSync(temporary, destination));
168
+ }
169
+ /** Commit `files`; a failure restores completed replacements or reports preserved recovery copies. */
170
+ export function commitFiles(files) {
171
+ commitFilesWith(files, replaceFile);
172
+ }
173
+ /** The real commit and rollback algorithm, with a replacement operation tests can fault. */
174
+ export function commitFilesWith(files, replace) {
175
+ // Stage both directions before publishing anything, so rollback never needs to recreate original bytes.
176
+ const staged = [];
177
+ // Recovery copies handed to the user survive the final cleanup.
178
+ const detached = new Set();
179
+ try {
180
+ for (const file of files) {
181
+ if (readOptional(file.path) !== file.original)
182
+ throw MemoryError.concurrent(file.path);
183
+ const prepared = { replacement: writeTemporary(file.path, file.text), original: undefined };
184
+ staged.push(prepared);
185
+ if (file.original !== undefined)
186
+ prepared.original = writeTemporary(file.path, file.original);
187
+ }
188
+ files.forEach((file, index) => {
189
+ const stage = staged[index];
190
+ let failure;
191
+ try {
192
+ // Recheck immediately before replacement to detect external edits during staging.
193
+ if (readOptional(file.path) !== file.original)
194
+ throw MemoryError.concurrent(file.path);
195
+ try {
196
+ replace(stage.replacement, file.path);
197
+ }
198
+ catch (error) {
199
+ throw MemoryError.io(file.path, 'replaced', error);
200
+ }
201
+ }
202
+ catch (error) {
203
+ failure = error instanceof MemoryError ? error : MemoryError.io(file.path, 'replaced', error);
204
+ }
205
+ if (failure === undefined)
206
+ return;
207
+ const inconsistent = [];
208
+ const recovery = [];
209
+ for (let completed = index - 1; completed >= 0; completed -= 1) {
210
+ // Never overwrite an unrelated external edit while undoing this transaction.
211
+ const previous = files[completed];
212
+ const previousStage = staged[completed];
213
+ let restored = false;
214
+ try {
215
+ const current = readOptional(previous.path);
216
+ if (current === previous.original) {
217
+ restored = true;
218
+ }
219
+ else if (current === previous.text) {
220
+ if (previousStage.original !== undefined)
221
+ replace(previousStage.original, previous.path);
222
+ else
223
+ retryFileMutation(() => rmSync(previous.path));
224
+ restored = true;
225
+ }
226
+ }
227
+ catch {
228
+ restored = false;
229
+ }
230
+ if (!restored) {
231
+ inconsistent.push(previous.path);
232
+ if (previousStage.original !== undefined) {
233
+ recovery.push(previousStage.original);
234
+ detached.add(previousStage.original);
235
+ }
236
+ }
237
+ }
238
+ if (inconsistent.length === 0)
239
+ throw failure;
240
+ throw new MemoryError('rollback_incomplete', `Memory commit failed: ${failure.message}. Rollback is incomplete for ${JSON.stringify(inconsistent)}. Original recovery files: ${JSON.stringify(recovery)}. Restore these files before retrying.`);
241
+ });
242
+ }
243
+ finally {
244
+ for (const stage of staged) {
245
+ rmSync(stage.replacement, { force: true });
246
+ if (stage.original !== undefined && !detached.has(stage.original))
247
+ rmSync(stage.original, { force: true });
248
+ }
249
+ }
250
+ }
@@ -0,0 +1,6 @@
1
+ /** Minute-precision UTC timestamps written into node metadata (`timestamp.rs`). */
2
+ /** `YYYY-MM-DDTHH:MMZ` for an instant. */
3
+ export declare function formatUtcMinute(instant: Date): string;
4
+ export declare function nowUtcMinute(): string;
5
+ /** Whether text has the exact `YYYY-MM-DDTHH:MMZ` shape. */
6
+ export declare function isUtcMinute(text: string): boolean;
@@ -0,0 +1,12 @@
1
+ /** Minute-precision UTC timestamps written into node metadata (`timestamp.rs`). */
2
+ /** `YYYY-MM-DDTHH:MMZ` for an instant. */
3
+ export function formatUtcMinute(instant) {
4
+ return `${instant.toISOString().slice(0, 16)}Z`;
5
+ }
6
+ export function nowUtcMinute() {
7
+ return formatUtcMinute(new Date());
8
+ }
9
+ /** Whether text has the exact `YYYY-MM-DDTHH:MMZ` shape. */
10
+ export function isUtcMinute(text) {
11
+ return /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}Z$/.test(text);
12
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Mixed-language token estimate, the same as VulcanCode's
3
+ * `vulcan-context::estimate_mixed_language_text_tokens`: every ASCII run counts a quarter token per
4
+ * character rounded up, CJK characters one token each, other characters two.
5
+ */
6
+ export declare function estimateTokens(text: string): number;
7
+ /** Whether a code point belongs to the common CJK text ranges. */
8
+ export declare function isCjkCharacter(code: number): boolean;
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Mixed-language token estimate, the same as VulcanCode's
3
+ * `vulcan-context::estimate_mixed_language_text_tokens`: every ASCII run counts a quarter token per
4
+ * character rounded up, CJK characters one token each, other characters two.
5
+ */
6
+ export function estimateTokens(text) {
7
+ let asciiRun = 0;
8
+ let tokens = 0;
9
+ for (const character of text) {
10
+ const code = character.codePointAt(0) ?? 0;
11
+ if (code < 0x80) {
12
+ asciiRun += 1;
13
+ continue;
14
+ }
15
+ if (asciiRun > 0) {
16
+ tokens += Math.ceil(asciiRun / 4);
17
+ asciiRun = 0;
18
+ }
19
+ tokens += isCjkCharacter(code) ? 1 : 2;
20
+ }
21
+ if (asciiRun > 0)
22
+ tokens += Math.ceil(asciiRun / 4);
23
+ return tokens;
24
+ }
25
+ /** Whether a code point belongs to the common CJK text ranges. */
26
+ export function isCjkCharacter(code) {
27
+ return (code >= 0x3400 && code <= 0x4DBF)
28
+ || (code >= 0x4E00 && code <= 0x9FFF)
29
+ || (code >= 0xF900 && code <= 0xFAFF)
30
+ || (code >= 0x3040 && code <= 0x30FF)
31
+ || (code >= 0xAC00 && code <= 0xD7AF)
32
+ || (code >= 0x20000 && code <= 0x2A6DF)
33
+ || (code >= 0x2A700 && code <= 0x2B73F)
34
+ || (code >= 0x2B740 && code <= 0x2B81F)
35
+ || (code >= 0x2B820 && code <= 0x2CEAF)
36
+ || (code >= 0x2CEB0 && code <= 0x2EBEF)
37
+ || (code >= 0x30000 && code <= 0x3134F);
38
+ }