jettypod 4.4.118 → 4.4.121

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 (240) hide show
  1. package/.env +4 -3
  2. package/Cargo.lock +6450 -0
  3. package/Cargo.toml +35 -0
  4. package/README.md +5 -1
  5. package/TAURI-MIGRATION-PLAN.md +840 -0
  6. package/apps/dashboard/app/connect-claude/page.tsx +5 -6
  7. package/apps/dashboard/app/decision/[id]/page.tsx +63 -58
  8. package/apps/dashboard/app/demo/gates/page.tsx +43 -45
  9. package/apps/dashboard/app/design-system/page.tsx +868 -0
  10. package/apps/dashboard/app/globals.css +80 -4
  11. package/apps/dashboard/app/install-claude/page.tsx +4 -6
  12. package/apps/dashboard/app/login/page.tsx +72 -54
  13. package/apps/dashboard/app/page.tsx +101 -48
  14. package/apps/dashboard/app/settings/page.tsx +61 -13
  15. package/apps/dashboard/app/signup/page.tsx +242 -0
  16. package/apps/dashboard/app/subscribe/page.tsx +0 -2
  17. package/apps/dashboard/app/tests/page.tsx +37 -4
  18. package/apps/dashboard/app/welcome/page.tsx +13 -16
  19. package/apps/dashboard/app/work/[id]/page.tsx +117 -118
  20. package/apps/dashboard/app/work/[id]/proof/page.tsx +1489 -0
  21. package/apps/dashboard/components/AppShell.tsx +92 -85
  22. package/apps/dashboard/components/CardMenu.tsx +45 -12
  23. package/apps/dashboard/components/ClaudePanel.tsx +771 -850
  24. package/apps/dashboard/components/ClaudePanelInput.tsx +43 -15
  25. package/apps/dashboard/components/ConnectClaudeScreen.tsx +17 -34
  26. package/apps/dashboard/components/CopyableId.tsx +3 -4
  27. package/apps/dashboard/components/DetailReviewActions.tsx +100 -0
  28. package/apps/dashboard/components/DragContext.tsx +134 -63
  29. package/apps/dashboard/components/DraggableCard.tsx +3 -5
  30. package/apps/dashboard/components/DropZone.tsx +6 -7
  31. package/apps/dashboard/components/EditableDetailDescription.tsx +7 -13
  32. package/apps/dashboard/components/EditableDetailTitle.tsx +6 -13
  33. package/apps/dashboard/components/EditableTitle.tsx +26 -7
  34. package/apps/dashboard/components/ElapsedTimer.tsx +66 -0
  35. package/apps/dashboard/components/EpicGroup.tsx +359 -0
  36. package/apps/dashboard/components/GateCard.tsx +79 -17
  37. package/apps/dashboard/components/GateChoiceCard.tsx +15 -18
  38. package/apps/dashboard/components/InstallClaudeScreen.tsx +15 -32
  39. package/apps/dashboard/components/JettyLoader.tsx +37 -0
  40. package/apps/dashboard/components/KanbanBoard.tsx +368 -958
  41. package/apps/dashboard/components/KanbanCard.tsx +740 -0
  42. package/apps/dashboard/components/LazyCard.tsx +62 -0
  43. package/apps/dashboard/components/LazyMarkdown.tsx +11 -0
  44. package/apps/dashboard/components/MainNav.tsx +38 -73
  45. package/apps/dashboard/components/MessageBlock.tsx +468 -0
  46. package/apps/dashboard/components/ModeStartCard.tsx +15 -16
  47. package/apps/dashboard/components/OnboardingWelcome.tsx +213 -0
  48. package/apps/dashboard/components/PlaceholderCard.tsx +3 -4
  49. package/apps/dashboard/components/ProjectSwitcher.tsx +30 -30
  50. package/apps/dashboard/components/PrototypeTimeline.tsx +72 -51
  51. package/apps/dashboard/components/RealTimeKanbanWrapper.tsx +406 -388
  52. package/apps/dashboard/components/RealTimeTestsWrapper.tsx +373 -235
  53. package/apps/dashboard/components/ReviewFooter.tsx +139 -0
  54. package/apps/dashboard/components/SessionList.tsx +19 -19
  55. package/apps/dashboard/components/SubscribeContent.tsx +91 -47
  56. package/apps/dashboard/components/TestTree.tsx +16 -16
  57. package/apps/dashboard/components/TipCard.tsx +16 -17
  58. package/apps/dashboard/components/Toast.tsx +5 -6
  59. package/apps/dashboard/components/TypeIcon.tsx +55 -0
  60. package/apps/dashboard/components/ViewModeToolbar.tsx +104 -0
  61. package/apps/dashboard/components/WaveCompletionAnimation.tsx +52 -65
  62. package/apps/dashboard/components/WelcomeScreen.tsx +19 -35
  63. package/apps/dashboard/components/WorkItemHeader.tsx +4 -5
  64. package/apps/dashboard/components/WorkItemTree.tsx +11 -32
  65. package/apps/dashboard/components/settings/AccountSection.tsx +55 -35
  66. package/apps/dashboard/components/settings/AiContextSection.tsx +89 -0
  67. package/apps/dashboard/components/settings/ContextDocumentsSection.tsx +317 -0
  68. package/apps/dashboard/components/settings/EnvVarsSection.tsx +74 -152
  69. package/apps/dashboard/components/settings/GeneralSection.tsx +162 -56
  70. package/apps/dashboard/components/settings/ProjectStackSection.tsx +948 -0
  71. package/apps/dashboard/components/settings/SettingsLayout.tsx +4 -5
  72. package/apps/dashboard/components/ui/Button.tsx +104 -0
  73. package/apps/dashboard/components/ui/Input.tsx +78 -0
  74. package/apps/dashboard/components.json +1 -1
  75. package/apps/dashboard/contexts/ClaudeSessionContext.tsx +711 -418
  76. package/apps/dashboard/contexts/ConnectionStatusContext.tsx +25 -5
  77. package/apps/dashboard/contexts/UsageContext.tsx +87 -32
  78. package/apps/dashboard/dev.sh +35 -0
  79. package/apps/dashboard/eslint.config.mjs +9 -9
  80. package/apps/dashboard/hooks/useKanbanAnimation.ts +29 -0
  81. package/apps/dashboard/hooks/useKanbanUndo.ts +83 -0
  82. package/apps/dashboard/hooks/useWebSocket.ts +138 -83
  83. package/apps/dashboard/index.html +73 -0
  84. package/apps/dashboard/lib/constants.ts +43 -0
  85. package/apps/dashboard/lib/data-bridge.ts +722 -0
  86. package/apps/dashboard/lib/db.ts +69 -1265
  87. package/apps/dashboard/lib/environment-config.ts +173 -0
  88. package/apps/dashboard/lib/environment-verification.ts +119 -0
  89. package/apps/dashboard/lib/kanban-utils.ts +270 -0
  90. package/apps/dashboard/lib/proof-run.ts +495 -0
  91. package/apps/dashboard/lib/proof-scenario-runner.ts +346 -0
  92. package/apps/dashboard/lib/run-migrations.js +27 -2
  93. package/apps/dashboard/lib/service-recovery.ts +326 -0
  94. package/apps/dashboard/lib/session-state-machine.ts +1 -0
  95. package/apps/dashboard/lib/session-state-utils.ts +0 -164
  96. package/apps/dashboard/lib/session-stream-manager.ts +308 -134
  97. package/apps/dashboard/lib/shadows.ts +7 -0
  98. package/apps/dashboard/lib/stream-manager-registry.ts +46 -6
  99. package/apps/dashboard/lib/tauri-bridge.ts +102 -0
  100. package/apps/dashboard/lib/tauri.ts +106 -0
  101. package/apps/dashboard/lib/utils.ts +6 -0
  102. package/apps/dashboard/next-env.d.ts +1 -1
  103. package/apps/dashboard/package.json +21 -32
  104. package/apps/dashboard/public/bug-icon.png +0 -0
  105. package/apps/dashboard/public/buoy-icon.png +0 -0
  106. package/apps/dashboard/public/fonts/Satoshi-Variable.woff2 +0 -0
  107. package/apps/dashboard/public/fonts/Satoshi-VariableItalic.woff2 +0 -0
  108. package/apps/dashboard/public/in-flight-seagull.png +0 -0
  109. package/apps/dashboard/public/jetty-icon-loading-alt.svg +11 -0
  110. package/apps/dashboard/public/jetty-icon-loading.svg +11 -0
  111. package/apps/dashboard/public/jettypod_logo.png +0 -0
  112. package/apps/dashboard/public/pier-icon.png +0 -0
  113. package/apps/dashboard/public/star-icon.png +0 -0
  114. package/apps/dashboard/public/wrench-icon.png +0 -0
  115. package/apps/dashboard/scripts/tauri-build.js +228 -0
  116. package/apps/dashboard/scripts/upload-tauri-to-r2.js +125 -0
  117. package/apps/dashboard/scripts/ws-server.js +191 -0
  118. package/apps/dashboard/src/main.tsx +12 -0
  119. package/apps/dashboard/src/router.tsx +107 -0
  120. package/apps/dashboard/src/vite-env.d.ts +1 -0
  121. package/apps/dashboard/tsconfig.json +7 -12
  122. package/apps/dashboard/tsconfig.tsbuildinfo +1 -1
  123. package/apps/dashboard/vite.config.ts +33 -0
  124. package/apps/update-server/src/index.ts +228 -80
  125. package/claude-hooks/global-guardrails.js +14 -13
  126. package/crates/jettypod-cli/Cargo.toml +19 -0
  127. package/crates/jettypod-cli/src/commands.rs +1249 -0
  128. package/crates/jettypod-cli/src/main.rs +595 -0
  129. package/crates/jettypod-core/Cargo.toml +26 -0
  130. package/crates/jettypod-core/build.rs +98 -0
  131. package/crates/jettypod-core/migrations/V1__baseline.sql +197 -0
  132. package/crates/jettypod-core/migrations/V2__work_items_indexes.sql +6 -0
  133. package/crates/jettypod-core/migrations/V3__qa_steps.sql +2 -0
  134. package/crates/jettypod-core/src/auth.rs +294 -0
  135. package/crates/jettypod-core/src/config.rs +397 -0
  136. package/crates/jettypod-core/src/db/mod.rs +507 -0
  137. package/crates/jettypod-core/src/db/recovery.rs +114 -0
  138. package/crates/jettypod-core/src/db/startup.rs +101 -0
  139. package/crates/jettypod-core/src/db/validate.rs +149 -0
  140. package/crates/jettypod-core/src/error.rs +76 -0
  141. package/crates/jettypod-core/src/git.rs +458 -0
  142. package/crates/jettypod-core/src/lib.rs +20 -0
  143. package/crates/jettypod-core/src/sessions.rs +625 -0
  144. package/crates/jettypod-core/src/skills.rs +556 -0
  145. package/crates/jettypod-core/src/work.rs +1086 -0
  146. package/crates/jettypod-core/src/worktree.rs +628 -0
  147. package/crates/jettypod-core/src/ws.rs +767 -0
  148. package/cucumber-test.cjs +6 -0
  149. package/cucumber.js +9 -3
  150. package/docs/COMMAND_REFERENCE.md +34 -0
  151. package/hooks/post-checkout +32 -75
  152. package/hooks/post-merge +111 -10
  153. package/jest.setup.js +1 -0
  154. package/jettypod.js +145 -116
  155. package/lib/bdd-preflight.js +96 -0
  156. package/lib/chore-taxonomy.js +33 -10
  157. package/lib/database.js +36 -16
  158. package/lib/db-watcher.js +1 -1
  159. package/lib/git-hooks/pre-commit +1 -1
  160. package/lib/jettypod-backup.js +27 -4
  161. package/lib/merge-lock.js +111 -253
  162. package/lib/migrations/027-plan-at-creation-column.js +3 -1
  163. package/lib/migrations/029-remove-autoincrement.js +307 -0
  164. package/lib/migrations/029-rename-corrupted-to-cleaned.js +149 -0
  165. package/lib/migrations/030-rejection-round-columns.js +54 -0
  166. package/lib/migrations/031-session-isolation-index.js +17 -0
  167. package/lib/migrations/index.js +47 -4
  168. package/lib/schema.js +10 -5
  169. package/lib/seed-onboarding.js +1 -1
  170. package/lib/update-command/index.js +9 -175
  171. package/lib/work-commands/index.js +144 -19
  172. package/lib/work-tracking/index.js +148 -27
  173. package/lib/worktree-diagnostics.js +16 -16
  174. package/lib/worktree-facade.js +1 -1
  175. package/lib/worktree-manager.js +8 -8
  176. package/lib/worktree-reconciler.js +5 -5
  177. package/package.json +9 -2
  178. package/scripts/ndjson-to-cucumber-json.js +152 -0
  179. package/scripts/postinstall.js +25 -0
  180. package/skills-templates/bug-mode/SKILL.md +79 -20
  181. package/skills-templates/bug-planning/SKILL.md +25 -29
  182. package/skills-templates/chore-mode/SKILL.md +171 -69
  183. package/skills-templates/chore-mode/verification.js +51 -10
  184. package/skills-templates/chore-planning/SKILL.md +47 -18
  185. package/skills-templates/design-system-selection/SKILL.md +273 -0
  186. package/skills-templates/epic-planning/SKILL.md +82 -48
  187. package/skills-templates/external-transition/SKILL.md +47 -47
  188. package/skills-templates/feature-planning/SKILL.md +173 -74
  189. package/skills-templates/production-mode/SKILL.md +69 -49
  190. package/skills-templates/request-routing/SKILL.md +4 -4
  191. package/skills-templates/simple-improvement/SKILL.md +74 -29
  192. package/skills-templates/speed-mode/SKILL.md +217 -141
  193. package/skills-templates/stable-mode/SKILL.md +148 -89
  194. package/apps/dashboard/README.md +0 -36
  195. package/apps/dashboard/app/api/claude/[workItemId]/message/route.ts +0 -386
  196. package/apps/dashboard/app/api/claude/[workItemId]/pin/route.ts +0 -24
  197. package/apps/dashboard/app/api/claude/[workItemId]/route.ts +0 -167
  198. package/apps/dashboard/app/api/claude/sessions/[sessionId]/content/route.ts +0 -52
  199. package/apps/dashboard/app/api/claude/sessions/[sessionId]/message/route.ts +0 -378
  200. package/apps/dashboard/app/api/claude/sessions/[sessionId]/pin/route.ts +0 -24
  201. package/apps/dashboard/app/api/claude/sessions/cleanup/route.ts +0 -34
  202. package/apps/dashboard/app/api/claude/sessions/route.ts +0 -184
  203. package/apps/dashboard/app/api/decisions/[id]/route.ts +0 -25
  204. package/apps/dashboard/app/api/internal/set-project/route.ts +0 -17
  205. package/apps/dashboard/app/api/kanban/route.ts +0 -15
  206. package/apps/dashboard/app/api/settings/env-vars/route.ts +0 -125
  207. package/apps/dashboard/app/api/settings/general/route.ts +0 -21
  208. package/apps/dashboard/app/api/tests/route.ts +0 -9
  209. package/apps/dashboard/app/api/tests/run/route.ts +0 -82
  210. package/apps/dashboard/app/api/tests/run/stream/route.ts +0 -71
  211. package/apps/dashboard/app/api/tests/undefined/route.ts +0 -9
  212. package/apps/dashboard/app/api/usage/route.ts +0 -17
  213. package/apps/dashboard/app/api/work/[id]/description/route.ts +0 -21
  214. package/apps/dashboard/app/api/work/[id]/epic/route.ts +0 -21
  215. package/apps/dashboard/app/api/work/[id]/order/route.ts +0 -21
  216. package/apps/dashboard/app/api/work/[id]/status/route.ts +0 -21
  217. package/apps/dashboard/app/api/work/[id]/title/route.ts +0 -21
  218. package/apps/dashboard/app/layout.tsx +0 -43
  219. package/apps/dashboard/components/UpgradeBanner.tsx +0 -29
  220. package/apps/dashboard/electron/ipc-handlers.js +0 -1028
  221. package/apps/dashboard/electron/main.js +0 -2124
  222. package/apps/dashboard/electron/preload.js +0 -123
  223. package/apps/dashboard/electron/session-manager.js +0 -141
  224. package/apps/dashboard/electron-builder.config.js +0 -357
  225. package/apps/dashboard/hooks/useClaudeSessions.ts +0 -299
  226. package/apps/dashboard/lib/claude-process-manager.ts +0 -492
  227. package/apps/dashboard/lib/db-bridge.ts +0 -282
  228. package/apps/dashboard/lib/prototypes.ts +0 -202
  229. package/apps/dashboard/lib/test-results-db.ts +0 -307
  230. package/apps/dashboard/lib/tests.ts +0 -282
  231. package/apps/dashboard/next.config.js +0 -50
  232. package/apps/dashboard/postcss.config.mjs +0 -7
  233. package/apps/dashboard/public/file.svg +0 -1
  234. package/apps/dashboard/public/globe.svg +0 -1
  235. package/apps/dashboard/public/next.svg +0 -1
  236. package/apps/dashboard/public/vercel.svg +0 -1
  237. package/apps/dashboard/public/window.svg +0 -1
  238. package/apps/dashboard/scripts/download-node.js +0 -104
  239. package/apps/dashboard/scripts/upload-to-r2.js +0 -89
  240. package/docs/bdd-guidance.md +0 -390
