truecourse 0.5.3 → 0.5.5

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 (4) hide show
  1. package/README.md +22 -0
  2. package/cli.mjs +370 -64
  3. package/package.json +1 -1
  4. package/server.mjs +376 -62
package/cli.mjs CHANGED
@@ -4335,8 +4335,8 @@ function resolveSkillsSrcDir() {
4335
4335
  const candidate = resolve(__dirname4, "skills", "truecourse");
4336
4336
  return existsSync(candidate) ? candidate : null;
4337
4337
  }
4338
- function skillDestDir(repoPath) {
4339
- return resolve(repoPath, ".claude", "skills", "truecourse");
4338
+ function skillsParentDir(repoPath) {
4339
+ return resolve(repoPath, ".claude", "skills");
4340
4340
  }
4341
4341
  function listSkillDirs(root) {
4342
4342
  if (!existsSync(root)) return [];
@@ -4345,9 +4345,9 @@ function listSkillDirs(root) {
4345
4345
  function computeMissingSkills(repoPath) {
4346
4346
  const src = resolveSkillsSrcDir();
4347
4347
  if (!src) return [];
4348
- const shipped = new Set(listSkillDirs(src));
4349
- const installed = new Set(listSkillDirs(skillDestDir(repoPath)));
4350
- return [...shipped].filter((name) => !installed.has(name));
4348
+ const shipped = listSkillDirs(src);
4349
+ const parent = skillsParentDir(repoPath);
4350
+ return shipped.filter((name) => !existsSync(resolve(parent, name)));
4351
4351
  }
4352
4352
  function hasInstalledSkills(repoPath) {
4353
4353
  return computeMissingSkills(repoPath).length === 0;
@@ -4358,11 +4358,11 @@ function copySkills(repoPath, skillNames) {
4358
4358
  O2.warn("Skills directory not found in package \u2014 skipping.");
4359
4359
  return;
4360
4360
  }
4361
- const destParent = skillDestDir(repoPath);
4362
- mkdirSync(destParent, { recursive: true });
4361
+ const parent = skillsParentDir(repoPath);
4362
+ mkdirSync(parent, { recursive: true });
4363
4363
  for (const name of skillNames) {
4364
4364
  const skillSrc = resolve(src, name);
4365
- const skillDest = resolve(destParent, name);
4365
+ const skillDest = resolve(parent, name);
4366
4366
  if (existsSync(skillDest)) continue;
4367
4367
  cpSync(skillSrc, skillDest, { recursive: true });
4368
4368
  }
@@ -4380,8 +4380,11 @@ async function promptInstallSkills(repoPath, { install } = {}) {
4380
4380
  }
4381
4381
  if (install === false) return;
4382
4382
  if (!isInteractive()) return;
4383
- const isUpgrade = existsSync(skillDestDir(repoPath));
4384
- const message = isUpgrade ? `New Claude Code skill${missing.length === 1 ? "" : "s"} available: ${missing.join(", ")}. Install?` : "Would you like to install Claude Code skills?";
4383
+ const src = resolveSkillsSrcDir();
4384
+ const shipped = src ? listSkillDirs(src) : [];
4385
+ const parent = skillsParentDir(repoPath);
4386
+ const alreadyInstalled = shipped.some((name) => existsSync(resolve(parent, name)));
4387
+ const message = alreadyInstalled ? `New Claude Code skill${missing.length === 1 ? "" : "s"} available: ${missing.join(", ")}. Install?` : "Would you like to install Claude Code skills?";
4385
4388
  const answer = await ot2({ message });
4386
4389
  if (q(answer) || !answer) return;
4387
4390
  copySkills(repoPath, missing);
@@ -121033,6 +121036,172 @@ var init_rules_service = __esm({
121033
121036
  }
121034
121037
  });
121035
121038
 
121039
+ // node_modules/.pnpm/yocto-queue@1.2.2/node_modules/yocto-queue/index.js
121040
+ var Node, Queue;
121041
+ var init_yocto_queue = __esm({
121042
+ "node_modules/.pnpm/yocto-queue@1.2.2/node_modules/yocto-queue/index.js"() {
121043
+ Node = class {
121044
+ value;
121045
+ next;
121046
+ constructor(value) {
121047
+ this.value = value;
121048
+ }
121049
+ };
121050
+ Queue = class {
121051
+ #head;
121052
+ #tail;
121053
+ #size;
121054
+ constructor() {
121055
+ this.clear();
121056
+ }
121057
+ enqueue(value) {
121058
+ const node = new Node(value);
121059
+ if (this.#head) {
121060
+ this.#tail.next = node;
121061
+ this.#tail = node;
121062
+ } else {
121063
+ this.#head = node;
121064
+ this.#tail = node;
121065
+ }
121066
+ this.#size++;
121067
+ }
121068
+ dequeue() {
121069
+ const current = this.#head;
121070
+ if (!current) {
121071
+ return;
121072
+ }
121073
+ this.#head = this.#head.next;
121074
+ this.#size--;
121075
+ if (!this.#head) {
121076
+ this.#tail = void 0;
121077
+ }
121078
+ return current.value;
121079
+ }
121080
+ peek() {
121081
+ if (!this.#head) {
121082
+ return;
121083
+ }
121084
+ return this.#head.value;
121085
+ }
121086
+ clear() {
121087
+ this.#head = void 0;
121088
+ this.#tail = void 0;
121089
+ this.#size = 0;
121090
+ }
121091
+ get size() {
121092
+ return this.#size;
121093
+ }
121094
+ *[Symbol.iterator]() {
121095
+ let current = this.#head;
121096
+ while (current) {
121097
+ yield current.value;
121098
+ current = current.next;
121099
+ }
121100
+ }
121101
+ *drain() {
121102
+ while (this.#head) {
121103
+ yield this.dequeue();
121104
+ }
121105
+ }
121106
+ };
121107
+ }
121108
+ });
121109
+
121110
+ // node_modules/.pnpm/p-limit@7.3.0/node_modules/p-limit/index.js
121111
+ function pLimit(concurrency) {
121112
+ let rejectOnClear = false;
121113
+ if (typeof concurrency === "object") {
121114
+ ({ concurrency, rejectOnClear = false } = concurrency);
121115
+ }
121116
+ validateConcurrency(concurrency);
121117
+ if (typeof rejectOnClear !== "boolean") {
121118
+ throw new TypeError("Expected `rejectOnClear` to be a boolean");
121119
+ }
121120
+ const queue = new Queue();
121121
+ let activeCount = 0;
121122
+ const resumeNext = () => {
121123
+ if (activeCount < concurrency && queue.size > 0) {
121124
+ activeCount++;
121125
+ queue.dequeue().run();
121126
+ }
121127
+ };
121128
+ const next = () => {
121129
+ activeCount--;
121130
+ resumeNext();
121131
+ };
121132
+ const run = async (function_, resolve8, arguments_) => {
121133
+ const result = (async () => function_(...arguments_))();
121134
+ resolve8(result);
121135
+ try {
121136
+ await result;
121137
+ } catch {
121138
+ }
121139
+ next();
121140
+ };
121141
+ const enqueue = (function_, resolve8, reject, arguments_) => {
121142
+ const queueItem = { reject };
121143
+ new Promise((internalResolve) => {
121144
+ queueItem.run = internalResolve;
121145
+ queue.enqueue(queueItem);
121146
+ }).then(run.bind(void 0, function_, resolve8, arguments_));
121147
+ if (activeCount < concurrency) {
121148
+ resumeNext();
121149
+ }
121150
+ };
121151
+ const generator = (function_, ...arguments_) => new Promise((resolve8, reject) => {
121152
+ enqueue(function_, resolve8, reject, arguments_);
121153
+ });
121154
+ Object.defineProperties(generator, {
121155
+ activeCount: {
121156
+ get: () => activeCount
121157
+ },
121158
+ pendingCount: {
121159
+ get: () => queue.size
121160
+ },
121161
+ clearQueue: {
121162
+ value() {
121163
+ if (!rejectOnClear) {
121164
+ queue.clear();
121165
+ return;
121166
+ }
121167
+ const abortError = AbortSignal.abort().reason;
121168
+ while (queue.size > 0) {
121169
+ queue.dequeue().reject(abortError);
121170
+ }
121171
+ }
121172
+ },
121173
+ concurrency: {
121174
+ get: () => concurrency,
121175
+ set(newConcurrency) {
121176
+ validateConcurrency(newConcurrency);
121177
+ concurrency = newConcurrency;
121178
+ queueMicrotask(() => {
121179
+ while (activeCount < concurrency && queue.size > 0) {
121180
+ resumeNext();
121181
+ }
121182
+ });
121183
+ }
121184
+ },
121185
+ map: {
121186
+ async value(iterable, function_) {
121187
+ const promises = Array.from(iterable, (value, index) => this(function_, value, index));
121188
+ return Promise.all(promises);
121189
+ }
121190
+ }
121191
+ });
121192
+ return generator;
121193
+ }
121194
+ function validateConcurrency(concurrency) {
121195
+ if (!((Number.isInteger(concurrency) || concurrency === Number.POSITIVE_INFINITY) && concurrency > 0)) {
121196
+ throw new TypeError("Expected `concurrency` to be a number from 1 and up");
121197
+ }
121198
+ }
121199
+ var init_p_limit = __esm({
121200
+ "node_modules/.pnpm/p-limit@7.3.0/node_modules/p-limit/index.js"() {
121201
+ init_yocto_queue();
121202
+ }
121203
+ });
121204
+
121036
121205
  // apps/server/dist/services/analysis-registry.js
121037
121206
  function registerChildProcess(repoId, child) {
121038
121207
  const entry = activeAnalyses.get(repoId);
@@ -123004,6 +123173,15 @@ var init_env = __esm({
123004
123173
  });
123005
123174
 
123006
123175
  // apps/server/dist/config/index.js
123176
+ function parsePositiveInt(envVar, raw, defaultValue) {
123177
+ if (raw === void 0 || raw === "")
123178
+ return defaultValue;
123179
+ const parsed = Number(raw);
123180
+ if (!Number.isInteger(parsed) || parsed < 1) {
123181
+ throw new Error(`${envVar} must be a positive integer, got "${raw}"`);
123182
+ }
123183
+ return parsed;
123184
+ }
123007
123185
  var config;
123008
123186
  var init_config2 = __esm({
123009
123187
  "apps/server/dist/config/index.js"() {
@@ -123016,7 +123194,8 @@ var init_config2 = __esm({
123016
123194
  claudeCodeBinary: process.env.CLAUDE_CODE_BINARY || "claude",
123017
123195
  claudeCodeModel: process.env.CLAUDE_CODE_MODEL || "",
123018
123196
  claudeCodeTimeoutMs: parseInt(process.env.CLAUDE_CODE_TIMEOUT_MS || "120000", 10),
123019
- claudeCodeMaxRetries: parseInt(process.env.CLAUDE_CODE_MAX_RETRIES || "2", 10)
123197
+ claudeCodeMaxRetries: parseInt(process.env.CLAUDE_CODE_MAX_RETRIES || "2", 10),
123198
+ claudeCodeMaxConcurrency: parsePositiveInt("CLAUDE_CODE_MAX_CONCURRENCY", process.env.CLAUDE_CODE_MAX_CONCURRENCY, 10)
123020
123199
  };
123021
123200
  }
123022
123201
  });
@@ -123687,6 +123866,7 @@ var init_cli_provider = __esm({
123687
123866
  "apps/server/dist/services/llm/cli-provider.js"() {
123688
123867
  "use strict";
123689
123868
  init_logger();
123869
+ init_p_limit();
123690
123870
  init_analysis_registry();
123691
123871
  init_esm5();
123692
123872
  init_config2();
@@ -123694,6 +123874,7 @@ var init_cli_provider = __esm({
123694
123874
  init_schemas2();
123695
123875
  BaseCLIProvider = class {
123696
123876
  maxRetries = config.claudeCodeMaxRetries ?? 2;
123877
+ limit = pLimit(config.claudeCodeMaxConcurrency);
123697
123878
  debugDir = null;
123698
123879
  callCounter = 0;
123699
123880
  _analysisId = null;
@@ -123886,36 +124067,43 @@ var init_cli_provider = __esm({
123886
124067
  }
123887
124068
  /** Spawn CLI with retry on parse/validation failure. */
123888
124069
  async spawnAndParse(prompt, schema2, opts) {
123889
- const jsonSchemaStr = this.toJsonSchema(schema2);
123890
- const label = opts?.label ?? "call";
123891
- let lastError = null;
123892
- for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
123893
- try {
123894
- const raw = await this.spawnCLI(prompt, jsonSchemaStr, opts);
123895
- this.dumpDebug(label, prompt, raw, jsonSchemaStr);
123896
- return this.parseAndValidate(raw, schema2);
123897
- } catch (err) {
123898
- lastError = err;
123899
- if (this._abortSignal?.aborted)
123900
- throw lastError;
123901
- if (attempt < this.maxRetries) {
123902
- log.warn(`[CLI] Attempt ${attempt + 1} failed, retrying... (${lastError.message})`);
124070
+ return this.limit(async () => {
124071
+ if (this._abortSignal?.aborted) {
124072
+ throw this._abortSignal.reason ?? new DOMException("Analysis cancelled", "AbortError");
124073
+ }
124074
+ opts?.onStart?.();
124075
+ const jsonSchemaStr = this.toJsonSchema(schema2);
124076
+ const label = opts?.label ?? "call";
124077
+ let lastError = null;
124078
+ for (let attempt = 0; attempt <= this.maxRetries; attempt++) {
124079
+ try {
124080
+ const raw = await this.spawnCLI(prompt, jsonSchemaStr, opts);
124081
+ this.dumpDebug(label, prompt, raw, jsonSchemaStr);
124082
+ return this.parseAndValidate(raw, schema2);
124083
+ } catch (err) {
124084
+ lastError = err;
124085
+ if (this._abortSignal?.aborted)
124086
+ throw lastError;
124087
+ if (attempt < this.maxRetries) {
124088
+ log.warn(`[CLI] Attempt ${attempt + 1} failed, retrying... (${lastError.message})`);
124089
+ }
123903
124090
  }
123904
124091
  }
123905
- }
123906
- throw lastError;
124092
+ throw lastError;
124093
+ });
123907
124094
  }
123908
124095
  // ---------------------------------------------------------------------------
123909
124096
  // LLMProvider implementation
123910
124097
  // ---------------------------------------------------------------------------
123911
- async generateServiceViolations(context) {
124098
+ async generateServiceViolations(context, opts) {
123912
124099
  const { vars, idMap } = buildServiceTemplateVars(context);
123913
124100
  const prompt = getPrompt("violations-service", vars);
123914
124101
  log.info("[CLI] Service violations call starting...");
123915
124102
  const t0 = Date.now();
123916
124103
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, ServiceViolationOutputSchema, {
123917
124104
  extraArgs: ["--tools", ""],
123918
- label: "service"
124105
+ label: "service",
124106
+ onStart: opts?.onStart
123919
124107
  });
123920
124108
  const dur = Date.now() - t0;
123921
124109
  log.info(`[CLI] Service violations call done in ${dur}ms \u2014 ${object.violations.length} violations`);
@@ -123938,14 +124126,15 @@ var init_cli_provider = __esm({
123938
124126
  }))
123939
124127
  };
123940
124128
  }
123941
- async generateDatabaseViolations(context) {
124129
+ async generateDatabaseViolations(context, opts) {
123942
124130
  const { vars, idMap } = buildDatabaseTemplateVars(context);
123943
124131
  const prompt = getPrompt("violations-database", vars);
123944
124132
  log.info("[CLI] Database violations call starting...");
123945
124133
  const t0 = Date.now();
123946
124134
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, DatabaseViolationOutputSchema, {
123947
124135
  extraArgs: ["--tools", ""],
123948
- label: "database"
124136
+ label: "database",
124137
+ onStart: opts?.onStart
123949
124138
  });
123950
124139
  const dur = Date.now() - t0;
123951
124140
  log.info(`[CLI] Database violations call done in ${dur}ms \u2014 ${object.violations.length} violations`);
@@ -123965,7 +124154,7 @@ var init_cli_provider = __esm({
123965
124154
  }))
123966
124155
  };
123967
124156
  }
123968
- async generateModuleViolations(context) {
124157
+ async generateModuleViolations(context, opts) {
123969
124158
  const { vars, idMap } = buildModuleTemplateVars(context);
123970
124159
  const prompt = getPrompt("violations-module", vars);
123971
124160
  const moduleIdToServiceId = new Map(context.modules.filter((m) => m.serviceId).map((m) => [m.id, m.serviceId]));
@@ -123975,7 +124164,8 @@ var init_cli_provider = __esm({
123975
124164
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, ModuleViolationOutputSchema, {
123976
124165
  extraArgs: ["--tools", ""],
123977
124166
  label: "module",
123978
- timeoutMs: moduleTimeoutMs
124167
+ timeoutMs: moduleTimeoutMs,
124168
+ onStart: opts?.onStart
123979
124169
  });
123980
124170
  const dur = Date.now() - t0;
123981
124171
  log.info(`[CLI] Module violations call done in ${dur}ms \u2014 ${object.violations.length} violations`);
@@ -124002,15 +124192,23 @@ var init_cli_provider = __esm({
124002
124192
  }
124003
124193
  async generateAllViolations(contexts) {
124004
124194
  const onStepComplete = contexts.onStepComplete;
124195
+ const onCallStart = contexts.onCallStart;
124196
+ const onCallDone = contexts.onCallDone;
124005
124197
  const promises = [];
124006
124198
  if (contexts.service) {
124007
- promises.push(["service", this.generateServiceViolations(contexts.service)]);
124199
+ promises.push(["service", this.generateServiceViolations(contexts.service, {
124200
+ onStart: () => onCallStart?.("service")
124201
+ })]);
124008
124202
  }
124009
124203
  if (contexts.database) {
124010
- promises.push(["database", this.generateDatabaseViolations(contexts.database)]);
124204
+ promises.push(["database", this.generateDatabaseViolations(contexts.database, {
124205
+ onStart: () => onCallStart?.("database")
124206
+ })]);
124011
124207
  }
124012
124208
  if (contexts.module) {
124013
- promises.push(["module", this.generateModuleViolations(contexts.module)]);
124209
+ promises.push(["module", this.generateModuleViolations(contexts.module, {
124210
+ onStart: () => onCallStart?.("module")
124211
+ })]);
124014
124212
  }
124015
124213
  const stepLabels = {
124016
124214
  service: "Service architecture checks done",
@@ -124019,7 +124217,11 @@ var init_cli_provider = __esm({
124019
124217
  };
124020
124218
  const settled = await Promise.allSettled(promises.map(([key, p2]) => p2.then((v) => {
124021
124219
  onStepComplete?.(stepLabels[key] || `${key} done`);
124220
+ onCallDone?.(key, true);
124022
124221
  return v;
124222
+ }, (err) => {
124223
+ onCallDone?.(key, false);
124224
+ throw err;
124023
124225
  })));
124024
124226
  const result = {};
124025
124227
  for (let i = 0; i < promises.length; i++) {
@@ -124034,6 +124236,7 @@ var init_cli_provider = __esm({
124034
124236
  return result;
124035
124237
  }
124036
124238
  async generateAllViolationsWithLifecycle(contexts, onStepComplete) {
124239
+ const onCallStart = contexts.onCallStart;
124037
124240
  const allResolved = [];
124038
124241
  const allNew = [];
124039
124242
  let serviceDescriptions = [];
@@ -124050,7 +124253,8 @@ var init_cli_provider = __esm({
124050
124253
  const t0 = Date.now();
124051
124254
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, LifecycleServiceOutputSchema, {
124052
124255
  extraArgs: ["--tools", ""],
124053
- label: "service-lifecycle"
124256
+ label: "service-lifecycle",
124257
+ onStart: () => onCallStart?.("service")
124054
124258
  });
124055
124259
  const dur = Date.now() - t0;
124056
124260
  log.info(`[CLI] Lifecycle service call done in ${dur}ms \u2014 resolved: ${object.resolvedViolationIds.length}, new: ${object.newViolations.length}`);
@@ -124058,7 +124262,9 @@ var init_cli_provider = __esm({
124058
124262
  return object;
124059
124263
  })()]);
124060
124264
  } else {
124061
- promises.push(["service-normal", this.generateServiceViolations(ctx)]);
124265
+ promises.push(["service-normal", this.generateServiceViolations(ctx, {
124266
+ onStart: () => onCallStart?.("service")
124267
+ })]);
124062
124268
  }
124063
124269
  }
124064
124270
  if (contexts.database) {
@@ -124072,7 +124278,8 @@ var init_cli_provider = __esm({
124072
124278
  const t0 = Date.now();
124073
124279
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, DiffViolationOutputSchema, {
124074
124280
  extraArgs: ["--tools", ""],
124075
- label: "database-lifecycle"
124281
+ label: "database-lifecycle",
124282
+ onStart: () => onCallStart?.("database")
124076
124283
  });
124077
124284
  const dur = Date.now() - t0;
124078
124285
  log.info(`[CLI] Lifecycle database call done in ${dur}ms \u2014 resolved: ${object.resolvedViolationIds.length}, new: ${object.newViolations.length}`);
@@ -124080,7 +124287,9 @@ var init_cli_provider = __esm({
124080
124287
  return object;
124081
124288
  })()]);
124082
124289
  } else {
124083
- promises.push(["database-normal", this.generateDatabaseViolations(ctx)]);
124290
+ promises.push(["database-normal", this.generateDatabaseViolations(ctx, {
124291
+ onStart: () => onCallStart?.("database")
124292
+ })]);
124084
124293
  }
124085
124294
  }
124086
124295
  if (contexts.module) {
@@ -124095,7 +124304,8 @@ var init_cli_provider = __esm({
124095
124304
  const t0 = Date.now();
124096
124305
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, DiffViolationOutputSchema, {
124097
124306
  extraArgs: ["--tools", ""],
124098
- label: "module-lifecycle"
124307
+ label: "module-lifecycle",
124308
+ onStart: () => onCallStart?.("module")
124099
124309
  });
124100
124310
  const dur = Date.now() - t0;
124101
124311
  log.info(`[CLI] Lifecycle module call done in ${dur}ms \u2014 resolved: ${object.resolvedViolationIds.length}, new: ${object.newViolations.length}`);
@@ -124116,7 +124326,9 @@ var init_cli_provider = __esm({
124116
124326
  };
124117
124327
  })()]);
124118
124328
  } else {
124119
- promises.push(["module-normal", this.generateModuleViolations(ctx)]);
124329
+ promises.push(["module-normal", this.generateModuleViolations(ctx, {
124330
+ onStart: () => onCallStart?.("module")
124331
+ })]);
124120
124332
  }
124121
124333
  }
124122
124334
  const stepLabels = {
@@ -124127,9 +124339,15 @@ var init_cli_provider = __esm({
124127
124339
  module: "Module checks done",
124128
124340
  "module-normal": "Module checks done"
124129
124341
  };
124342
+ const baseKey = (key) => key.replace("-normal", "");
124343
+ const onCallDone = contexts.onCallDone;
124130
124344
  const settled = await Promise.allSettled(promises.map(([key, p2]) => p2.then((v) => {
124131
124345
  onStepComplete?.(stepLabels[key] || `${key} done`);
124346
+ onCallDone?.(baseKey(key), true);
124132
124347
  return v;
124348
+ }, (err) => {
124349
+ onCallDone?.(baseKey(key), false);
124350
+ throw err;
124133
124351
  })));
124134
124352
  for (let i = 0; i < promises.length; i++) {
124135
124353
  const [key] = promises[i];
@@ -124221,7 +124439,7 @@ var init_cli_provider = __esm({
124221
124439
  }
124222
124440
  return { resolvedViolationIds: allResolved, newViolations: allNew, serviceDescriptions };
124223
124441
  }
124224
- async generateCodeViolations(context) {
124442
+ async generateCodeViolations(context, opts) {
124225
124443
  const hasExisting = context.existingViolations && context.existingViolations.length > 0;
124226
124444
  let promptName;
124227
124445
  if (context.tier === "metadata") {
@@ -124242,7 +124460,8 @@ var init_cli_provider = __esm({
124242
124460
  const { data: object2, usage: cliUsage2 } = await this.spawnAndParse(prompt, CodeViolationLifecycleOutputSchema, {
124243
124461
  extraArgs: codeExtraArgs,
124244
124462
  label: "code-lifecycle",
124245
- timeoutMs: codeTimeoutMs
124463
+ timeoutMs: codeTimeoutMs,
124464
+ onStart: opts?.onStart
124246
124465
  });
124247
124466
  const dur2 = Date.now() - t0;
124248
124467
  log.info(`[CLI] Code violations call done in ${dur2}ms \u2014 new: ${object2.newViolations.length}, resolved: ${object2.resolvedViolationIds.length}, unchanged: ${object2.unchangedViolationIds.length}`);
@@ -124265,7 +124484,8 @@ var init_cli_provider = __esm({
124265
124484
  const { data: object, usage: cliUsage } = await this.spawnAndParse(prompt, CodeViolationOutputSchema, {
124266
124485
  extraArgs: codeExtraArgs,
124267
124486
  label: "code",
124268
- timeoutMs: codeTimeoutMs
124487
+ timeoutMs: codeTimeoutMs,
124488
+ onStart: opts?.onStart
124269
124489
  });
124270
124490
  const dur = Date.now() - t0;
124271
124491
  log.info(`[CLI] Code violations call done in ${dur}ms \u2014 ${object.violations.length} violations`);
@@ -124755,7 +124975,7 @@ var init_context_router = __esm({
124755
124975
  });
124756
124976
 
124757
124977
  // apps/server/dist/services/violation.service.js
124758
- async function generateViolations(input, onProgress, externalProvider) {
124978
+ async function generateViolations(input, onProgress, externalProvider, onCallStart, onCallDone) {
124759
124979
  const provider = externalProvider ?? createLLMProvider();
124760
124980
  const archRules = (input.llmRules || []).filter((r) => r.category === "service");
124761
124981
  const dbRules = (input.llmRules || []).filter((r) => r.category === "database");
@@ -124799,7 +125019,9 @@ async function generateViolations(input, onProgress, externalProvider) {
124799
125019
  service: serviceContext,
124800
125020
  database: dbContext,
124801
125021
  module: moduleContext,
124802
- onStepComplete: onProgress
125022
+ onStepComplete: onProgress,
125023
+ onCallStart,
125024
+ onCallDone
124803
125025
  });
124804
125026
  const allViolations = [];
124805
125027
  let serviceDescriptions = [];
@@ -124836,7 +125058,7 @@ async function generateViolations(input, onProgress, externalProvider) {
124836
125058
  }
124837
125059
  return { violations: allViolations, serviceDescriptions };
124838
125060
  }
124839
- async function generateViolationsWithLifecycle(input, onProgress, externalProvider) {
125061
+ async function generateViolationsWithLifecycle(input, onProgress, externalProvider, onCallStart, onCallDone) {
124840
125062
  const provider = externalProvider ?? createLLMProvider();
124841
125063
  const archRules = (input.llmRules || []).filter((r) => r.category === "service");
124842
125064
  const dbRules = (input.llmRules || []).filter((r) => r.category === "database");
@@ -124877,7 +125099,9 @@ async function generateViolationsWithLifecycle(input, onProgress, externalProvid
124877
125099
  const result = await provider.generateAllViolationsWithLifecycle({
124878
125100
  service: serviceContext,
124879
125101
  database: dbContext,
124880
- module: moduleContext
125102
+ module: moduleContext,
125103
+ onCallStart,
125104
+ onCallDone
124881
125105
  }, (step) => {
124882
125106
  onProgress?.(step);
124883
125107
  });
@@ -125142,6 +125366,48 @@ function throwIfAborted(signal) {
125142
125366
  if (signal?.aborted)
125143
125367
  throw new DOMException("Analysis cancelled", "AbortError");
125144
125368
  }
125369
+ function formatElapsed(ms) {
125370
+ const totalSec = Math.floor(ms / 1e3);
125371
+ const min = Math.floor(totalSec / 60);
125372
+ const sec = totalSec % 60;
125373
+ return min === 0 ? `${sec}s` : `${min}m ${sec}s`;
125374
+ }
125375
+ function renderLlmDetail(s) {
125376
+ const parts = [];
125377
+ if (s.detCount > 0)
125378
+ parts.push(`${s.detCount} det`);
125379
+ parts.push(`LLM ${s.done}/${s.total}`);
125380
+ if (s.running > 0)
125381
+ parts.push(`${s.running} running`);
125382
+ if (s.elapsedMs >= 1e3)
125383
+ parts.push(formatElapsed(s.elapsedMs));
125384
+ return parts.join(" \xB7 ");
125385
+ }
125386
+ function createLlmTracker(tracker, domain, detCount, total) {
125387
+ let done = 0;
125388
+ let running = 0;
125389
+ const t0 = Date.now();
125390
+ const render = () => tracker?.detail(domain, renderLlmDetail({
125391
+ detCount,
125392
+ total,
125393
+ done,
125394
+ running,
125395
+ elapsedMs: Date.now() - t0
125396
+ }));
125397
+ return {
125398
+ initialDetail: renderLlmDetail({ detCount, total, done: 0, running: 0, elapsedMs: 0 }),
125399
+ onCallStart: () => {
125400
+ running++;
125401
+ render();
125402
+ },
125403
+ onCallDone: (started) => {
125404
+ if (started)
125405
+ running--;
125406
+ done++;
125407
+ render();
125408
+ }
125409
+ };
125410
+ }
125145
125411
  function getDetComparisonKey(v) {
125146
125412
  return `${v.ruleKey}::${v.serviceName}::${v.moduleName || ""}::${v.methodName || ""}::${v.title}`;
125147
125413
  }
@@ -125480,12 +125746,6 @@ async function runViolationPipeline(input) {
125480
125746
  const allNewLlmItems = [];
125481
125747
  const allResolvedLlmIds = [];
125482
125748
  const hasArchLlm = enableLlmRules !== false && !llmSkipped;
125483
- if (hasArchLlm)
125484
- tracker?.start("architecture", "Running LLM analysis...");
125485
- for (const [domain] of domainCodeBatches) {
125486
- const detCount = violationsByDomain.get(domain) ?? 0;
125487
- tracker?.start(domain, detCount > 0 ? `${detCount} det, running LLM...` : "Running LLM...");
125488
- }
125489
125749
  const previousDetForComparison = previousDetViolations.map((v) => ({
125490
125750
  ruleKey: v.ruleKey,
125491
125751
  serviceName: v.targetServiceName || "",
@@ -125712,13 +125972,42 @@ async function runViolationPipeline(input) {
125712
125972
  existingDatabaseViolations: void 0,
125713
125973
  existingModuleViolations: hasLlmOnlyExistingViolations ? existingModuleViolations : void 0
125714
125974
  };
125975
+ const llmTrackers = /* @__PURE__ */ new Map();
125976
+ for (const [domain, batches] of domainCodeBatches) {
125977
+ const detCount = violationsByDomain.get(domain) ?? 0;
125978
+ const ll = createLlmTracker(tracker, domain, detCount, batches.length);
125979
+ llmTrackers.set(domain, ll);
125980
+ tracker?.start(domain, ll.initialDetail);
125981
+ }
125982
+ if (dbSchemaContext && !llmSkipped && !domainCodeBatches.has("database")) {
125983
+ const detCount = violationsByDomain.get("database") ?? 0;
125984
+ const ll = createLlmTracker(tracker, "database", detCount, 1);
125985
+ llmTrackers.set("database", ll);
125986
+ tracker?.start("database", ll.initialDetail);
125987
+ }
125988
+ if (hasArchLlm) {
125989
+ const detCount = violationsByDomain.get("architecture") ?? 0;
125990
+ const archTotal2 = 1 + (violationModules && violationModules.length > 0 ? 1 : 0);
125991
+ const ll = createLlmTracker(tracker, "architecture", detCount, archTotal2);
125992
+ llmTrackers.set("architecture", ll);
125993
+ tracker?.start("architecture", ll.initialDetail);
125994
+ }
125715
125995
  const domainLlmPromises = [];
125716
125996
  for (const [domain, batches] of domainCodeBatches) {
125717
125997
  domainLlmPromises.push((async () => {
125718
125998
  const detCount = violationsByDomain.get(domain) ?? 0;
125719
125999
  log.info(`[LLM] ${domain}: starting (${batches.length} code batches)`);
125720
126000
  const t0 = Date.now();
125721
- const codeResults = await Promise.allSettled(batches.map((b) => provider.generateCodeViolations(b)));
126001
+ const ll = llmTrackers.get(domain);
126002
+ const codeResults = await Promise.allSettled(batches.map((b) => {
126003
+ let started = false;
126004
+ return provider.generateCodeViolations(b, {
126005
+ onStart: () => {
126006
+ started = true;
126007
+ ll.onCallStart();
126008
+ }
126009
+ }).finally(() => ll.onCallDone(started));
126010
+ }));
125722
126011
  const rawViolations = [];
125723
126012
  const resolvedIds = [];
125724
126013
  const unchangedIds = [];
@@ -125744,15 +126033,19 @@ async function runViolationPipeline(input) {
125744
126033
  }
125745
126034
  let dbSchemaViolations = [];
125746
126035
  if (dbSchemaContext && !llmSkipped) {
125747
- if (!domainCodeBatches.has("database")) {
125748
- const detCount = violationsByDomain.get("database") ?? 0;
125749
- tracker?.start("database", detCount > 0 ? `${detCount} det, running LLM...` : "Running LLM...");
125750
- }
126036
+ const schemaLl = domainCodeBatches.has("database") ? void 0 : llmTrackers.get("database");
125751
126037
  domainLlmPromises.push((async () => {
125752
126038
  log.info(`[LLM] database-schema: starting`);
125753
126039
  const t0 = Date.now();
126040
+ let started = false;
125754
126041
  try {
125755
- const dbResult = await provider.generateDatabaseViolations(dbSchemaContext);
126042
+ const dbResult = await provider.generateDatabaseViolations(dbSchemaContext, {
126043
+ onStart: () => {
126044
+ started = true;
126045
+ schemaLl?.onCallStart();
126046
+ }
126047
+ });
126048
+ schemaLl?.onCallDone(started);
125756
126049
  const dur = Date.now() - t0;
125757
126050
  log.info(`[LLM] database-schema: done in ${dur}ms \u2014 ${dbResult.violations.length} violations`);
125758
126051
  for (const v of dbResult.violations) {
@@ -125792,6 +126085,7 @@ async function runViolationPipeline(input) {
125792
126085
  }
125793
126086
  return { domain: "database-schema", violations: [], resolvedIds: [], unchangedIds: [] };
125794
126087
  } catch (err) {
126088
+ schemaLl?.onCallDone(started);
125795
126089
  const dur = Date.now() - t0;
125796
126090
  log.warn(`[LLM] database-schema: failed in ${dur}ms \u2014 ${err instanceof Error ? err.message : String(err)}`);
125797
126091
  if (!domainCodeBatches.has("database"))
@@ -125805,8 +126099,17 @@ async function runViolationPipeline(input) {
125805
126099
  const llmRulePromise = (async () => {
125806
126100
  if (enableLlmRules === false || llmSkipped)
125807
126101
  return;
126102
+ const archLl = llmTrackers.get("architecture");
126103
+ const archStarted = /* @__PURE__ */ new Set();
126104
+ const archOnCallStart = (key) => {
126105
+ archStarted.add(key);
126106
+ archLl?.onCallStart();
126107
+ };
126108
+ const archOnCallDone = (key) => {
126109
+ archLl?.onCallDone(archStarted.has(key));
126110
+ };
125808
126111
  if (hasLlmOnlyExistingViolations) {
125809
- const archResult = await generateViolationsWithLifecycle(violationInput, (step) => tracker?.detail("architecture", step), provider);
126112
+ const archResult = await generateViolationsWithLifecycle(violationInput, void 0, provider, archOnCallStart, archOnCallDone);
125810
126113
  serviceDescriptions = archResult.serviceDescriptions;
125811
126114
  allResolvedLlmIds.push(...archResult.resolvedViolationIds);
125812
126115
  allNewLlmItems.push(...archResult.newViolations);
@@ -125828,7 +126131,7 @@ async function runViolationPipeline(input) {
125828
126131
  resolved.push(...lifecycle.resolved);
125829
126132
  resolvedRefs.push(...lifecycle.resolvedRefs);
125830
126133
  } else {
125831
- const archResult = await generateViolations(violationInput, (step) => tracker?.detail("architecture", step), provider);
126134
+ const archResult = await generateViolations(violationInput, void 0, provider, archOnCallStart, archOnCallDone);
125832
126135
  serviceDescriptions = archResult.serviceDescriptions;
125833
126136
  for (const v of archResult.violations) {
125834
126137
  added.push({
@@ -126632,8 +126935,11 @@ import path16 from "node:path";
126632
126935
  init_analysis_store();
126633
126936
  init_analyze_core();
126634
126937
  init_analyze_persist();
126938
+ init_config2();
126939
+ init_logger();
126635
126940
  async function analyzeInProcess(project, options = {}) {
126636
126941
  const startedAt = Date.now();
126942
+ log.info(`[LLM] Provider: claude-code, model: ${config.claudeCodeModel || "default"}, maxConcurrency: ${config.claudeCodeMaxConcurrency}`);
126637
126943
  const core2 = await analyzeCore(project, { ...options, mode: "full" });
126638
126944
  return persistFullAnalysis(project, core2, startedAt);
126639
126945
  }
@@ -130730,7 +131036,7 @@ async function runHooksRun() {
130730
131036
 
130731
131037
  // tools/cli/src/index.ts
130732
131038
  var program2 = new Command();
130733
- program2.name("truecourse").version("0.5.3").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
131039
+ program2.name("truecourse").version("0.5.5").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
130734
131040
  var dashboardCmd = program2.command("dashboard").description("Start the TrueCourse dashboard and open it in your browser").option("--reconfigure", "Re-prompt for console vs background service mode").option("--service", "Run as a background service (skips mode prompt)").option("--console", "Run in this terminal (skips mode prompt)").action(async (options) => {
130735
131041
  if (options.service && options.console) {
130736
131042
  console.error("error: --service and --console are mutually exclusive");