truecourse 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -113,6 +113,7 @@ Once the server is running, `cd` into any repo and:
113
113
  npx truecourse add # Register repo without analyzing
114
114
  npx truecourse analyze # Analyze current repo, show violations
115
115
  npx truecourse analyze --code-review # Analyze with LLM code review (off by default)
116
+ npx truecourse analyze --no-llm # Deterministic checks only, skip all LLM calls
116
117
  npx truecourse analyze --diff # Show new/resolved violations from uncommitted changes
117
118
  npx truecourse list # Show violations from latest analysis
118
119
  npx truecourse list --diff # Show saved diff check results
@@ -177,6 +178,39 @@ All rules are visible in the **Rules** tab in the web UI. Custom rule generation
177
178
  | Rust | Planned |
178
179
  | PHP | Planned |
179
180
 
181
+ ## Telemetry
182
+
183
+ TrueCourse collects anonymous usage data to help us understand adoption and improve the product. Telemetry is **enabled by default** and can be disabled at any time.
184
+
185
+ ### What is collected
186
+
187
+ - Event type (`analyze` or `diff-check`)
188
+ - Tool version
189
+ - Languages detected (e.g., TypeScript, Python)
190
+ - File count range (bucketed: 1-50, 50-200, etc.)
191
+ - Service count
192
+ - Analysis duration range (bucketed)
193
+ - OS and architecture (e.g., `darwin-arm64`)
194
+ - Random anonymous session ID (not tied to user identity)
195
+
196
+ ### What is NOT collected
197
+
198
+ - Source code, file paths, repo names, or git URLs
199
+ - Violation details, rule results, or LLM outputs
200
+ - IP addresses, user identity, or machine hostname
201
+
202
+ ### Opt out
203
+
204
+ ```bash
205
+ npx truecourse telemetry disable # Disable telemetry
206
+ npx truecourse telemetry enable # Re-enable telemetry
207
+ npx truecourse telemetry status # Check current status
208
+ ```
209
+
210
+ Telemetry is automatically disabled in CI environments (`CI=true`) and can also be disabled by setting `TRUECOURSE_TELEMETRY=0`.
211
+
212
+ Data is sent to [PostHog](https://posthog.com) for aggregation.
213
+
180
214
  ## License
181
215
 
182
216
  MIT
package/cli.mjs CHANGED
@@ -972,8 +972,8 @@ var require_command = __commonJS({
972
972
  "node_modules/.pnpm/commander@12.1.0/node_modules/commander/lib/command.js"(exports) {
973
973
  var EventEmitter = __require("node:events").EventEmitter;
974
974
  var childProcess = __require("node:child_process");
975
- var path9 = __require("node:path");
976
- var fs8 = __require("node:fs");
975
+ var path10 = __require("node:path");
976
+ var fs9 = __require("node:fs");
977
977
  var process2 = __require("node:process");
978
978
  var { Argument: Argument2, humanReadableArgName } = require_argument();
979
979
  var { CommanderError: CommanderError2 } = require_error();
@@ -1905,11 +1905,11 @@ Expecting one of '${allowedValues.join("', '")}'`);
1905
1905
  let launchWithNode = false;
1906
1906
  const sourceExt = [".js", ".ts", ".tsx", ".mjs", ".cjs"];
1907
1907
  function findFile(baseDir, baseName) {
1908
- const localBin = path9.resolve(baseDir, baseName);
1909
- if (fs8.existsSync(localBin)) return localBin;
1910
- if (sourceExt.includes(path9.extname(baseName))) return void 0;
1908
+ const localBin = path10.resolve(baseDir, baseName);
1909
+ if (fs9.existsSync(localBin)) return localBin;
1910
+ if (sourceExt.includes(path10.extname(baseName))) return void 0;
1911
1911
  const foundExt = sourceExt.find(
1912
- (ext) => fs8.existsSync(`${localBin}${ext}`)
1912
+ (ext) => fs9.existsSync(`${localBin}${ext}`)
1913
1913
  );
1914
1914
  if (foundExt) return `${localBin}${foundExt}`;
1915
1915
  return void 0;
@@ -1921,21 +1921,21 @@ Expecting one of '${allowedValues.join("', '")}'`);
1921
1921
  if (this._scriptPath) {
1922
1922
  let resolvedScriptPath;
1923
1923
  try {
1924
- resolvedScriptPath = fs8.realpathSync(this._scriptPath);
1924
+ resolvedScriptPath = fs9.realpathSync(this._scriptPath);
1925
1925
  } catch (err) {
1926
1926
  resolvedScriptPath = this._scriptPath;
1927
1927
  }
1928
- executableDir = path9.resolve(
1929
- path9.dirname(resolvedScriptPath),
1928
+ executableDir = path10.resolve(
1929
+ path10.dirname(resolvedScriptPath),
1930
1930
  executableDir
1931
1931
  );
1932
1932
  }
1933
1933
  if (executableDir) {
1934
1934
  let localFile = findFile(executableDir, executableFile);
1935
1935
  if (!localFile && !subcommand._executableFile && this._scriptPath) {
1936
- const legacyName = path9.basename(
1936
+ const legacyName = path10.basename(
1937
1937
  this._scriptPath,
1938
- path9.extname(this._scriptPath)
1938
+ path10.extname(this._scriptPath)
1939
1939
  );
1940
1940
  if (legacyName !== this._name) {
1941
1941
  localFile = findFile(
@@ -1946,7 +1946,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
1946
1946
  }
1947
1947
  executableFile = localFile || executableFile;
1948
1948
  }
1949
- launchWithNode = sourceExt.includes(path9.extname(executableFile));
1949
+ launchWithNode = sourceExt.includes(path10.extname(executableFile));
1950
1950
  let proc;
1951
1951
  if (process2.platform !== "win32") {
1952
1952
  if (launchWithNode) {
@@ -2786,7 +2786,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
2786
2786
  * @return {Command}
2787
2787
  */
2788
2788
  nameFromFilename(filename) {
2789
- this._name = path9.basename(filename, path9.extname(filename));
2789
+ this._name = path10.basename(filename, path10.extname(filename));
2790
2790
  return this;
2791
2791
  }
2792
2792
  /**
@@ -2800,9 +2800,9 @@ Expecting one of '${allowedValues.join("', '")}'`);
2800
2800
  * @param {string} [path]
2801
2801
  * @return {(string|null|Command)}
2802
2802
  */
2803
- executableDir(path10) {
2804
- if (path10 === void 0) return this._executableDir;
2805
- this._executableDir = path10;
2803
+ executableDir(path11) {
2804
+ if (path11 === void 0) return this._executableDir;
2805
+ this._executableDir = path11;
2806
2806
  return this;
2807
2807
  }
2808
2808
  /**
@@ -3766,7 +3766,7 @@ ${import_picocolors2.default.gray(m2)} ${s}
3766
3766
  // node_modules/.pnpm/xmlhttprequest-ssl@2.1.2/node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js
3767
3767
  var require_XMLHttpRequest = __commonJS({
3768
3768
  "node_modules/.pnpm/xmlhttprequest-ssl@2.1.2/node_modules/xmlhttprequest-ssl/lib/XMLHttpRequest.js"(exports, module) {
3769
- var fs8 = __require("fs");
3769
+ var fs9 = __require("fs");
3770
3770
  var Url = __require("url");
3771
3771
  var spawn3 = __require("child_process").spawn;
3772
3772
  module.exports = XMLHttpRequest3;
@@ -3924,7 +3924,7 @@ var require_XMLHttpRequest = __commonJS({
3924
3924
  throw new Error("XMLHttpRequest: Only GET method is supported");
3925
3925
  }
3926
3926
  if (settings.async) {
3927
- fs8.readFile(unescape(url2.pathname), function(error, data2) {
3927
+ fs9.readFile(unescape(url2.pathname), function(error, data2) {
3928
3928
  if (error) {
3929
3929
  self.handleError(error, error.errno || -1);
3930
3930
  } else {
@@ -3936,7 +3936,7 @@ var require_XMLHttpRequest = __commonJS({
3936
3936
  });
3937
3937
  } else {
3938
3938
  try {
3939
- this.response = fs8.readFileSync(unescape(url2.pathname));
3939
+ this.response = fs9.readFileSync(unescape(url2.pathname));
3940
3940
  this.responseText = this.response.toString("utf8");
3941
3941
  this.status = 200;
3942
3942
  setState(self.DONE);
@@ -4062,15 +4062,15 @@ var require_XMLHttpRequest = __commonJS({
4062
4062
  } else {
4063
4063
  var contentFile = ".node-xmlhttprequest-content-" + process.pid;
4064
4064
  var syncFile = ".node-xmlhttprequest-sync-" + process.pid;
4065
- fs8.writeFileSync(syncFile, "", "utf8");
4065
+ fs9.writeFileSync(syncFile, "", "utf8");
4066
4066
  var execString = "var http = require('http'), https = require('https'), fs = require('fs');var doRequest = http" + (ssl ? "s" : "") + ".request;var options = " + JSON.stringify(options) + ";var responseText = '';var responseData = Buffer.alloc(0);var req = doRequest(options, function(response) {response.on('data', function(chunk) { var data = Buffer.from(chunk); responseText += data.toString('utf8'); responseData = Buffer.concat([responseData, data]);});response.on('end', function() {fs.writeFileSync('" + contentFile + "', JSON.stringify({err: null, data: {statusCode: response.statusCode, headers: response.headers, text: responseText, data: responseData.toString('base64')}}), 'utf8');fs.unlinkSync('" + syncFile + "');});response.on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});}).on('error', function(error) {fs.writeFileSync('" + contentFile + "', 'NODE-XMLHTTPREQUEST-ERROR:' + JSON.stringify(error), 'utf8');fs.unlinkSync('" + syncFile + "');});" + (data ? "req.write('" + JSON.stringify(data).slice(1, -1).replace(/'/g, "\\'") + "');" : "") + "req.end();";
4067
4067
  var syncProc = spawn3(process.argv[0], ["-e", execString]);
4068
4068
  var statusText;
4069
- while (fs8.existsSync(syncFile)) {
4069
+ while (fs9.existsSync(syncFile)) {
4070
4070
  }
4071
- self.responseText = fs8.readFileSync(contentFile, "utf8");
4071
+ self.responseText = fs9.readFileSync(contentFile, "utf8");
4072
4072
  syncProc.stdin.end();
4073
- fs8.unlinkSync(contentFile);
4073
+ fs9.unlinkSync(contentFile);
4074
4074
  if (self.responseText.match(/^NODE-XMLHTTPREQUEST-ERROR:/)) {
4075
4075
  var errorObj = JSON.parse(self.responseText.replace(/^NODE-XMLHTTPREQUEST-ERROR:/, ""));
4076
4076
  self.handleError(errorObj, 503);
@@ -9762,12 +9762,12 @@ function parse2(str) {
9762
9762
  uri.queryKey = queryKey(uri, uri["query"]);
9763
9763
  return uri;
9764
9764
  }
9765
- function pathNames(obj, path9) {
9766
- const regx = /\/{2,9}/g, names = path9.replace(regx, "/").split("/");
9767
- if (path9.slice(0, 1) == "/" || path9.length === 0) {
9765
+ function pathNames(obj, path10) {
9766
+ const regx = /\/{2,9}/g, names = path10.replace(regx, "/").split("/");
9767
+ if (path10.slice(0, 1) == "/" || path10.length === 0) {
9768
9768
  names.splice(0, 1);
9769
9769
  }
9770
- if (path9.slice(-1) == "/") {
9770
+ if (path10.slice(-1) == "/") {
9771
9771
  names.splice(names.length - 1, 1);
9772
9772
  }
9773
9773
  return names;
@@ -10441,7 +10441,7 @@ var init_esm_debug = __esm({
10441
10441
  });
10442
10442
 
10443
10443
  // node_modules/.pnpm/socket.io-client@4.8.3/node_modules/socket.io-client/build/esm-debug/url.js
10444
- function url(uri, path9 = "", loc) {
10444
+ function url(uri, path10 = "", loc) {
10445
10445
  let obj = uri;
10446
10446
  loc = loc || typeof location !== "undefined" && location;
10447
10447
  if (null == uri)
@@ -10475,7 +10475,7 @@ function url(uri, path9 = "", loc) {
10475
10475
  obj.path = obj.path || "/";
10476
10476
  const ipv6 = obj.host.indexOf(":") !== -1;
10477
10477
  const host = ipv6 ? "[" + obj.host + "]" : obj.host;
10478
- obj.id = obj.protocol + "://" + host + ":" + obj.port + path9;
10478
+ obj.id = obj.protocol + "://" + host + ":" + obj.port + path10;
10479
10479
  obj.href = obj.protocol + "://" + host + (loc && loc.port === obj.port ? "" : ":" + obj.port);
10480
10480
  return obj;
10481
10481
  }
@@ -12146,8 +12146,8 @@ function lookup(uri, opts) {
12146
12146
  const parsed = url(uri, opts.path || "/socket.io");
12147
12147
  const source = parsed.source;
12148
12148
  const id = parsed.id;
12149
- const path9 = parsed.path;
12150
- const sameNamespace = cache[id] && path9 in cache[id]["nsps"];
12149
+ const path10 = parsed.path;
12150
+ const sameNamespace = cache[id] && path10 in cache[id]["nsps"];
12151
12151
  const newConnection = opts.forceNew || opts["force new connection"] || false === opts.multiplex || sameNamespace;
12152
12152
  let io;
12153
12153
  if (newConnection) {
@@ -12683,10 +12683,16 @@ function startConsoleMode(openBrowser) {
12683
12683
  process.execPath,
12684
12684
  [serverPath],
12685
12685
  {
12686
- stdio: "inherit",
12686
+ stdio: ["ignore", "pipe", "pipe"],
12687
12687
  env: { ...process.env }
12688
12688
  }
12689
12689
  );
12690
+ const forwardOutput = (data) => {
12691
+ process.stdout.write("\r\x1B[K");
12692
+ process.stderr.write(data);
12693
+ };
12694
+ serverProcess.stdout?.on("data", forwardOutput);
12695
+ serverProcess.stderr?.on("data", forwardOutput);
12690
12696
  serverProcess.on("error", (error) => {
12691
12697
  v2.error(`Failed to start server: ${error.message}`);
12692
12698
  process.exit(1);
@@ -13364,9 +13370,9 @@ var {
13364
13370
  init_dist2();
13365
13371
  init_setup();
13366
13372
  init_start();
13367
- import fs7 from "node:fs";
13368
- import path8 from "node:path";
13369
- import os6 from "node:os";
13373
+ import fs8 from "node:fs";
13374
+ import path9 from "node:path";
13375
+ import os7 from "node:os";
13370
13376
 
13371
13377
  // tools/cli/src/commands/add.ts
13372
13378
  init_dist2();
@@ -13415,9 +13421,73 @@ async function runAdd() {
13415
13421
  // tools/cli/src/commands/analyze.ts
13416
13422
  init_dist2();
13417
13423
  init_helpers();
13424
+
13425
+ // tools/cli/src/telemetry.ts
13426
+ init_dist2();
13427
+ import fs7 from "node:fs";
13428
+ import path7 from "node:path";
13429
+ import os6 from "node:os";
13430
+ import crypto from "node:crypto";
13431
+ var DEFAULT_CONFIG2 = {
13432
+ enabled: true,
13433
+ anonymousId: "",
13434
+ noticeShown: false
13435
+ };
13436
+ function getTelemetryConfigPath() {
13437
+ return path7.join(os6.homedir(), ".truecourse", "telemetry.json");
13438
+ }
13439
+ function readTelemetryConfig() {
13440
+ const configPath = getTelemetryConfigPath();
13441
+ try {
13442
+ const raw = fs7.readFileSync(configPath, "utf-8");
13443
+ const parsed = JSON.parse(raw);
13444
+ const config = { ...DEFAULT_CONFIG2, ...parsed };
13445
+ if (!config.anonymousId) {
13446
+ config.anonymousId = crypto.randomUUID();
13447
+ writeTelemetryConfig(config);
13448
+ }
13449
+ return config;
13450
+ } catch {
13451
+ const config = {
13452
+ enabled: true,
13453
+ anonymousId: crypto.randomUUID(),
13454
+ noticeShown: false
13455
+ };
13456
+ writeTelemetryConfig(config);
13457
+ return config;
13458
+ }
13459
+ }
13460
+ function writeTelemetryConfig(partial) {
13461
+ const configPath = getTelemetryConfigPath();
13462
+ const dir = path7.dirname(configPath);
13463
+ fs7.mkdirSync(dir, { recursive: true });
13464
+ let current;
13465
+ try {
13466
+ const raw = fs7.readFileSync(configPath, "utf-8");
13467
+ current = { ...DEFAULT_CONFIG2, ...JSON.parse(raw) };
13468
+ } catch {
13469
+ current = { ...DEFAULT_CONFIG2 };
13470
+ }
13471
+ const merged = { ...current, ...partial };
13472
+ fs7.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
13473
+ }
13474
+ function showFirstRunNotice() {
13475
+ try {
13476
+ const config = readTelemetryConfig();
13477
+ if (!config.enabled || config.noticeShown) return;
13478
+ v2.info(
13479
+ "TrueCourse collects anonymous usage data to improve the product. Run `npx truecourse telemetry disable` to opt out."
13480
+ );
13481
+ writeTelemetryConfig({ noticeShown: true });
13482
+ } catch {
13483
+ }
13484
+ }
13485
+
13486
+ // tools/cli/src/commands/analyze.ts
13418
13487
  var TIMEOUT_MS = 15 * 60 * 1e3;
13419
- async function runAnalyze({ noAutostart = false, codeReview = false } = {}) {
13488
+ async function runAnalyze({ noAutostart = false, codeReview = false, deterministicOnly = false } = {}) {
13420
13489
  we("Analyzing repository");
13490
+ showFirstRunNotice();
13421
13491
  if (noAutostart) {
13422
13492
  const url2 = getServerUrl();
13423
13493
  try {
@@ -13480,7 +13550,7 @@ async function runAnalyze({ noAutostart = false, codeReview = false } = {}) {
13480
13550
  fetch(`${serverUrl}/api/repos/${repo.id}/analyze`, {
13481
13551
  method: "POST",
13482
13552
  headers: { "Content-Type": "application/json" },
13483
- body: JSON.stringify({ codeReview })
13553
+ body: JSON.stringify({ codeReview, deterministicOnly })
13484
13554
  }).then((res2) => {
13485
13555
  if (!res2.ok) {
13486
13556
  clearTimeout(timeout);
@@ -13508,7 +13578,9 @@ async function runAnalyze({ noAutostart = false, codeReview = false } = {}) {
13508
13578
  }
13509
13579
  const violations = await res.json();
13510
13580
  renderViolationsSummary(violations);
13511
- v2.info("Code review running in background \u2014 results will appear in the dashboard");
13581
+ if (!deterministicOnly) {
13582
+ v2.info("Code review running in background \u2014 results will appear in the dashboard");
13583
+ }
13512
13584
  const repoUrl = `${serverUrl}/repos/${repo.id}`;
13513
13585
  if (firstRun) {
13514
13586
  openInBrowser(repoUrl);
@@ -13533,6 +13605,7 @@ async function runAnalyze({ noAutostart = false, codeReview = false } = {}) {
13533
13605
  }
13534
13606
  async function runAnalyzeDiff({ noAutostart = false } = {}) {
13535
13607
  we("Running diff check");
13608
+ showFirstRunNotice();
13536
13609
  if (noAutostart) {
13537
13610
  const url2 = getServerUrl();
13538
13611
  try {
@@ -13627,11 +13700,11 @@ init_dist2();
13627
13700
  init_platform();
13628
13701
  init_logs();
13629
13702
  init_helpers();
13630
- import path7 from "node:path";
13703
+ import path8 from "node:path";
13631
13704
  import { fileURLToPath as fileURLToPath3 } from "node:url";
13632
- var __dirname2 = path7.dirname(fileURLToPath3(import.meta.url));
13705
+ var __dirname2 = path8.dirname(fileURLToPath3(import.meta.url));
13633
13706
  function getServerPath2() {
13634
- return path7.join(__dirname2, "..", "server.mjs");
13707
+ return path8.join(__dirname2, "..", "server.mjs");
13635
13708
  }
13636
13709
  async function healthcheck2() {
13637
13710
  const url2 = getServerUrl();
@@ -13786,7 +13859,7 @@ function registerServiceCommand(program3) {
13786
13859
  init_helpers();
13787
13860
  init_platform();
13788
13861
  var program2 = new Command();
13789
- program2.name("truecourse").version("0.1.0").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13862
+ program2.name("truecourse").version("0.2.2").description("TrueCourse CLI - Setup and manage your TrueCourse instance");
13790
13863
  program2.command("setup").description("Run the setup wizard to configure TrueCourse").action(async () => {
13791
13864
  await runSetup();
13792
13865
  });
@@ -13796,11 +13869,11 @@ program2.command("start").description("Start TrueCourse services").action(async
13796
13869
  program2.command("add").description("Add the current directory as a repository").action(async () => {
13797
13870
  await runAdd();
13798
13871
  });
13799
- program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").option("--code-review", "Include LLM code review (off by default)").option("--no-autostart", "Don't auto-start the server (for use from Claude Code skills)").action(async (options) => {
13872
+ program2.command("analyze").description("Analyze the current repository").option("--diff", "Run diff check against latest analysis").option("--code-review", "Include LLM code review (off by default)").option("--no-llm", "Skip all LLM calls, run only deterministic checks").option("--no-autostart", "Don't auto-start the server (for use from Claude Code skills)").action(async (options) => {
13800
13873
  if (options.diff) {
13801
13874
  await runAnalyzeDiff({ noAutostart: !options.autostart });
13802
13875
  } else {
13803
- await runAnalyze({ noAutostart: !options.autostart, codeReview: options.codeReview ?? false });
13876
+ await runAnalyze({ noAutostart: !options.autostart, codeReview: options.codeReview ?? false, deterministicOnly: !options.llm });
13804
13877
  }
13805
13878
  });
13806
13879
  program2.command("code-review").description("Run LLM code review on the latest analysis").option("--diff", "Run on the latest diff analysis instead").option("--no-autostart", "Don't auto-start the server").action(async (options) => {
@@ -13832,10 +13905,29 @@ program2.command("stop").description("Stop the TrueCourse background service").a
13832
13905
  v2.info("Press Ctrl+C in the terminal where TrueCourse is running.");
13833
13906
  }
13834
13907
  });
13908
+ var telemetryCmd = program2.command("telemetry").description("Manage anonymous usage telemetry");
13909
+ telemetryCmd.command("enable").description("Enable anonymous usage telemetry").action(() => {
13910
+ writeTelemetryConfig({ enabled: true });
13911
+ v2.success("Telemetry enabled. Thank you for helping improve TrueCourse!");
13912
+ });
13913
+ telemetryCmd.command("disable").description("Disable anonymous usage telemetry").action(() => {
13914
+ writeTelemetryConfig({ enabled: false });
13915
+ v2.success("Telemetry disabled. No data will be collected.");
13916
+ });
13917
+ telemetryCmd.command("status").description("Show current telemetry status").action(() => {
13918
+ const config = readTelemetryConfig();
13919
+ if (process.env.CI === "true") {
13920
+ v2.info("Telemetry is automatically disabled in CI environments.");
13921
+ } else if (config.enabled) {
13922
+ v2.info("Telemetry is enabled.");
13923
+ } else {
13924
+ v2.info("Telemetry is disabled.");
13925
+ }
13926
+ });
13835
13927
  program2.action(async () => {
13836
- const configDir = path8.join(os6.homedir(), ".truecourse");
13837
- const envPath = path8.join(configDir, ".env");
13838
- const isFirstRun = !fs7.existsSync(envPath);
13928
+ const configDir = path9.join(os7.homedir(), ".truecourse");
13929
+ const envPath = path9.join(configDir, ".env");
13930
+ const isFirstRun = !fs8.existsSync(envPath);
13839
13931
  if (isFirstRun) {
13840
13932
  await runSetup();
13841
13933
  }
@@ -0,0 +1,2 @@
1
+ ALTER TABLE "analyses" ADD COLUMN "status" text DEFAULT 'completed' NOT NULL;
2
+ -- Existing rows are completed analyses; new rows created at analysis start will explicitly set 'running'