@@ -0,0 +1,101 @@
1
+ //! Startup validation and automatic recovery.
2
+ //!
3
+ //! Port of Node.js `validateOnStartup()` — opens the database with integrity
4
+ //! checks, schema validation, and automatic snapshot recovery on failure.
5
+
6
+ use crate::error::{CoreError, Result};
7
+ use std::path::Path;
8
+ use super::Database;
9
+
10
+ impl Database {
11
+ /// Open the work database for a project root directory.
12
+ ///
13
+ /// The database file is at `<project_root>/.jettypod/work.db`.
14
+ /// Includes integrity checks, schema validation, and automatic
15
+ /// snapshot recovery on failure.
16
+ pub fn open(project_root: &Path) -> Result<Self> {
17
+ let db_path = project_root.join(".jettypod").join("work.db");
18
+ Self::open_path(&db_path)
19
+ }
20
+
21
+ /// Open a database at a specific path with full validation and recovery.
22
+ ///
23
+ /// This is the primary entry point for production callers. It:
24
+ /// 1. Recovers from snapshots if the DB file is missing
25
+ /// 2. Attempts snapshot recovery if the DB fails to open
26
+ /// 3. Runs integrity checks with recovery on corruption
27
+ /// 4. Validates schema and tables
28
+ pub fn open_path(path: &Path) -> Result<Self> {
29
+ // 1. If DB file is missing, try snapshot recovery first
30
+ if !path.exists() {
31
+ match Self::recover_from_snapshots(path) {
32
+ Ok(count) => {
33
+ eprintln!("Recovered {count} rows from snapshots");
34
+ return Self::open_path_unchecked(path);
35
+ }
36
+ Err(_) => {
37
+ // No snapshots or recovery failed — open_path_unchecked will create fresh
38
+ }
39
+ }
40
+ }
41
+
42
+ // 2. Try to open
43
+ let db = match Self::open_path_unchecked(path) {
44
+ Ok(db) => db,
45
+ Err(open_err) => {
46
+ // Try snapshot recovery
47
+ match Self::recover_from_snapshots(path) {
48
+ Ok(count) => {
49
+ eprintln!("Recovered {count} rows from snapshots");
50
+ return Self::open_path_unchecked(path);
51
+ }
52
+ Err(recovery_err) => {
53
+ return Err(CoreError::Config(format!(
54
+ "Database failed to open and recovery failed.\n\
55
+ Original error: {open_err}\n\
56
+ Recovery error: {recovery_err}\n\n\
57
+ Manual recovery options:\n \
58
+ 1. Restore from backup: jettypod work restore-backup latest\n \
59
+ 2. Check ~/.jettypod-backups/ for global backups"
60
+ )));
61
+ }
62
+ }
63
+ }
64
+ };
65
+
66
+ // 3. Check integrity
67
+ let integrity = db.check_integrity()?;
68
+ if !integrity.ok {
69
+ drop(db);
70
+ match Self::recover_from_snapshots(path) {
71
+ Ok(count) => {
72
+ eprintln!("Recovered {count} rows from snapshots (corruption detected)");
73
+ let db = Self::open_path_unchecked(path)?;
74
+ let verify = db.check_integrity()?;
75
+ if verify.ok {
76
+ db.fixup_missing_columns()?;
77
+ db.validate_schema()?;
78
+ db.validate_tables()?;
79
+ return Ok(db);
80
+ }
81
+ }
82
+ Err(_) => {}
83
+ }
84
+ let errors = integrity.errors.join("\n - ");
85
+ return Err(CoreError::Config(format!(
86
+ "Database integrity check failed and automatic recovery was unsuccessful.\n\
87
+ Errors found:\n - {errors}\n\n\
88
+ Manual recovery options:\n \
89
+ 1. Restore from backup: jettypod work restore-backup latest\n \
90
+ 2. Check ~/.jettypod-backups/ for global backups"
91
+ )));
92
+ }
93
+
94
+ // 4. Fix up missing columns, then validate schema and tables
95
+ db.fixup_missing_columns()?;
96
+ db.validate_schema()?;
97
+ db.validate_tables()?;
98
+
99
+ Ok(db)
100
+ }
101
+ }
@@ -0,0 +1,149 @@
1
+ //! Schema validation and database integrity utilities.
2
+
3
+ use crate::error::{CoreError, Result};
4
+ use super::Database;
5
+
6
+ // Column and table lists derived from V1__baseline.sql at build time.
7
+ include!(concat!(env!("OUT_DIR"), "/schema_info.rs"));
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Result types
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /// Result of a WAL checkpoint operation.
14
+ #[derive(Debug)]
15
+ pub struct WalCheckpointResult {
16
+ /// Whether the checkpoint completed without being blocked.
17
+ pub success: bool,
18
+ /// Number of pages written to the database file.
19
+ pub pages_written: i32,
20
+ }
21
+
22
+ /// Result of an integrity check.
23
+ #[derive(Debug)]
24
+ pub struct IntegrityResult {
25
+ /// `true` if the database passed all integrity checks.
26
+ pub ok: bool,
27
+ /// Error messages, empty if `ok` is true.
28
+ pub errors: Vec<String>,
29
+ }
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Database validation methods
33
+ // ---------------------------------------------------------------------------
34
+
35
+ impl Database {
36
+ /// Force a WAL checkpoint, flushing all pending writes to the main
37
+ /// database file.
38
+ ///
39
+ /// Uses TRUNCATE mode to also remove the WAL file. Should be called
40
+ /// before git checkout/merge operations to prevent WAL corruption.
41
+ pub fn wal_checkpoint(&self) -> Result<WalCheckpointResult> {
42
+ let row: (i32, i32, i32) = self
43
+ .conn
44
+ .query_row("PRAGMA wal_checkpoint(TRUNCATE)", [], |row| {
45
+ Ok((row.get(0)?, row.get(1)?, row.get(2)?))
46
+ })?;
47
+
48
+ let (busy, _log_pages, checkpointed) = row;
49
+ Ok(WalCheckpointResult {
50
+ success: busy == 0,
51
+ pages_written: checkpointed,
52
+ })
53
+ }
54
+
55
+ /// Run SQLite's built-in integrity check.
56
+ pub fn check_integrity(&self) -> Result<IntegrityResult> {
57
+ let mut stmt = self.conn.prepare("PRAGMA integrity_check")?;
58
+ let results: Vec<String> = stmt
59
+ .query_map([], |row| row.get(0))?
60
+ .collect::<std::result::Result<Vec<_>, _>>()?;
61
+
62
+ let ok = results.len() == 1 && results[0] == "ok";
63
+ Ok(IntegrityResult {
64
+ ok,
65
+ errors: if ok { Vec::new() } else { results },
66
+ })
67
+ }
68
+
69
+ /// Add any missing columns to work_items using definitions from V1__baseline.sql.
70
+ ///
71
+ /// Existing databases created before new columns were added to V1 won't have them
72
+ /// since CREATE TABLE IF NOT EXISTS skips existing tables. This method fills the gap.
73
+ pub fn fixup_missing_columns(&self) -> Result<()> {
74
+ let mut stmt = self.conn.prepare("PRAGMA table_info(work_items)")?;
75
+ let existing: Vec<String> = stmt
76
+ .query_map([], |row| row.get::<_, String>(1))?
77
+ .collect::<std::result::Result<Vec<_>, _>>()?;
78
+
79
+ for (col_name, col_def) in WORK_ITEM_COLUMN_DEFS {
80
+ if !existing.iter().any(|e| e == *col_name) {
81
+ let sql = format!("ALTER TABLE work_items ADD COLUMN {col_name} {col_def}");
82
+ self.conn.execute(&sql, [])?;
83
+ eprintln!("Schema fixup: added missing column {col_name}");
84
+ }
85
+ }
86
+
87
+ Ok(())
88
+ }
89
+
90
+ /// Validate that all expected columns exist on the work_items table.
91
+ pub fn validate_schema(&self) -> Result<()> {
92
+ let mut stmt = self.conn.prepare("PRAGMA table_info(work_items)")?;
93
+ let existing: Vec<String> = stmt
94
+ .query_map([], |row| row.get::<_, String>(1))?
95
+ .collect::<std::result::Result<Vec<_>, _>>()?;
96
+
97
+ let missing: Vec<&str> = EXPECTED_WORK_ITEM_COLUMNS
98
+ .iter()
99
+ .filter(|col| !existing.iter().any(|e| e == **col))
100
+ .copied()
101
+ .collect();
102
+
103
+ if missing.is_empty() {
104
+ Ok(())
105
+ } else {
106
+ Err(CoreError::Config(format!(
107
+ "Schema validation failed: missing columns: {}",
108
+ missing.join(", ")
109
+ )))
110
+ }
111
+ }
112
+
113
+ /// Validate that all expected tables exist in the database.
114
+ pub fn validate_tables(&self) -> Result<()> {
115
+ let mut stmt = self.conn.prepare(
116
+ "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'"
117
+ )?;
118
+ let existing: Vec<String> = stmt
119
+ .query_map([], |row| row.get::<_, String>(0))?
120
+ .collect::<std::result::Result<Vec<_>, _>>()?;
121
+
122
+ let missing: Vec<&str> = EXPECTED_TABLES
123
+ .iter()
124
+ .filter(|tbl| !existing.iter().any(|e| e == **tbl))
125
+ .copied()
126
+ .collect();
127
+
128
+ if missing.is_empty() {
129
+ Ok(())
130
+ } else {
131
+ Err(CoreError::Config(format!(
132
+ "Schema validation failed: missing tables: {}",
133
+ missing.join(", ")
134
+ )))
135
+ }
136
+ }
137
+
138
+ /// Sync `_meta.schema_version` so the Node.js forward-compatibility guard
139
+ /// knows the current schema state. The value matches the count of Node.js
140
+ /// migration files (33) that our V1 baseline represents.
141
+ pub fn sync_meta_version(&self) -> Result<()> {
142
+ const NODE_MIGRATION_COUNT: &str = "33";
143
+ self.conn.execute(
144
+ "INSERT OR REPLACE INTO _meta (key, value) VALUES ('schema_version', ?1)",
145
+ [NODE_MIGRATION_COUNT],
146
+ )?;
147
+ Ok(())
148
+ }
149
+ }
@@ -0,0 +1,76 @@
1
+ use thiserror::Error;
2
+
3
+ /// Structured git operation errors.
4
+ #[derive(Debug, Error)]
5
+ pub enum GitError {
6
+ #[error("git command failed: git {args}\n{stderr}")]
7
+ CommandFailed { args: String, stderr: String },
8
+
9
+ #[error("git not available: {0}")]
10
+ NotAvailable(String),
11
+
12
+ #[error("invalid branch name: {0}")]
13
+ InvalidBranch(String),
14
+
15
+ #[error("not a git repository: {0}")]
16
+ NotARepo(String),
17
+
18
+ #[error("{0}")]
19
+ Other(String),
20
+ }
21
+
22
+ /// Structured work item errors.
23
+ #[derive(Debug, Error)]
24
+ pub enum WorkError {
25
+ #[error("work item #{0} not found")]
26
+ NotFound(i64),
27
+
28
+ #[error("title cannot be empty")]
29
+ EmptyTitle,
30
+
31
+ #[error("invalid state: {0}")]
32
+ InvalidState(String),
33
+
34
+ #[error("{0}")]
35
+ Other(String),
36
+ }
37
+
38
+ /// Core error type for all jettypod-core operations.
39
+ ///
40
+ /// Each variant maps to a module boundary. Automatic `From` conversions
41
+ /// via `#[from]` keep call sites clean — use `?` freely.
42
+ #[derive(Debug, Error)]
43
+ pub enum CoreError {
44
+ #[error("database: {0}")]
45
+ Database(#[from] rusqlite::Error),
46
+
47
+ #[error("I/O: {0}")]
48
+ Io(#[from] std::io::Error),
49
+
50
+ #[error("JSON: {0}")]
51
+ Json(#[from] serde_json::Error),
52
+
53
+ #[error("JWT: {0}")]
54
+ Jwt(#[from] jsonwebtoken::errors::Error),
55
+
56
+ #[error("config: {0}")]
57
+ Config(String),
58
+
59
+ #[error("git: {0}")]
60
+ Git(#[from] GitError),
61
+
62
+ #[error("work: {0}")]
63
+ Work(#[from] WorkError),
64
+
65
+ #[error("skills: {0}")]
66
+ Skills(String),
67
+
68
+ #[error("parse: {0}")]
69
+ Parse(#[from] strum::ParseError),
70
+
71
+ #[error("{0}")]
72
+ Other(String),
73
+ }
74
+
75
+ /// Convenience alias used throughout jettypod-core.
76
+ pub type Result<T> = std::result::Result<T, CoreError>;