@kontextmind/kxm 0.6.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 (227) hide show
  1. package/.claude-plugin/marketplace.json +19 -0
  2. package/.kxm/README.md +14 -0
  3. package/.kxm/assets/README.md +5 -0
  4. package/.kxm/assets/retrospectives/README.md +5 -0
  5. package/.kxm/config/README.md +5 -0
  6. package/.kxm/config/agents.json +43 -0
  7. package/.kxm/config/env.example +56 -0
  8. package/.kxm/config/update.example.yaml +9 -0
  9. package/.kxm/config/workflows/fix.json +160 -0
  10. package/.kxm/config/workflows/jira-development.json +116 -0
  11. package/.kxm/config/workflows/provenance-quorum.json +150 -0
  12. package/.kxm/config/workflows/v04-dogfood.json +72 -0
  13. package/CHANGELOG.md +465 -0
  14. package/LICENSE +21 -0
  15. package/README.md +306 -0
  16. package/SECURITY.md +72 -0
  17. package/docs/README.md +48 -0
  18. package/docs/agent-communication-envelopes-and-gates.md +553 -0
  19. package/docs/architecture.md +242 -0
  20. package/docs/assignment-runner.md +241 -0
  21. package/docs/configuration.md +361 -0
  22. package/docs/continuous-improvement.md +114 -0
  23. package/docs/getting-started.md +253 -0
  24. package/docs/kxm-handbook.md +1090 -0
  25. package/docs/operations.md +205 -0
  26. package/docs/provenance-gates.md +291 -0
  27. package/docs/skills.md +45 -0
  28. package/docs/templates/README.md +95 -0
  29. package/docs/templates/adr.md +88 -0
  30. package/docs/templates/architecture.md +120 -0
  31. package/docs/templates/bug-fix.md +109 -0
  32. package/docs/templates/feature.md +108 -0
  33. package/docs/templates/handoff.md +72 -0
  34. package/docs/templates/postmortem.md +77 -0
  35. package/docs/templates/research.md +100 -0
  36. package/docs/templates/review.md +85 -0
  37. package/docs/templates/runbook.md +73 -0
  38. package/docs/templates/test-plan.md +87 -0
  39. package/docs/templates/test-report.md +72 -0
  40. package/docs/test-matrix.md +121 -0
  41. package/docs/troubleshooting.md +249 -0
  42. package/docs/vnext/README.md +62 -0
  43. package/docs/vnext/architecture.md +185 -0
  44. package/docs/vnext/effects-and-recovery.md +172 -0
  45. package/docs/vnext/lifecycles.md +235 -0
  46. package/docs/vnext/migration.md +220 -0
  47. package/docs/vnext/routing.md +184 -0
  48. package/docs/vnext/synchronization.md +172 -0
  49. package/docs/vnext/terminology.md +240 -0
  50. package/docs/vnext/validation.md +335 -0
  51. package/docs/webhook-workflows.md +240 -0
  52. package/docs/workflow-guide.md +1150 -0
  53. package/examples/README.md +102 -0
  54. package/examples/provenance-workflow.json +40 -0
  55. package/examples/requester.ts +30 -0
  56. package/examples/reviewer-agent.ts +29 -0
  57. package/examples/roundtrip.ts +46 -0
  58. package/examples/vnext/.kxm/agents/coordinator.yaml +16 -0
  59. package/examples/vnext/.kxm/agents/critic-1.yaml +16 -0
  60. package/examples/vnext/.kxm/agents/critic-2.yaml +15 -0
  61. package/examples/vnext/.kxm/agents/critic-3.yaml +15 -0
  62. package/examples/vnext/.kxm/agents/implementer.yaml +15 -0
  63. package/examples/vnext/.kxm/agents/planner.yaml +13 -0
  64. package/examples/vnext/.kxm/agents/reproducer.yaml +15 -0
  65. package/examples/vnext/.kxm/agents/reviewer.yaml +15 -0
  66. package/examples/vnext/.kxm/gates.yaml +8 -0
  67. package/examples/vnext/.kxm/models/critic-claude.yaml +11 -0
  68. package/examples/vnext/.kxm/models/critic-gemini.yaml +11 -0
  69. package/examples/vnext/.kxm/models/critic-grok.yaml +12 -0
  70. package/examples/vnext/.kxm/models/implementation.yaml +14 -0
  71. package/examples/vnext/.kxm/models/primary.yaml +17 -0
  72. package/examples/vnext/.kxm/prices.yaml +111 -0
  73. package/examples/vnext/.kxm/project/env.yaml +7 -0
  74. package/examples/vnext/.kxm/project.yaml +32 -0
  75. package/examples/vnext/.kxm/repo/repo.yaml +8 -0
  76. package/examples/vnext/.kxm/workflows/default.yaml +92 -0
  77. package/examples/vnext/.kxm/workflows/fix.yaml +376 -0
  78. package/examples/vnext/.kxm/workflows/improve.yaml +57 -0
  79. package/examples/vnext/README.md +53 -0
  80. package/examples/vnext/records/assignment-result-recorded.json +63 -0
  81. package/examples/vnext/records/assignment-result.json +46 -0
  82. package/examples/vnext/records/context-candidate.json +42 -0
  83. package/examples/vnext/records/delivery-manifest.json +66 -0
  84. package/examples/vnext/records/effect-uncertainty-resolved-sync.json +67 -0
  85. package/examples/vnext/records/effect-uncertainty-resolved.json +62 -0
  86. package/examples/vnext/records/run-created.json +54 -0
  87. package/examples/vnext/records/sync-event.json +65 -0
  88. package/examples/vnext/repositories/api/.kxm/repo/env.yaml +7 -0
  89. package/examples/vnext/repositories/api/.kxm/repo/repo.yaml +8 -0
  90. package/examples/vnext/repositories/web/.kxm/repo/repo.yaml +8 -0
  91. package/examples/workflow-signal.ts +63 -0
  92. package/package.json +129 -0
  93. package/plugins/kxm/.claude-plugin/plugin.json +73 -0
  94. package/plugins/kxm/.mcp.json +19 -0
  95. package/plugins/kxm/README.md +93 -0
  96. package/plugins/kxm/dist/cli.js +42853 -0
  97. package/plugins/kxm/dist/client.js +416 -0
  98. package/plugins/kxm/dist/core.js +1823 -0
  99. package/plugins/kxm/dist/extension.js +3797 -0
  100. package/plugins/kxm/dist/mcp-server.js +17104 -0
  101. package/plugins/kxm/dist/runtime.js +23361 -0
  102. package/plugins/kxm/dist/server.js +13640 -0
  103. package/plugins/kxm/dist/vnext-runtime-supervisor.js +21109 -0
  104. package/plugins/kxm/package.json +12 -0
  105. package/plugins/kxm/skills/kxm/SKILL.md +97 -0
  106. package/plugins/kxm/skills/kxm/references/protocol.md +103 -0
  107. package/plugins/kxm/skills/kxm-session/SKILL.md +53 -0
  108. package/plugins/kxm/src/arbiter.ts +355 -0
  109. package/plugins/kxm/src/artifacts-exist.ts +62 -0
  110. package/plugins/kxm/src/autocomplete.ts +236 -0
  111. package/plugins/kxm/src/cli.ts +3707 -0
  112. package/plugins/kxm/src/client.ts +614 -0
  113. package/plugins/kxm/src/commands.ts +1063 -0
  114. package/plugins/kxm/src/config.ts +290 -0
  115. package/plugins/kxm/src/context/providers.ts +101 -0
  116. package/plugins/kxm/src/context-packet.ts +332 -0
  117. package/plugins/kxm/src/context.ts +499 -0
  118. package/plugins/kxm/src/core.ts +6 -0
  119. package/plugins/kxm/src/database.ts +563 -0
  120. package/plugins/kxm/src/diagnostics.ts +184 -0
  121. package/plugins/kxm/src/envelope.ts +118 -0
  122. package/plugins/kxm/src/extension.ts +895 -0
  123. package/plugins/kxm/src/external-effects.ts +299 -0
  124. package/plugins/kxm/src/github-watch.ts +255 -0
  125. package/plugins/kxm/src/hub-binding.ts +160 -0
  126. package/plugins/kxm/src/hub.ts +2502 -0
  127. package/plugins/kxm/src/improve.ts +383 -0
  128. package/plugins/kxm/src/inbox.ts +10 -0
  129. package/plugins/kxm/src/kxm-install-kind.ts +113 -0
  130. package/plugins/kxm/src/kxm-update-config.ts +39 -0
  131. package/plugins/kxm/src/kxm-update.ts +238 -0
  132. package/plugins/kxm/src/local-snapshot.ts +406 -0
  133. package/plugins/kxm/src/logger.ts +198 -0
  134. package/plugins/kxm/src/mcp-server.ts +143 -0
  135. package/plugins/kxm/src/memory.ts +385 -0
  136. package/plugins/kxm/src/nous-pi.ts +287 -0
  137. package/plugins/kxm/src/nous-provider.ts +729 -0
  138. package/plugins/kxm/src/price-calc.ts +87 -0
  139. package/plugins/kxm/src/prices.ts +121 -0
  140. package/plugins/kxm/src/protocol.ts +172 -0
  141. package/plugins/kxm/src/recovery.ts +211 -0
  142. package/plugins/kxm/src/redact.ts +26 -0
  143. package/plugins/kxm/src/retrospective.ts +400 -0
  144. package/plugins/kxm/src/routing.ts +830 -0
  145. package/plugins/kxm/src/runtime.ts +9 -0
  146. package/plugins/kxm/src/server.ts +117 -0
  147. package/plugins/kxm/src/session-work.ts +571 -0
  148. package/plugins/kxm/src/session.ts +184 -0
  149. package/plugins/kxm/src/skills.ts +535 -0
  150. package/plugins/kxm/src/state.ts +326 -0
  151. package/plugins/kxm/src/store.ts +637 -0
  152. package/plugins/kxm/src/studio-layout.ts +268 -0
  153. package/plugins/kxm/src/suggest.ts +162 -0
  154. package/plugins/kxm/src/task-manager.ts +244 -0
  155. package/plugins/kxm/src/telemetry.ts +116 -0
  156. package/plugins/kxm/src/tui.ts +1046 -0
  157. package/plugins/kxm/src/vnext-bindings.ts +403 -0
  158. package/plugins/kxm/src/vnext-config.ts +1646 -0
  159. package/plugins/kxm/src/vnext-engine-artifacts.ts +86 -0
  160. package/plugins/kxm/src/vnext-engine-command.ts +533 -0
  161. package/plugins/kxm/src/vnext-engine-compile.ts +722 -0
  162. package/plugins/kxm/src/vnext-engine-evidence.ts +273 -0
  163. package/plugins/kxm/src/vnext-engine-fold.ts +1400 -0
  164. package/plugins/kxm/src/vnext-engine-gate-records.ts +583 -0
  165. package/plugins/kxm/src/vnext-engine-plan.ts +717 -0
  166. package/plugins/kxm/src/vnext-engine.ts +2458 -0
  167. package/plugins/kxm/src/vnext-gate-hash.ts +10 -0
  168. package/plugins/kxm/src/vnext-harness.ts +1142 -0
  169. package/plugins/kxm/src/vnext-init.ts +430 -0
  170. package/plugins/kxm/src/vnext-migrate.ts +1848 -0
  171. package/plugins/kxm/src/vnext-oneshot-producer.ts +424 -0
  172. package/plugins/kxm/src/vnext-permission.ts +936 -0
  173. package/plugins/kxm/src/vnext-pi-producer.ts +628 -0
  174. package/plugins/kxm/src/vnext-repair.ts +1094 -0
  175. package/plugins/kxm/src/vnext-runtime-owner.ts +320 -0
  176. package/plugins/kxm/src/vnext-runtime-store.ts +1560 -0
  177. package/plugins/kxm/src/vnext-runtime-supervisor.ts +586 -0
  178. package/plugins/kxm/src/vnext-runtime.ts +663 -0
  179. package/plugins/kxm/src/vnext-template.ts +247 -0
  180. package/plugins/kxm/src/wiki.ts +313 -0
  181. package/plugins/kxm/src/workflow.ts +1548 -0
  182. package/schemas/vnext/README.md +46 -0
  183. package/schemas/vnext/agent.schema.json +40 -0
  184. package/schemas/vnext/assignment-result.schema.json +66 -0
  185. package/schemas/vnext/backup-manifest.schema.json +89 -0
  186. package/schemas/vnext/candidate.schema.json +109 -0
  187. package/schemas/vnext/common.schema.json +422 -0
  188. package/schemas/vnext/context-candidate.schema.json +76 -0
  189. package/schemas/vnext/context-packet.schema.json +192 -0
  190. package/schemas/vnext/delivery-manifest.schema.json +159 -0
  191. package/schemas/vnext/environment.schema.json +66 -0
  192. package/schemas/vnext/gate-registry.schema.json +109 -0
  193. package/schemas/vnext/handoff-manifest.schema.json +146 -0
  194. package/schemas/vnext/init-operation.schema.json +61 -0
  195. package/schemas/vnext/local-repository-bindings.schema.json +30 -0
  196. package/schemas/vnext/memory-record.schema.json +45 -0
  197. package/schemas/vnext/migration-decision.schema.json +26 -0
  198. package/schemas/vnext/migration-plan.schema.json +123 -0
  199. package/schemas/vnext/migration-receipt.schema.json +52 -0
  200. package/schemas/vnext/model.schema.json +42 -0
  201. package/schemas/vnext/permission-diff.schema.json +57 -0
  202. package/schemas/vnext/prices.schema.json +115 -0
  203. package/schemas/vnext/project.schema.json +85 -0
  204. package/schemas/vnext/repository.schema.json +24 -0
  205. package/schemas/vnext/run-event.schema.json +460 -0
  206. package/schemas/vnext/session-brief.schema.json +153 -0
  207. package/schemas/vnext/sync-event.schema.json +234 -0
  208. package/schemas/vnext/template-provenance.schema.json +38 -0
  209. package/schemas/vnext/workflow.schema.json +248 -0
  210. package/scripts/assignment-run.d.mts +354 -0
  211. package/scripts/assignment-run.mjs +4451 -0
  212. package/scripts/build-runtime.mjs +56 -0
  213. package/scripts/check-generated.mjs +77 -0
  214. package/scripts/check-versions.mjs +34 -0
  215. package/scripts/emit-codex-artifacts.d.mts +9 -0
  216. package/scripts/emit-codex-artifacts.mjs +91 -0
  217. package/scripts/harness-run.d.mts +83 -0
  218. package/scripts/harness-run.mjs +2095 -0
  219. package/scripts/kxm-hub.mjs +105 -0
  220. package/scripts/kxm-publish-npm.mjs +327 -0
  221. package/scripts/kxm-release-github.mjs +472 -0
  222. package/scripts/kxm-runtime-supervisor.mjs +7 -0
  223. package/scripts/kxm-worker.mjs +1127 -0
  224. package/scripts/kxm.mjs +27 -0
  225. package/scripts/roster-policy.d.mts +20 -0
  226. package/scripts/roster-policy.mjs +161 -0
  227. package/scripts/smoke-multi-pi.mjs +479 -0
@@ -0,0 +1,563 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import {
3
+ chmodSync,
4
+ copyFileSync,
5
+ existsSync,
6
+ lstatSync,
7
+ mkdirSync,
8
+ readdirSync,
9
+ readFileSync,
10
+ unlinkSync,
11
+ writeFileSync,
12
+ } from "node:fs";
13
+ import { basename, dirname, join, resolve } from "node:path";
14
+ import { DatabaseSync } from "node:sqlite";
15
+ import { VnextConfigError, type VnextConfigIssue } from "./vnext-config.ts";
16
+
17
+ export interface DatabaseMigrationStep {
18
+ fromVersion: number;
19
+ toVersion: number;
20
+ migrate: (database: DatabaseSync) => void;
21
+ }
22
+
23
+ export interface DatabaseSchemaSpec {
24
+ schema: string;
25
+ version: number;
26
+ tables?: Readonly<Record<string, readonly string[]>>;
27
+ migrations?: readonly DatabaseMigrationStep[];
28
+ timeoutMs?: number;
29
+ }
30
+
31
+ export interface BackupStoreRecord {
32
+ storeId: string;
33
+ sourcePath: string;
34
+ backupFile: string;
35
+ schemaVersion: number;
36
+ sha256: string;
37
+ bytes: number;
38
+ integrity: "ok";
39
+ }
40
+
41
+ export interface BackupManifest {
42
+ schema: "kxm.backup-manifest.v1";
43
+ backupId: string;
44
+ createdAt: string;
45
+ projectRoot?: string;
46
+ stores: BackupStoreRecord[];
47
+ manifestSha256?: string;
48
+ }
49
+
50
+ export interface RestoreStoreRecord {
51
+ storeId: string;
52
+ sourcePath: string;
53
+ backupFile: string;
54
+ schemaVersion: number;
55
+ integrity: "ok";
56
+ }
57
+
58
+ export interface RestoreResult {
59
+ manifestPath: string;
60
+ backupId: string;
61
+ restoredStores: RestoreStoreRecord[];
62
+ }
63
+
64
+ export function databaseError(code: string, file: string, message: string): VnextConfigError {
65
+ const issue: VnextConfigIssue = { phase: "semantic", code, file, message };
66
+ return new VnextConfigError([issue]);
67
+ }
68
+
69
+ export function checkedParent(path: string, description: string): void {
70
+ const parent = dirname(path);
71
+ if (!existsSync(parent)) mkdirSync(parent, { recursive: true, mode: 0o700 });
72
+ const stat = lstatSync(parent, { throwIfNoEntry: false });
73
+ if (!stat || stat.isSymbolicLink() || !stat.isDirectory()) {
74
+ throw databaseError("runtime_path_invalid", description, `${description} parent must be a regular directory, not a link`);
75
+ }
76
+ }
77
+
78
+ export function userTables(database: DatabaseSync): string[] {
79
+ const rows = database.prepare(
80
+ "SELECT name FROM sqlite_master WHERE type = \x27table\x27 AND name NOT LIKE \x27sqlite_%\x27 ORDER BY name",
81
+ ).all() as Array<{ name: string }>;
82
+ return rows.map((row) => row.name);
83
+ }
84
+
85
+ export function tableColumns(database: DatabaseSync, table: string): string[] {
86
+ const rows = database.prepare(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
87
+ return rows.map((row) => row.name).sort();
88
+ }
89
+
90
+ export function verifyExpectedTables(
91
+ database: DatabaseSync,
92
+ file: string,
93
+ description: string,
94
+ expected: Readonly<Record<string, readonly string[]>>,
95
+ ): void {
96
+ const present = new Set(userTables(database));
97
+ for (const [table, columns] of Object.entries(expected)) {
98
+ if (!present.has(table)) {
99
+ throw databaseError("runtime_schema_shape_invalid", file, `${description} is missing table ${table}`);
100
+ }
101
+ const actual = tableColumns(database, table);
102
+ const missing = columns.filter((column) => !actual.includes(column));
103
+ if (missing.length > 0) {
104
+ throw databaseError("runtime_schema_shape_invalid", file, `${description} table ${table} is missing columns ${missing.join(", ")}`);
105
+ }
106
+ }
107
+ }
108
+
109
+ export function ensureWalJournalMode(database: DatabaseSync, file: string, description: string, timeoutMs = 5000): void {
110
+ const deadline = Date.now() + timeoutMs;
111
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
112
+ while (true) {
113
+ try {
114
+ const current = database.prepare("PRAGMA journal_mode").get() as { journal_mode?: string } | undefined;
115
+ if (current?.journal_mode === "wal") {
116
+ return;
117
+ }
118
+ const updated = database.prepare("PRAGMA journal_mode = WAL").get() as { journal_mode?: string } | undefined;
119
+ if (updated?.journal_mode === "wal") {
120
+ return;
121
+ }
122
+ } catch (error) {
123
+ const sqliteError = error as { code?: string; errcode?: number };
124
+ if (sqliteError.code === "ERR_SQLITE_ERROR" && sqliteError.errcode === 5 && Date.now() < deadline) {
125
+ Atomics.wait(sleeper, 0, 0, 10);
126
+ continue;
127
+ }
128
+ throw error;
129
+ }
130
+ if (Date.now() >= deadline) {
131
+ throw databaseError("runtime_timeout", file, `${description} timed out enabling WAL journal mode`);
132
+ }
133
+ Atomics.wait(sleeper, 0, 0, 10);
134
+ }
135
+ }
136
+
137
+ export function openDatabase(file: string, description: string, spec: DatabaseSchemaSpec): DatabaseSync {
138
+ const isMemory = file === ":memory:";
139
+ if (!isMemory) {
140
+ checkedParent(file, description);
141
+ const stat = lstatSync(file, { throwIfNoEntry: false });
142
+ if (stat) {
143
+ if (stat.isSymbolicLink() || !stat.isFile()) {
144
+ throw databaseError("runtime_path_invalid", description, `${description} must be a regular file, not a link or directory`);
145
+ }
146
+ }
147
+ for (const sidecar of [`${file}-wal`, `${file}-shm`]) {
148
+ const info = lstatSync(sidecar, { throwIfNoEntry: false });
149
+ if (info?.isSymbolicLink()) {
150
+ throw databaseError("runtime_path_invalid", description, `${description} sidecar must not be a link`);
151
+ }
152
+ }
153
+ }
154
+
155
+ const database = new DatabaseSync(file);
156
+ let transaction = false;
157
+ try {
158
+ database.exec(`PRAGMA busy_timeout = ${spec.timeoutMs ?? 5000}`);
159
+ if (!isMemory) {
160
+ ensureWalJournalMode(database, file, description, spec.timeoutMs);
161
+ }
162
+ database.exec("PRAGMA synchronous = NORMAL");
163
+ database.exec("PRAGMA foreign_keys = ON");
164
+
165
+ database.exec("BEGIN IMMEDIATE");
166
+ transaction = true;
167
+ const row = database.prepare("PRAGMA user_version").get() as { user_version: number } | undefined;
168
+ const version = row?.user_version ?? 0;
169
+
170
+ if (version > spec.version) {
171
+ throw databaseError("runtime_schema_newer", file, `${description} schema version ${version} is newer than this runtime supports`);
172
+ }
173
+
174
+ if (version === 0) {
175
+ const existing = userTables(database);
176
+ if (existing.length > 0) {
177
+ throw databaseError("runtime_schema_shape_invalid", file, `${description} has tables at schema version 0`);
178
+ }
179
+ database.exec(spec.schema);
180
+ database.exec(`PRAGMA user_version = ${spec.version}`);
181
+ } else if (version < spec.version) {
182
+ let currentVersion = version;
183
+ while (currentVersion < spec.version) {
184
+ const step = spec.migrations?.find((m) => m.fromVersion === currentVersion);
185
+ if (!step) {
186
+ throw databaseError(
187
+ "runtime_schema_outdated",
188
+ file,
189
+ `${description} schema version ${version} is older than ${spec.version}; no migration lane, backup and restore remain E6`,
190
+ );
191
+ }
192
+ step.migrate(database);
193
+ currentVersion = step.toVersion;
194
+ database.exec(`PRAGMA user_version = ${currentVersion}`);
195
+ }
196
+ }
197
+
198
+ if (spec.tables) {
199
+ verifyExpectedTables(database, file, description, spec.tables);
200
+ }
201
+
202
+ database.exec("COMMIT");
203
+ transaction = false;
204
+ return database;
205
+ } catch (error) {
206
+ if (transaction) {
207
+ try { database.exec("ROLLBACK"); } catch { /* already rolled back */ }
208
+ }
209
+ database.close();
210
+ throw error;
211
+ }
212
+ }
213
+
214
+ const activeTransactions = new WeakSet<DatabaseSync>();
215
+
216
+ export function withDatabaseTransaction<T>(
217
+ database: DatabaseSync,
218
+ work: () => T,
219
+ mode: "IMMEDIATE" | "DEFERRED" | "EXCLUSIVE" = "IMMEDIATE",
220
+ ): T {
221
+ if (activeTransactions.has(database)) {
222
+ throw databaseError("runtime_transaction_nested", "transaction", "nested transactions are not allowed");
223
+ }
224
+ activeTransactions.add(database);
225
+ database.exec(`BEGIN ${mode}`);
226
+ try {
227
+ const result = work();
228
+ database.exec("COMMIT");
229
+ return result;
230
+ } catch (error) {
231
+ try { database.exec("ROLLBACK"); } catch { /* ignore rollback error if connection dead */ }
232
+ throw error;
233
+ } finally {
234
+ activeTransactions.delete(database);
235
+ }
236
+ }
237
+
238
+ export function checkpointWal(
239
+ database: DatabaseSync,
240
+ mode: "PASSIVE" | "FULL" | "RESTART" | "TRUNCATE" = "TRUNCATE",
241
+ ): { busy: number; log: number; checkpointed: number } {
242
+ const row = database.prepare(`PRAGMA wal_checkpoint(${mode})`).get() as {
243
+ busy?: number;
244
+ log?: number;
245
+ checkpointed?: number;
246
+ } | undefined;
247
+ return {
248
+ busy: row?.busy ?? 0,
249
+ log: row?.log ?? 0,
250
+ checkpointed: row?.checkpointed ?? 0,
251
+ };
252
+ }
253
+
254
+ export function checkIntegrity(database: DatabaseSync): boolean {
255
+ const rows = database.prepare("PRAGMA integrity_check").all() as Array<{ integrity_check?: string }>;
256
+ return rows.length === 1 && rows[0]?.integrity_check === "ok";
257
+ }
258
+
259
+ export function fileSha256(filePath: string): string {
260
+ const bytes = readFileSync(filePath);
261
+ return `sha256:${createHash("sha256").update(bytes).digest("hex")}`;
262
+ }
263
+
264
+ export function backupDatabaseFile(sourcePath: string, targetPath: string, storeId: string): BackupStoreRecord {
265
+ const resolvedSource = resolve(sourcePath);
266
+ const resolvedTarget = resolve(targetPath);
267
+
268
+ const sourceStat = lstatSync(resolvedSource, { throwIfNoEntry: false });
269
+ if (!sourceStat || !sourceStat.isFile() || sourceStat.isSymbolicLink()) {
270
+ throw databaseError("runtime_path_invalid", resolvedSource, `source database ${resolvedSource} must be a regular file, not a link or directory`);
271
+ }
272
+
273
+ checkedParent(resolvedTarget, "backup target");
274
+
275
+ if (existsSync(resolvedTarget)) {
276
+ unlinkSync(resolvedTarget);
277
+ }
278
+
279
+ const sourceDb = new DatabaseSync(resolvedSource);
280
+ let schemaVersion = 0;
281
+ try {
282
+ sourceDb.exec("PRAGMA busy_timeout = 5000");
283
+ checkpointWal(sourceDb, "TRUNCATE");
284
+ if (!checkIntegrity(sourceDb)) {
285
+ throw databaseError("database_corrupted", resolvedSource, `database ${resolvedSource} failed integrity check`);
286
+ }
287
+ const versionRow = sourceDb.prepare("PRAGMA user_version").get() as { user_version: number } | undefined;
288
+ schemaVersion = versionRow?.user_version ?? 0;
289
+
290
+ const escapedTarget = resolvedTarget.replace(/\x27/g, "\x27\x27");
291
+ sourceDb.exec(`VACUUM INTO \x27${escapedTarget}\x27`);
292
+ } finally {
293
+ sourceDb.close();
294
+ }
295
+
296
+ const targetDb = new DatabaseSync(resolvedTarget);
297
+ try {
298
+ targetDb.exec("PRAGMA busy_timeout = 5000");
299
+ if (!checkIntegrity(targetDb)) {
300
+ throw databaseError("database_corrupted", resolvedTarget, `backup database ${resolvedTarget} failed integrity check`);
301
+ }
302
+ } finally {
303
+ targetDb.close();
304
+ }
305
+
306
+ try { chmodSync(resolvedTarget, 0o600); } catch { /* Windows */ }
307
+
308
+ const sha256 = fileSha256(resolvedTarget);
309
+ const bytes = lstatSync(resolvedTarget).size;
310
+
311
+ return {
312
+ storeId,
313
+ sourcePath: resolvedSource,
314
+ backupFile: basename(resolvedTarget),
315
+ schemaVersion,
316
+ sha256,
317
+ bytes,
318
+ integrity: "ok",
319
+ };
320
+ }
321
+
322
+ export function restoreDatabaseFile(
323
+ backupPath: string,
324
+ targetPath: string,
325
+ storeId: string,
326
+ expectedSchemaVersion?: number,
327
+ maxSupportedVersion?: number,
328
+ ): RestoreStoreRecord {
329
+ const resolvedBackup = resolve(backupPath);
330
+ const resolvedTarget = resolve(targetPath);
331
+
332
+ const backupStat = lstatSync(resolvedBackup, { throwIfNoEntry: false });
333
+ if (!backupStat || !backupStat.isFile() || backupStat.isSymbolicLink()) {
334
+ throw databaseError("runtime_path_invalid", resolvedBackup, `backup database ${resolvedBackup} must be a regular file, not a link or directory`);
335
+ }
336
+
337
+ const backupDb = new DatabaseSync(resolvedBackup);
338
+ let schemaVersion = 0;
339
+ try {
340
+ backupDb.exec("PRAGMA busy_timeout = 5000");
341
+ if (!checkIntegrity(backupDb)) {
342
+ throw databaseError("database_corrupted", resolvedBackup, `backup database ${resolvedBackup} failed integrity check`);
343
+ }
344
+ const versionRow = backupDb.prepare("PRAGMA user_version").get() as { user_version: number } | undefined;
345
+ schemaVersion = versionRow?.user_version ?? 0;
346
+
347
+ if (maxSupportedVersion !== undefined && schemaVersion > maxSupportedVersion) {
348
+ throw databaseError(
349
+ "runtime_schema_newer",
350
+ resolvedBackup,
351
+ `backup store ${storeId} schema version ${schemaVersion} is newer than supported maximum ${maxSupportedVersion}`,
352
+ );
353
+ }
354
+ if (expectedSchemaVersion !== undefined && schemaVersion !== expectedSchemaVersion) {
355
+ throw databaseError(
356
+ "runtime_schema_mismatch",
357
+ resolvedBackup,
358
+ `backup store ${storeId} schema version ${schemaVersion} does not match manifest version ${expectedSchemaVersion}`,
359
+ );
360
+ }
361
+ } finally {
362
+ backupDb.close();
363
+ }
364
+
365
+ checkedParent(resolvedTarget, "restore target");
366
+
367
+ for (const file of [resolvedTarget, `${resolvedTarget}-wal`, `${resolvedTarget}-shm`]) {
368
+ if (existsSync(file)) {
369
+ try { unlinkSync(file); } catch { /* ignore */ }
370
+ }
371
+ }
372
+
373
+ copyFileSync(resolvedBackup, resolvedTarget);
374
+ try { chmodSync(resolvedTarget, 0o600); } catch { /* Windows */ }
375
+
376
+ const targetDb = new DatabaseSync(resolvedTarget);
377
+ try {
378
+ targetDb.exec("PRAGMA busy_timeout = 5000");
379
+ if (!checkIntegrity(targetDb)) {
380
+ throw databaseError("database_corrupted", resolvedTarget, `restored database ${resolvedTarget} failed integrity check`);
381
+ }
382
+ } finally {
383
+ targetDb.close();
384
+ }
385
+
386
+ return {
387
+ storeId,
388
+ sourcePath: resolvedTarget,
389
+ backupFile: basename(resolvedBackup),
390
+ schemaVersion,
391
+ integrity: "ok",
392
+ };
393
+ }
394
+
395
+ export function discoverProjectStores(projectRoot: string, options: { hubDataPath?: string } = {}): Array<{ storeId: string; sourcePath: string; maxSupportedVersion: number }> {
396
+ const root = resolve(projectRoot);
397
+ const stores: Array<{ storeId: string; sourcePath: string; maxSupportedVersion: number }> = [];
398
+
399
+ const hubPath = options.hubDataPath ? resolve(options.hubDataPath) : join(root, ".kxm", "state", "kxm.db");
400
+ if (existsSync(hubPath)) {
401
+ stores.push({ storeId: "hub-store", sourcePath: hubPath, maxSupportedVersion: 3 });
402
+ }
403
+
404
+ const registryPath = join(root, ".kxm", "runtime", "registry.db");
405
+ if (existsSync(registryPath)) {
406
+ stores.push({ storeId: "registry", sourcePath: registryPath, maxSupportedVersion: 1 });
407
+ }
408
+
409
+ const bindingsPath = join(root, ".kxm", "runtime", "bindings.db");
410
+ if (existsSync(bindingsPath)) {
411
+ stores.push({ storeId: "binding-store", sourcePath: bindingsPath, maxSupportedVersion: 1 });
412
+ }
413
+
414
+ const eventsDir = join(root, ".kxm", "runtime", "events");
415
+ if (existsSync(eventsDir)) {
416
+ const entries = readdirSync(eventsDir, { withFileTypes: true });
417
+ for (const entry of entries) {
418
+ if (entry.isFile() && entry.name.endsWith(".db")) {
419
+ const key = entry.name.replace(/\.db$/, "");
420
+ stores.push({
421
+ storeId: `events:${key}`,
422
+ sourcePath: join(eventsDir, entry.name),
423
+ maxSupportedVersion: 3,
424
+ });
425
+ }
426
+ }
427
+ }
428
+
429
+ return stores;
430
+ }
431
+
432
+ export function createBackup(options: {
433
+ projectRoot?: string;
434
+ outDir?: string;
435
+ hubDataPath?: string;
436
+ } = {}): { manifest: BackupManifest; outDir: string } {
437
+ const projectRoot = options.projectRoot ? resolve(options.projectRoot) : process.cwd();
438
+ const stores = discoverProjectStores(projectRoot, {
439
+ ...(options.hubDataPath !== undefined ? { hubDataPath: options.hubDataPath } : {}),
440
+ });
441
+
442
+ if (stores.length === 0) {
443
+ throw databaseError("backup_no_stores", projectRoot, "no existing SQLite stores found to backup");
444
+ }
445
+
446
+ const now = new Date();
447
+ const timestamp = now.toISOString().replace(/[:.]/g, "-");
448
+ const backupId = `bk_${randomBytes(8).toString("hex")}`;
449
+ const outDir = options.outDir ? resolve(options.outDir) : join(projectRoot, ".kxm", "backups", `backup-${timestamp}`);
450
+
451
+ if (!existsSync(outDir)) {
452
+ mkdirSync(outDir, { recursive: true, mode: 0o700 });
453
+ }
454
+
455
+ const backedUpStores: BackupStoreRecord[] = [];
456
+ const usedFilenames = new Set<string>();
457
+
458
+ for (const store of stores) {
459
+ let filename = basename(store.sourcePath);
460
+ if (usedFilenames.has(filename)) {
461
+ const sanitizedId = store.storeId.replace(/[^a-zA-Z0-9_.-]/g, "_");
462
+ filename = `${sanitizedId}-${filename}`;
463
+ }
464
+ usedFilenames.add(filename);
465
+
466
+ const targetFile = join(outDir, filename);
467
+ const record = backupDatabaseFile(store.sourcePath, targetFile, store.storeId);
468
+ backedUpStores.push(record);
469
+ }
470
+
471
+ const manifest: BackupManifest = {
472
+ schema: "kxm.backup-manifest.v1",
473
+ backupId,
474
+ createdAt: now.toISOString(),
475
+ projectRoot,
476
+ stores: backedUpStores,
477
+ };
478
+
479
+ const manifestJson = JSON.stringify(manifest, null, 2) + "\n";
480
+ const manifestSha256 = `sha256:${createHash("sha256").update(manifestJson, "utf8").digest("hex")}`;
481
+ manifest.manifestSha256 = manifestSha256;
482
+
483
+ const finalJson = JSON.stringify(manifest, null, 2) + "\n";
484
+ const manifestPath = join(outDir, "manifest.json");
485
+ writeFileSync(manifestPath, finalJson, "utf8");
486
+
487
+ return { manifest, outDir };
488
+ }
489
+
490
+ export function restoreBackup(
491
+ manifestPathOrDir: string,
492
+ options: { projectRoot?: string } = {},
493
+ ): RestoreResult {
494
+ let manifestPath = resolve(manifestPathOrDir);
495
+ const stat = lstatSync(manifestPath, { throwIfNoEntry: false });
496
+ if (!stat) {
497
+ throw databaseError("runtime_path_invalid", manifestPath, `manifest path ${manifestPath} does not exist`);
498
+ }
499
+ if (stat.isDirectory()) {
500
+ manifestPath = join(manifestPath, "manifest.json");
501
+ }
502
+
503
+ if (!existsSync(manifestPath)) {
504
+ throw databaseError("runtime_path_invalid", manifestPath, `backup manifest ${manifestPath} not found`);
505
+ }
506
+
507
+ const manifestDir = dirname(manifestPath);
508
+ const rawText = readFileSync(manifestPath, "utf8");
509
+ let manifest: BackupManifest;
510
+ try {
511
+ manifest = JSON.parse(rawText) as BackupManifest;
512
+ } catch (error) {
513
+ throw databaseError("restore_manifest_invalid", manifestPath, `failed to parse backup manifest: ${error instanceof Error ? error.message : String(error)}`);
514
+ }
515
+
516
+ if (manifest.schema !== "kxm.backup-manifest.v1" || !Array.isArray(manifest.stores) || manifest.stores.length === 0) {
517
+ throw databaseError("restore_manifest_invalid", manifestPath, "manifest is not a valid kxm.backup-manifest.v1 document");
518
+ }
519
+
520
+ const restoredStores: RestoreStoreRecord[] = [];
521
+
522
+ for (const store of manifest.stores) {
523
+ const backupFilePath = join(manifestDir, store.backupFile);
524
+ if (!existsSync(backupFilePath)) {
525
+ throw databaseError("restore_file_missing", backupFilePath, `backup file ${store.backupFile} missing from ${manifestDir}`);
526
+ }
527
+
528
+ const actualSha256 = fileSha256(backupFilePath);
529
+ if (actualSha256 !== store.sha256) {
530
+ throw databaseError(
531
+ "restore_manifest_digest_mismatch",
532
+ backupFilePath,
533
+ `backup file ${store.backupFile} sha256 ${actualSha256} does not match manifest hash ${store.sha256}`,
534
+ );
535
+ }
536
+
537
+ let maxSupported = 3;
538
+ if (store.storeId === "registry" || store.storeId === "binding-store") {
539
+ maxSupported = 1;
540
+ }
541
+
542
+ let targetPath = store.sourcePath;
543
+ if (options.projectRoot && manifest.projectRoot && targetPath.startsWith(manifest.projectRoot)) {
544
+ const rel = targetPath.slice(manifest.projectRoot.length).replace(/^[\\/]+/, "");
545
+ targetPath = join(resolve(options.projectRoot), rel);
546
+ }
547
+
548
+ const result = restoreDatabaseFile(
549
+ backupFilePath,
550
+ targetPath,
551
+ store.storeId,
552
+ store.schemaVersion,
553
+ maxSupported,
554
+ );
555
+ restoredStores.push(result);
556
+ }
557
+
558
+ return {
559
+ manifestPath,
560
+ backupId: manifest.backupId,
561
+ restoredStores,
562
+ };
563
+ }