truecourse 0.6.0-next.8 → 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 (4) hide show
  1. package/README.md +4 -2
  2. package/cli.mjs +296 -51
  3. package/package.json +1 -1
  4. package/server.mjs +247 -39
package/README.md CHANGED
@@ -256,7 +256,8 @@ truecourse spec docs uninclude <path>
256
256
 
257
257
  # Contract extraction (canonical spec → .tc artifacts)
258
258
  truecourse contracts generate # Extract / re-extract TC contract files
259
- truecourse contracts list # List generated contracts
259
+ truecourse contracts list # List artifacts (kind · identity · location)
260
+ truecourse contracts list --inferred / --authored # Only reverse-engineered (_inferred/) / only authored
260
261
  truecourse contracts validate # Parse + resolve TC files; report unresolved refs
261
262
 
262
263
  # Verification (code against contracts)
@@ -270,6 +271,7 @@ truecourse drifts list --offset 20 / --severity critical,high # Page through /
270
271
  # Inference (code → inferred contracts) — reverse-engineer undocumented decisions
271
272
  truecourse infer # Write inferred .tc files to contracts/_inferred/
272
273
  truecourse infer --dry-run # Report what would be written, touch nothing
274
+ truecourse contracts list --inferred # Review what infer produced (kind · confidence · code location)
273
275
  ```
274
276
 
275
277
  ---
@@ -364,7 +366,7 @@ Patterns are anchored to the file's location, so `src/generated/` matches the to
364
366
 
365
367
  ## Telemetry
366
368
 
367
- TrueCourse collects anonymous usage data (event type, language, file count range, OS) to improve the product. No source code, file paths, or violation details are collected. It is automatically disabled in CI environments.
369
+ TrueCourse collects anonymous usage data to improve the product — one event per command (`analyze`, `spec_scan`, `contracts_generate`, `verify`, `infer`), each carrying only coarse, bucketed counts (file/artifact/drift/decision *ranges*, duration range), the surface (CLI vs dashboard), OS, and tool version. No source code, file paths, identities, or violation/drift details are collected. It is automatically disabled in CI environments.
368
370
 
369
371
  ```bash
370
372
  truecourse telemetry status # Check telemetry status
package/cli.mjs CHANGED
@@ -12451,10 +12451,10 @@ function computeFunctionMetrics(node2) {
12451
12451
  if (!bodyNode) {
12452
12452
  return { lineCount, statementCount: 0, maxNestingDepth: 0 };
12453
12453
  }
12454
- let statementCount = 0;
12454
+ let statementCount2 = 0;
12455
12455
  for (const child of bodyNode.namedChildren) {
12456
12456
  if (STATEMENT_NODE_TYPES.has(child.type)) {
12457
- statementCount++;
12457
+ statementCount2++;
12458
12458
  }
12459
12459
  }
12460
12460
  let maxNestingDepth2 = 0;
@@ -12471,7 +12471,7 @@ function computeFunctionMetrics(node2) {
12471
12471
  }
12472
12472
  }
12473
12473
  walkNesting(bodyNode, 0);
12474
- return { lineCount, statementCount, maxNestingDepth: maxNestingDepth2 };
12474
+ return { lineCount, statementCount: statementCount2, maxNestingDepth: maxNestingDepth2 };
12475
12475
  }
12476
12476
  var NESTING_NODE_TYPES, STATEMENT_NODE_TYPES;
12477
12477
  var init_common = __esm({
@@ -13369,10 +13369,10 @@ function computePythonFunctionMetrics(node2) {
13369
13369
  if (!bodyNode) {
13370
13370
  return { lineCount, statementCount: 0, maxNestingDepth: 0 };
13371
13371
  }
13372
- let statementCount = 0;
13372
+ let statementCount2 = 0;
13373
13373
  for (const child of bodyNode.namedChildren) {
13374
13374
  if (PYTHON_STATEMENT_NODE_TYPES.has(child.type)) {
13375
- statementCount++;
13375
+ statementCount2++;
13376
13376
  }
13377
13377
  }
13378
13378
  let maxNestingDepth2 = 0;
@@ -13389,7 +13389,7 @@ function computePythonFunctionMetrics(node2) {
13389
13389
  }
13390
13390
  }
13391
13391
  walkNesting(bodyNode, 0);
13392
- return { lineCount, statementCount, maxNestingDepth: maxNestingDepth2 };
13392
+ return { lineCount, statementCount: statementCount2, maxNestingDepth: maxNestingDepth2 };
13393
13393
  }
13394
13394
  function isNestedInFunction3(node2) {
13395
13395
  let current = node2.parent;
@@ -48980,6 +48980,19 @@ var init_prototype_pollution = __esm({
48980
48980
  });
48981
48981
 
48982
48982
  // packages/analyzer/dist/rules/bugs/visitors/javascript/void-zero-argument.js
48983
+ function operandIsCall(operand) {
48984
+ let cur = operand;
48985
+ while (cur) {
48986
+ if (cur.type === "call_expression" || cur.type === "new_expression")
48987
+ return true;
48988
+ if (cur.type === "parenthesized_expression" || cur.type === "await_expression") {
48989
+ cur = cur.namedChildren[cur.namedChildren.length - 1] ?? null;
48990
+ continue;
48991
+ }
48992
+ return false;
48993
+ }
48994
+ return false;
48995
+ }
48983
48996
  var voidZeroArgumentVisitor;
48984
48997
  var init_void_zero_argument = __esm({
48985
48998
  "packages/analyzer/dist/rules/bugs/visitors/javascript/void-zero-argument.js"() {
@@ -48994,6 +49007,8 @@ var init_void_zero_argument = __esm({
48994
49007
  const op = node2.children.find((c2) => c2.text === "void");
48995
49008
  if (!op)
48996
49009
  return null;
49010
+ if (operandIsCall(node2.children[node2.children.length - 1]))
49011
+ return null;
48997
49012
  return makeViolation(this.ruleKey, node2, filePath, "medium", "Unnecessary void expression", `\`${node2.text}\` can be replaced with \`undefined\` directly.`, sourceCode, "Use `undefined` instead of `void 0`.");
48998
49013
  }
48999
49014
  };
@@ -51816,12 +51831,34 @@ function isModuleLevel(node2) {
51816
51831
  function isMutableInit(node2) {
51817
51832
  return node2.type === "object" || node2.type === "array" || node2.type === "new_expression";
51818
51833
  }
51819
- var sharedMutableModuleStateVisitor;
51834
+ function isClientSideFile(filePath, program3) {
51835
+ for (const tok of CLIENT_PATH_TOKENS) {
51836
+ if (filePath.includes(tok))
51837
+ return true;
51838
+ }
51839
+ const first2 = program3?.namedChildren[0];
51840
+ if (first2?.type === "expression_statement") {
51841
+ const text = first2.text.trim().replace(/;$/, "").replace(/['"`]/g, "");
51842
+ if (text === "use client")
51843
+ return true;
51844
+ }
51845
+ return false;
51846
+ }
51847
+ var CLIENT_PATH_TOKENS, sharedMutableModuleStateVisitor;
51820
51848
  var init_shared_mutable_module_state = __esm({
51821
51849
  "packages/analyzer/dist/rules/bugs/visitors/javascript/shared-mutable-module-state.js"() {
51822
51850
  "use strict";
51823
51851
  init_types();
51824
51852
  init_helpers2();
51853
+ CLIENT_PATH_TOKENS = [
51854
+ "/client/",
51855
+ "/client-only/",
51856
+ "/hooks/",
51857
+ "/primitives/",
51858
+ "/components/",
51859
+ ".client.ts",
51860
+ ".client.tsx"
51861
+ ];
51825
51862
  sharedMutableModuleStateVisitor = {
51826
51863
  ruleKey: "bugs/deterministic/shared-mutable-module-state",
51827
51864
  languages: JS_LANGUAGES,
@@ -51829,6 +51866,11 @@ var init_shared_mutable_module_state = __esm({
51829
51866
  visit(node2, filePath, sourceCode) {
51830
51867
  if (!isModuleLevel(node2))
51831
51868
  return null;
51869
+ let program3 = node2;
51870
+ while (program3 && program3.type !== "program")
51871
+ program3 = program3.parent;
51872
+ if (isClientSideFile(filePath, program3))
51873
+ return null;
51832
51874
  const kindChild = node2.children[0];
51833
51875
  if (!kindChild || kindChild.text === "const")
51834
51876
  return null;
@@ -53917,13 +53959,38 @@ var init_switch_exhaustiveness = __esm({
53917
53959
  });
53918
53960
 
53919
53961
  // packages/analyzer/dist/rules/bugs/visitors/javascript/non-number-arithmetic.js
53920
- var ARITHMETIC_OPS, nonNumberArithmeticVisitor;
53962
+ function isConfidentlyNonNumeric(t2) {
53963
+ if (CONFIDENTLY_NON_NUMERIC.has(t2))
53964
+ return true;
53965
+ if (/^["']/.test(t2))
53966
+ return true;
53967
+ if (t2 === "true" || t2 === "false")
53968
+ return true;
53969
+ if (/\[\]$/.test(t2) || /^readonly\s/.test(t2))
53970
+ return true;
53971
+ if (t2.includes("=>") || t2.startsWith("{"))
53972
+ return true;
53973
+ return false;
53974
+ }
53975
+ var ARITHMETIC_OPS, CONFIDENTLY_NON_NUMERIC, nonNumberArithmeticVisitor;
53921
53976
  var init_non_number_arithmetic = __esm({
53922
53977
  "packages/analyzer/dist/rules/bugs/visitors/javascript/non-number-arithmetic.js"() {
53923
53978
  "use strict";
53924
53979
  init_types();
53925
53980
  init_helpers2();
53926
53981
  ARITHMETIC_OPS = /* @__PURE__ */ new Set(["-", "*", "/", "%", "**"]);
53982
+ CONFIDENTLY_NON_NUMERIC = /* @__PURE__ */ new Set([
53983
+ "string",
53984
+ "boolean",
53985
+ "null",
53986
+ "undefined",
53987
+ "void",
53988
+ "symbol",
53989
+ "never",
53990
+ "object",
53991
+ // Common runtime types that don't auto-coerce to number in arithmetic.
53992
+ "Date"
53993
+ ]);
53927
53994
  nonNumberArithmeticVisitor = {
53928
53995
  ruleKey: "bugs/deterministic/non-number-arithmetic",
53929
53996
  languages: TS_LANGUAGES,
@@ -53943,16 +54010,14 @@ var init_non_number_arithmetic = __esm({
53943
54010
  const rightType = typeQuery.getTypeAtPosition(filePath, right.startPosition.row, right.startPosition.column, right.endPosition.row, right.endPosition.column);
53944
54011
  if (!leftType || !rightType)
53945
54012
  return null;
53946
- const numericTypes = /* @__PURE__ */ new Set(["number", "bigint", "any"]);
53947
- const leftOk = numericTypes.has(leftType) || /^\d+$/.test(leftType);
53948
- const rightOk = numericTypes.has(rightType) || /^\d+$/.test(rightType);
53949
54013
  if (left.type === "number" || right.type === "number")
53950
54014
  return null;
53951
- if (!leftOk || !rightOk) {
53952
- const badSide = !leftOk ? `left operand is \`${leftType}\`` : `right operand is \`${rightType}\``;
53953
- return makeViolation(this.ruleKey, node2, filePath, "high", "Non-numeric value in arithmetic", `Arithmetic operator \`${operator.text}\` used with non-numeric operand \u2014 ${badSide}. This will produce \`NaN\` at runtime.`, sourceCode, "Convert operands to numbers or fix the expression.");
53954
- }
53955
- return null;
54015
+ const leftBad = isConfidentlyNonNumeric(leftType);
54016
+ const rightBad = isConfidentlyNonNumeric(rightType);
54017
+ if (!leftBad && !rightBad)
54018
+ return null;
54019
+ const badSide = leftBad ? `left operand is \`${leftType}\`` : `right operand is \`${rightType}\``;
54020
+ return makeViolation(this.ruleKey, node2, filePath, "high", "Non-numeric value in arithmetic", `Arithmetic operator \`${operator.text}\` used with non-numeric operand \u2014 ${badSide}. This will produce \`NaN\` at runtime.`, sourceCode, "Convert operands to numbers or fix the expression.");
53956
54021
  }
53957
54022
  };
53958
54023
  }
@@ -66811,12 +66876,29 @@ var init_no_var_declaration = __esm({
66811
66876
  });
66812
66877
 
66813
66878
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/cognitive-complexity.js
66814
- var cognitiveComplexityVisitor;
66879
+ function isInsideJsxExpression(node2, functionNodeId) {
66880
+ let cur = node2.parent;
66881
+ while (cur && cur.id !== functionNodeId) {
66882
+ if (JSX_EXPRESSION_TYPES.has(cur.type))
66883
+ return true;
66884
+ if (JS_FUNCTION_TYPES.includes(cur.type))
66885
+ return false;
66886
+ cur = cur.parent;
66887
+ }
66888
+ return false;
66889
+ }
66890
+ var JSX_EXPRESSION_TYPES, cognitiveComplexityVisitor;
66815
66891
  var init_cognitive_complexity = __esm({
66816
66892
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/cognitive-complexity.js"() {
66817
66893
  "use strict";
66818
66894
  init_types();
66819
66895
  init_helpers4();
66896
+ JSX_EXPRESSION_TYPES = /* @__PURE__ */ new Set([
66897
+ "jsx_expression",
66898
+ // {expr} attribute / child
66899
+ "jsx_attribute"
66900
+ // attr={expr}
66901
+ ]);
66820
66902
  cognitiveComplexityVisitor = {
66821
66903
  ruleKey: "code-quality/deterministic/cognitive-complexity",
66822
66904
  languages: ["typescript", "tsx", "javascript"],
@@ -66832,14 +66914,17 @@ var init_cognitive_complexity = __esm({
66832
66914
  if (JS_FUNCTION_TYPES.includes(n.type) && n.id !== node2.id)
66833
66915
  return;
66834
66916
  if (INCREMENT_TYPES.has(n.type)) {
66835
- complexity += 1 + nesting;
66917
+ if (n.type === "ternary_expression" && isInsideJsxExpression(n, node2.id)) {
66918
+ } else {
66919
+ complexity += 1 + nesting;
66920
+ }
66836
66921
  }
66837
66922
  if (n.type === "else_clause") {
66838
66923
  complexity += 1;
66839
66924
  }
66840
66925
  if (n.type === "binary_expression") {
66841
66926
  const op = n.children.find((c2) => c2.type === "&&" || c2.type === "||");
66842
- if (op)
66927
+ if (op && !isInsideJsxExpression(n, node2.id))
66843
66928
  complexity += 1;
66844
66929
  }
66845
66930
  const nextNesting = NESTING_TYPES3.has(n.type) ? nesting + 1 : nesting;
@@ -67351,6 +67436,19 @@ var init_no_proto = __esm({
67351
67436
  });
67352
67437
 
67353
67438
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/no-void.js
67439
+ function operandIsCall2(operand) {
67440
+ let cur = operand;
67441
+ while (cur) {
67442
+ if (cur.type === "call_expression" || cur.type === "new_expression")
67443
+ return true;
67444
+ if (cur.type === "parenthesized_expression" || cur.type === "await_expression") {
67445
+ cur = cur.namedChildren[cur.namedChildren.length - 1] ?? null;
67446
+ continue;
67447
+ }
67448
+ return false;
67449
+ }
67450
+ return false;
67451
+ }
67354
67452
  var noVoidVisitor;
67355
67453
  var init_no_void = __esm({
67356
67454
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/no-void.js"() {
@@ -67367,6 +67465,8 @@ var init_no_void = __esm({
67367
67465
  const operand = node2.children[1];
67368
67466
  if (operand?.text === "0")
67369
67467
  return null;
67468
+ if (operandIsCall2(operand))
67469
+ return null;
67370
67470
  return makeViolation(this.ruleKey, node2, filePath, "low", "Void expression", "The `void` operator is confusing. Use `undefined` directly or omit the return value.", sourceCode, "Replace `void expr` with `undefined` or remove the expression.");
67371
67471
  }
67372
67472
  };
@@ -70901,7 +71001,7 @@ var init_unnamed_regex_capture = __esm({
70901
71001
  visit(node2, filePath, sourceCode) {
70902
71002
  const pattern = node2.namedChildren.find((c2) => c2.type === "regex_pattern");
70903
71003
  const src = pattern?.text ?? "";
70904
- let hasUnnamed = false;
71004
+ let unnamedCount = 0;
70905
71005
  for (let i = 0; i < src.length; i++) {
70906
71006
  if (src[i] === "\\") {
70907
71007
  i++;
@@ -70924,19 +71024,22 @@ var init_unnamed_regex_capture = __esm({
70924
71024
  j2++;
70925
71025
  }
70926
71026
  const groupContent = src.slice(i + 1, j2 - 1);
71027
+ const after = src[j2];
71028
+ if (after === "+" || after === "*" || after === "?" || after === "{") {
71029
+ continue;
71030
+ }
70927
71031
  if (groupContent.includes("|")) {
70928
71032
  const alternatives = groupContent.split("|");
70929
71033
  const isSimpleAlternation = alternatives.every((alt) => /^[a-zA-Z0-9_\\?.^$*+\-]+$/.test(alt) && alt.length <= 10);
70930
71034
  if (isSimpleAlternation)
70931
71035
  continue;
70932
71036
  }
70933
- hasUnnamed = true;
70934
- break;
71037
+ unnamedCount++;
70935
71038
  }
70936
71039
  }
70937
71040
  }
70938
- if (hasUnnamed) {
70939
- return makeViolation(this.ruleKey, node2, filePath, "low", "Unnamed capture group", "Regex contains unnamed capture groups. Use named groups `(?<name>...)` for better readability.", sourceCode, "Convert capture groups to named: `(?<name>...)` or non-capturing: `(?:...)`.");
71041
+ if (unnamedCount >= 2) {
71042
+ return makeViolation(this.ruleKey, node2, filePath, "low", "Unnamed capture groups", "Regex contains multiple unnamed capture groups. Use named groups `(?<name>...)` for better readability.", sourceCode, "Convert capture groups to named: `(?<name>...)` or non-capturing: `(?:...)`.");
70940
71043
  }
70941
71044
  return null;
70942
71045
  }
@@ -71425,6 +71528,25 @@ var init_computed_enum_value = __esm({
71425
71528
  });
71426
71529
 
71427
71530
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/useless-empty-export.js
71531
+ function fileHasOtherTopLevelModuleStatement(emptyExport) {
71532
+ let program3 = emptyExport;
71533
+ while (program3 && program3.type !== "program")
71534
+ program3 = program3.parent;
71535
+ if (!program3)
71536
+ return false;
71537
+ for (const child of program3.namedChildren) {
71538
+ if (child.id === emptyExport.id)
71539
+ continue;
71540
+ if (child.type === "import_statement")
71541
+ return true;
71542
+ if (child.type === "export_statement") {
71543
+ const inner = child.namedChildren.find((c2) => c2.type === "named_exports" || c2.type === "export_clause");
71544
+ if (!inner || inner.namedChildCount > 0)
71545
+ return true;
71546
+ }
71547
+ }
71548
+ return false;
71549
+ }
71428
71550
  var uselessEmptyExportVisitor;
71429
71551
  var init_useless_empty_export = __esm({
71430
71552
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/useless-empty-export.js"() {
@@ -71438,10 +71560,11 @@ var init_useless_empty_export = __esm({
71438
71560
  const namedExports = node2.namedChildren.find((c2) => c2.type === "named_exports" || c2.type === "export_clause");
71439
71561
  if (!namedExports)
71440
71562
  return null;
71441
- if (namedExports.namedChildCount === 0) {
71442
- return makeViolation(this.ruleKey, node2, filePath, "low", "Useless empty export", "`export {}` does nothing useful. Remove it unless it is needed to mark the file as a module.", sourceCode, "Remove the empty `export {}` statement.");
71443
- }
71444
- return null;
71563
+ if (namedExports.namedChildCount !== 0)
71564
+ return null;
71565
+ if (!fileHasOtherTopLevelModuleStatement(node2))
71566
+ return null;
71567
+ return makeViolation(this.ruleKey, node2, filePath, "low", "Useless empty export", "`export {}` does nothing useful. Remove it unless it is needed to mark the file as a module.", sourceCode, "Remove the empty `export {}` statement.");
71445
71568
  }
71446
71569
  };
71447
71570
  }
@@ -71745,6 +71868,14 @@ var init_multiline_block_without_braces = __esm({
71745
71868
  });
71746
71869
 
71747
71870
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/negated-condition.js
71871
+ function statementCount(body) {
71872
+ if (!body)
71873
+ return 0;
71874
+ if (body.type === "statement_block") {
71875
+ return body.namedChildren.filter((c2) => c2.type !== "comment").length;
71876
+ }
71877
+ return 1;
71878
+ }
71748
71879
  var negatedConditionVisitor;
71749
71880
  var init_negated_condition = __esm({
71750
71881
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/negated-condition.js"() {
@@ -71768,6 +71899,11 @@ var init_negated_condition = __esm({
71768
71899
  const elseBody = elsePart.namedChildren[0];
71769
71900
  if (elseBody?.type === "if_statement")
71770
71901
  return null;
71902
+ const ifBody = node2.childForFieldName("consequence");
71903
+ const ifCount = statementCount(ifBody);
71904
+ const elseCount = statementCount(elseBody);
71905
+ if (elseCount < ifCount + 2)
71906
+ return null;
71771
71907
  return makeViolation(this.ruleKey, node2, filePath, "low", "Negated condition with else", "Condition is negated but has an else block. Invert the condition and swap the branches for better readability.", sourceCode, "Invert the condition and swap the if/else bodies.");
71772
71908
  }
71773
71909
  };
@@ -73338,6 +73474,15 @@ var init_undefined_as_identifier = __esm({
73338
73474
  });
73339
73475
 
73340
73476
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/require-unicode-regexp.js
73477
+ function unicodeFlagWouldMatter(pattern) {
73478
+ if (/[^\x00-\x7F]/.test(pattern))
73479
+ return true;
73480
+ if (/\\u\{/.test(pattern))
73481
+ return true;
73482
+ if (/\\[pP]\{/.test(pattern))
73483
+ return true;
73484
+ return false;
73485
+ }
73341
73486
  var requireUnicodeRegexpVisitor;
73342
73487
  var init_require_unicode_regexp = __esm({
73343
73488
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/require-unicode-regexp.js"() {
@@ -73352,6 +73497,9 @@ var init_require_unicode_regexp = __esm({
73352
73497
  const flagText = flags?.text ?? "";
73353
73498
  if (flagText.includes("u") || flagText.includes("v"))
73354
73499
  return null;
73500
+ const pattern = node2.namedChildren.find((c2) => c2.type === "regex_pattern")?.text ?? "";
73501
+ if (!unicodeFlagWouldMatter(pattern))
73502
+ return null;
73355
73503
  return makeViolation(this.ruleKey, node2, filePath, "low", "RegExp missing unicode flag", "Regular expression should use the `u` or `v` flag for correct Unicode character handling.", sourceCode, `Add the \`u\` flag: ${node2.text}u`);
73356
73504
  }
73357
73505
  };
@@ -77217,6 +77365,18 @@ var init_readonly_parameter_types = __esm({
77217
77365
  });
77218
77366
 
77219
77367
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/complex-type-alias.js
77368
+ function flattenUnionMembers(node2) {
77369
+ if (node2.type !== "union_type")
77370
+ return [node2];
77371
+ const out = [];
77372
+ for (const child of node2.namedChildren) {
77373
+ if (child.type === "union_type")
77374
+ out.push(...flattenUnionMembers(child));
77375
+ else
77376
+ out.push(child);
77377
+ }
77378
+ return out;
77379
+ }
77220
77380
  function maxBracketDepth(text) {
77221
77381
  let depth = 0;
77222
77382
  let max = 0;
@@ -77244,11 +77404,20 @@ function countUnionIntersectionMembers(text) {
77244
77404
  }
77245
77405
  return count;
77246
77406
  }
77247
- var DEPTH_THRESHOLD, MEMBER_THRESHOLD, complexTypeAliasVisitor;
77407
+ var SIMPLE_UNION_MEMBER_TYPES, DEPTH_THRESHOLD, MEMBER_THRESHOLD, complexTypeAliasVisitor;
77248
77408
  var init_complex_type_alias = __esm({
77249
77409
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/complex-type-alias.js"() {
77250
77410
  "use strict";
77251
77411
  init_types();
77412
+ SIMPLE_UNION_MEMBER_TYPES = /* @__PURE__ */ new Set([
77413
+ "literal_type",
77414
+ "string",
77415
+ "number",
77416
+ "type_identifier",
77417
+ "predefined_type",
77418
+ "null",
77419
+ "undefined"
77420
+ ]);
77252
77421
  DEPTH_THRESHOLD = 4;
77253
77422
  MEMBER_THRESHOLD = 6;
77254
77423
  complexTypeAliasVisitor = {
@@ -77263,11 +77432,9 @@ var init_complex_type_alias = __esm({
77263
77432
  const depth = maxBracketDepth(typeText);
77264
77433
  const members = countUnionIntersectionMembers(typeText);
77265
77434
  if (typeValue.type === "union_type") {
77266
- const allLiterals = typeValue.namedChildren.every((child) => {
77267
- const t2 = child.type;
77268
- return t2 === "literal_type" || t2 === "string" || t2 === "number" || t2 === "predefined_type" && (child.text === "string" || child.text === "number");
77269
- });
77270
- if (allLiterals)
77435
+ const members2 = flattenUnionMembers(typeValue);
77436
+ const allSimple = members2.every((m) => SIMPLE_UNION_MEMBER_TYPES.has(m.type));
77437
+ if (allSimple)
77271
77438
  return null;
77272
77439
  }
77273
77440
  if (depth >= DEPTH_THRESHOLD || members >= MEMBER_THRESHOLD) {
@@ -128244,7 +128411,7 @@ function readToolVersion() {
128244
128411
  if (cachedVersion)
128245
128412
  return cachedVersion;
128246
128413
  if (true) {
128247
- cachedVersion = "0.6.0-next.8";
128414
+ cachedVersion = "0.6.0";
128248
128415
  return cachedVersion;
128249
128416
  }
128250
128417
  try {
@@ -148507,6 +148674,7 @@ function deleteVerifyDiff(repoPath) {
148507
148674
  }
148508
148675
 
148509
148676
  // packages/core/dist/commands/spec-in-process.js
148677
+ init_telemetry_service();
148510
148678
  function perfNow3() {
148511
148679
  return Number(process.hrtime.bigint() / 1000000n);
148512
148680
  }
@@ -148585,6 +148753,7 @@ function buildScanState(result) {
148585
148753
  }
148586
148754
  async function scanInProcess(repoRoot6, options = {}) {
148587
148755
  const { tracker } = options;
148756
+ const startedAt = Date.now();
148588
148757
  let docsSeen = 0;
148589
148758
  let blocksTotal = 0;
148590
148759
  let blocksDone = 0;
@@ -148701,6 +148870,15 @@ async function scanInProcess(repoRoot6, options = {}) {
148701
148870
  const tWriteStart = perfNow3();
148702
148871
  writeScanState(repoRoot6, scanState);
148703
148872
  debugLog3(`scan: buildScanState=${(tWriteStart - tBuildStart).toFixed(0)}ms writeScanState=${(perfNow3() - tWriteStart).toFixed(0)}ms`);
148873
+ if (options.source) {
148874
+ await trackEvent("spec_scan", {
148875
+ source: options.source,
148876
+ docsScannedRange: bucketFileCount(result.extract.docsScanned),
148877
+ claimsRange: bucketFileCount(result.extract.claims.length),
148878
+ openConflicts: result.merge.openConflicts.length,
148879
+ durationRange: bucketDuration(Date.now() - startedAt)
148880
+ });
148881
+ }
148704
148882
  return { consolidate: result, scanState };
148705
148883
  }
148706
148884
  async function resolveAllDefaultsInProcess(repoRoot6, options = {}) {
@@ -148800,6 +148978,7 @@ function legacyVerifyStatePath(repoRoot6) {
148800
148978
  }
148801
148979
  async function verifyInProcess(repoRoot6, options = {}) {
148802
148980
  const { tracker } = options;
148981
+ const startedAt = Date.now();
148803
148982
  const contractsDir = options.contractsDir ?? path53.join(repoRoot6, ".truecourse", "contracts");
148804
148983
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot6);
148805
148984
  if (!fs46.existsSync(contractsDir)) {
@@ -148871,6 +149050,16 @@ async function verifyInProcess(repoRoot6, options = {}) {
148871
149050
  resolverErrors: result.resolverErrors,
148872
149051
  unresolvedRefs: result.unresolvedRefs
148873
149052
  };
149053
+ if (options.source) {
149054
+ await trackEvent("verify", {
149055
+ source: options.source,
149056
+ mode: "full",
149057
+ artifactCountRange: bucketFileCount(result.artifactCount),
149058
+ operationCountRange: bucketFileCount(result.extractedOperationCount),
149059
+ driftCountRange: bucketFileCount(result.drifts.length),
149060
+ durationRange: bucketDuration(Date.now() - startedAt)
149061
+ });
149062
+ }
148874
149063
  return { verify: result, state };
148875
149064
  }
148876
149065
  async function gitMeta(repoRoot6) {
@@ -148936,6 +149125,7 @@ async function gitChangedFiles(repoRoot6) {
148936
149125
  }
148937
149126
  async function verifyDiffInProcess(repoRoot6, options = {}) {
148938
149127
  const { tracker } = options;
149128
+ const startedAt = Date.now();
148939
149129
  const contractsDir = options.contractsDir ?? path53.join(repoRoot6, ".truecourse", "contracts");
148940
149130
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot6);
148941
149131
  if (!await isGitRepo(repoRoot6)) {
@@ -148983,10 +149173,20 @@ async function verifyDiffInProcess(repoRoot6, options = {}) {
148983
149173
  };
148984
149174
  writeVerifyDiff(repoRoot6, diff);
148985
149175
  tracker?.done("compare", `+${added.length} / -${resolved.length} drift${added.length + resolved.length === 1 ? "" : "s"}`);
149176
+ if (options.source) {
149177
+ await trackEvent("verify", {
149178
+ source: options.source,
149179
+ mode: "diff",
149180
+ addedRange: bucketFileCount(added.length),
149181
+ resolvedRange: bucketFileCount(resolved.length),
149182
+ durationRange: bucketDuration(Date.now() - startedAt)
149183
+ });
149184
+ }
148986
149185
  return { verify: result, diff };
148987
149186
  }
148988
149187
  async function inferInProcess(repoRoot6, options = {}) {
148989
149188
  const { tracker } = options;
149189
+ const startedAt = Date.now();
148990
149190
  const contractsDir = options.contractsDir ?? path53.join(repoRoot6, ".truecourse", "contracts");
148991
149191
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot6);
148992
149192
  tracker?.start("load");
@@ -149006,6 +149206,14 @@ async function inferInProcess(repoRoot6, options = {}) {
149006
149206
  dryRun: options.dryRun
149007
149207
  });
149008
149208
  tracker?.done("write", options.dryRun ? `${proposed.length} would be written` : `${written.length} written`);
149209
+ if (options.source) {
149210
+ await trackEvent("infer", {
149211
+ source: options.source,
149212
+ decisionsRange: bucketFileCount(result.decisions.length),
149213
+ dryRun: !!options.dryRun,
149214
+ durationRange: bucketDuration(Date.now() - startedAt)
149215
+ });
149216
+ }
149009
149217
  return { infer: result, written, proposed };
149010
149218
  }
149011
149219
  function autodetectCodeDir(repoRoot6) {
@@ -149109,6 +149317,7 @@ function removeManualInclude(repoRoot6, docPath) {
149109
149317
  }
149110
149318
 
149111
149319
  // tools/cli/src/commands/contracts.ts
149320
+ init_telemetry_service();
149112
149321
  init_helpers();
149113
149322
 
149114
149323
  // tools/cli/src/commands/git-guard.ts
@@ -149123,6 +149332,7 @@ async function requireGitRepo(root) {
149123
149332
  // tools/cli/src/commands/contracts.ts
149124
149333
  async function runContractsGenerate(options = {}) {
149125
149334
  const repoRoot6 = options.cwd ?? process.cwd();
149335
+ const startedAt = Date.now();
149126
149336
  mt(options.diff ? "Contracts (dry run)" : "Contracts");
149127
149337
  await requireGitRepo(repoRoot6);
149128
149338
  if (!hasCanonicalSpec(repoRoot6)) {
@@ -149214,6 +149424,12 @@ async function runContractsGenerate(options = {}) {
149214
149424
  return;
149215
149425
  }
149216
149426
  stampGeneratedMarker(repoRoot6);
149427
+ await trackEvent("contracts_generate", {
149428
+ source: "cli",
149429
+ artifactsWrittenRange: bucketFileCount(result.write.written.length),
149430
+ validationIssues: result.validationIssues.length,
149431
+ durationRange: bucketDuration(Date.now() - startedAt)
149432
+ });
149217
149433
  if (result.write.written.length === 0) {
149218
149434
  gt("Up to date \u2014 run `truecourse verify`.");
149219
149435
  return;
@@ -149230,23 +149446,51 @@ async function runContractsList(options = {}) {
149230
149446
  O2.info("No contracts found. Run `truecourse contracts generate` first.");
149231
149447
  return;
149232
149448
  }
149233
- const files = [];
149449
+ const { parser: parser4, resolver } = await Promise.resolve().then(() => (init_dist8(), dist_exports3));
149450
+ const fileNodes = [];
149451
+ let parseErrors = 0;
149234
149452
  const visit = (dir) => {
149235
149453
  for (const entry of fs47.readdirSync(dir, { withFileTypes: true })) {
149236
149454
  const full = path54.join(dir, entry.name);
149237
149455
  if (entry.isDirectory()) visit(full);
149238
- else if (entry.isFile() && entry.name.endsWith(".tc")) files.push(full);
149456
+ else if (entry.isFile() && entry.name.endsWith(".tc")) {
149457
+ try {
149458
+ fileNodes.push(parser4.parseFile(full, fs47.readFileSync(full, "utf-8")));
149459
+ } catch {
149460
+ parseErrors += 1;
149461
+ }
149462
+ }
149239
149463
  }
149240
149464
  };
149241
149465
  visit(contractsDir);
149242
- files.sort();
149243
- if (files.length === 0) {
149244
- O2.info("No .tc files in .truecourse/contracts/.");
149466
+ const filter2 = options.inferred && !options.authored ? "inferred" : options.authored && !options.inferred ? "authored" : null;
149467
+ const resolution = resolver.resolve(fileNodes);
149468
+ const artifacts = [...resolution.index.values()].filter((a) => filter2 ? a.provenance === filter2 : true).sort(
149469
+ (a, b) => a.ref.type === b.ref.type ? a.ref.identity.localeCompare(b.ref.identity) : a.ref.type.localeCompare(b.ref.type)
149470
+ );
149471
+ if (artifacts.length === 0) {
149472
+ if (filter2 === "inferred") {
149473
+ O2.info("No inferred contracts. Run `truecourse infer` to reverse-engineer undocumented decisions.");
149474
+ } else if (filter2 === "authored") {
149475
+ O2.info("No authored contracts. Run `truecourse contracts generate` first.");
149476
+ } else {
149477
+ O2.info("No .tc files in .truecourse/contracts/.");
149478
+ }
149245
149479
  return;
149246
149480
  }
149247
- mt(`Contracts (${files.length})`);
149248
- for (const f2 of files) console.log(` ${path54.relative(repoRoot6, f2)}`);
149249
- gt("`truecourse contracts validate` then `truecourse verify`.");
149481
+ const scope = filter2 ? `${filter2} ` : "";
149482
+ mt(`Contracts \u2014 ${artifacts.length} ${scope}artifact${artifacts.length === 1 ? "" : "s"}`);
149483
+ for (const a of artifacts) {
149484
+ const conf = a.confidence ? `[${a.confidence}] ` : "";
149485
+ const loc = a.origin ? a.origin.lines[0] >= 0 ? `${a.origin.source}:${a.origin.lines[0]}` : a.origin.source : `${path54.relative(repoRoot6, a.declarationLoc.filePath)}:${a.declarationLoc.lineStart}`;
149486
+ console.log(` ${conf}${a.ref.type}:${a.ref.identity} ${loc}`);
149487
+ }
149488
+ if (parseErrors > 0) {
149489
+ O2.warn(`${parseErrors} file${parseErrors === 1 ? "" : "s"} could not be parsed \u2014 run \`truecourse contracts validate\`.`);
149490
+ }
149491
+ gt(
149492
+ filter2 ? "`truecourse verify`." : "Filter with `--inferred` / `--authored`. `truecourse contracts validate` then `truecourse verify`."
149493
+ );
149250
149494
  }
149251
149495
  async function runContractsValidate(options = {}) {
149252
149496
  const repoRoot6 = options.cwd ?? process.cwd();
@@ -149378,7 +149622,7 @@ async function runSpecScan(opts = {}) {
149378
149622
  await requireGitRepo(root);
149379
149623
  const { renderer, tracker } = withTracker(SCAN_STEPS);
149380
149624
  try {
149381
- const { consolidate: consolidate2 } = await scanInProcess(root, { tracker });
149625
+ const { consolidate: consolidate2 } = await scanInProcess(root, { tracker, source: "cli" });
149382
149626
  renderer.dispose();
149383
149627
  const { extract: extract4, merge: merge2 } = consolidate2;
149384
149628
  O2.step(`docs ${extract4.docsScanned}`);
@@ -149475,7 +149719,7 @@ async function runVerify(opts = {}) {
149475
149719
  await requireGitRepo(root);
149476
149720
  const { renderer, tracker } = withTracker(VERIFY_STEPS);
149477
149721
  try {
149478
- const { verify: verify2 } = await verifyInProcess(root, { tracker, codeDir: opts.codeDir, skipStash });
149722
+ const { verify: verify2 } = await verifyInProcess(root, { tracker, codeDir: opts.codeDir, skipStash, source: "cli" });
149479
149723
  renderer.dispose();
149480
149724
  O2.step(`artifacts ${verify2.artifactCount}`);
149481
149725
  O2.step(`operations ${verify2.extractedOperationCount} (extracted from code)`);
@@ -149514,7 +149758,7 @@ async function runVerifyDiff(opts) {
149514
149758
  await requireGitRepo(root);
149515
149759
  const { renderer, tracker } = withTracker(VERIFY_STEPS);
149516
149760
  try {
149517
- const { diff } = await verifyDiffInProcess(root, { tracker, codeDir: opts.codeDir });
149761
+ const { diff } = await verifyDiffInProcess(root, { tracker, codeDir: opts.codeDir, source: "cli" });
149518
149762
  renderer.dispose();
149519
149763
  O2.step(`added ${diff.summary.added}`);
149520
149764
  O2.step(`resolved ${diff.summary.resolved}`);
@@ -149548,7 +149792,8 @@ async function runInfer(opts = {}) {
149548
149792
  const { infer: infer2, written, proposed } = await inferInProcess(root, {
149549
149793
  tracker,
149550
149794
  codeDir: opts.codeDir,
149551
- dryRun: opts.dryRun
149795
+ dryRun: opts.dryRun,
149796
+ source: "cli"
149552
149797
  });
149553
149798
  renderer.dispose();
149554
149799
  const byKind = /* @__PURE__ */ new Map();
@@ -149568,7 +149813,7 @@ async function runInfer(opts = {}) {
149568
149813
  }
149569
149814
  const wrote = opts.dryRun ? proposed.length : written.length;
149570
149815
  gt(
149571
- infer2.decisions.length === 0 ? "No undocumented decisions found." : opts.dryRun ? `${wrote} inferred contract${wrote === 1 ? "" : "s"} would be written to _inferred/ (dry run).` : `${wrote} inferred contract${wrote === 1 ? "" : "s"} written to _inferred/.`
149816
+ infer2.decisions.length === 0 ? "No undocumented decisions found." : opts.dryRun ? `${wrote} inferred contract${wrote === 1 ? "" : "s"} would be written to _inferred/ (dry run).` : `${wrote} inferred contract${wrote === 1 ? "" : "s"} written to _inferred/ \u2014 review with \`truecourse contracts list --inferred\`.`
149572
149817
  );
149573
149818
  } catch (e) {
149574
149819
  renderer.dispose();
@@ -152907,7 +153152,7 @@ async function runHooksRun() {
152907
153152
 
152908
153153
  // tools/cli/src/index.ts
152909
153154
  var program2 = new Command();
152910
- program2.name("truecourse").version("0.6.0-next.8").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
153155
+ program2.name("truecourse").version("0.6.0").description("TrueCourse CLI \u2014 analyze your repository and open the dashboard");
152911
153156
  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) => {
152912
153157
  if (options.service && options.console) {
152913
153158
  console.error("error: --service and --console are mutually exclusive");
@@ -152964,8 +153209,8 @@ var contractsCmd = program2.command("contracts").description("Manage spec-driven
152964
153209
  contractsCmd.command("generate").description("Extract .tc artifacts from prose specs (LLM, cached)").option("--diff", "Dry run \u2014 show what would change without writing").action(async (options) => {
152965
153210
  await runContractsGenerate({ diff: !!options.diff });
152966
153211
  });
152967
- contractsCmd.command("list").description("List the .tc artifacts in this repo").action(async () => {
152968
- await runContractsList();
153212
+ contractsCmd.command("list").description("List the .tc artifacts in this repo (kind \xB7 identity \xB7 location)").option("--inferred", "Only inferred artifacts (reverse-engineered, in _inferred/)").option("--authored", "Only authored artifacts (exclude _inferred/)").action(async (options) => {
153213
+ await runContractsList({ inferred: !!options.inferred, authored: !!options.authored });
152969
153214
  });
152970
153215
  contractsCmd.command("validate").description("Parse and resolve all .tc files, report any issues").action(async () => {
152971
153216
  await runContractsValidate();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "truecourse",
3
- "version": "0.6.0-next.8",
3
+ "version": "0.6.0",
4
4
  "description": "Visualize your codebase architecture as an interactive graph",
5
5
  "type": "module",
6
6
  "bin": {
package/server.mjs CHANGED
@@ -42492,10 +42492,10 @@ function computeFunctionMetrics(node2) {
42492
42492
  if (!bodyNode) {
42493
42493
  return { lineCount, statementCount: 0, maxNestingDepth: 0 };
42494
42494
  }
42495
- let statementCount = 0;
42495
+ let statementCount2 = 0;
42496
42496
  for (const child of bodyNode.namedChildren) {
42497
42497
  if (STATEMENT_NODE_TYPES.has(child.type)) {
42498
- statementCount++;
42498
+ statementCount2++;
42499
42499
  }
42500
42500
  }
42501
42501
  let maxNestingDepth2 = 0;
@@ -42512,7 +42512,7 @@ function computeFunctionMetrics(node2) {
42512
42512
  }
42513
42513
  }
42514
42514
  walkNesting(bodyNode, 0);
42515
- return { lineCount, statementCount, maxNestingDepth: maxNestingDepth2 };
42515
+ return { lineCount, statementCount: statementCount2, maxNestingDepth: maxNestingDepth2 };
42516
42516
  }
42517
42517
  var NESTING_NODE_TYPES, STATEMENT_NODE_TYPES;
42518
42518
  var init_common = __esm({
@@ -43410,10 +43410,10 @@ function computePythonFunctionMetrics(node2) {
43410
43410
  if (!bodyNode) {
43411
43411
  return { lineCount, statementCount: 0, maxNestingDepth: 0 };
43412
43412
  }
43413
- let statementCount = 0;
43413
+ let statementCount2 = 0;
43414
43414
  for (const child of bodyNode.namedChildren) {
43415
43415
  if (PYTHON_STATEMENT_NODE_TYPES.has(child.type)) {
43416
- statementCount++;
43416
+ statementCount2++;
43417
43417
  }
43418
43418
  }
43419
43419
  let maxNestingDepth2 = 0;
@@ -43430,7 +43430,7 @@ function computePythonFunctionMetrics(node2) {
43430
43430
  }
43431
43431
  }
43432
43432
  walkNesting(bodyNode, 0);
43433
- return { lineCount, statementCount, maxNestingDepth: maxNestingDepth2 };
43433
+ return { lineCount, statementCount: statementCount2, maxNestingDepth: maxNestingDepth2 };
43434
43434
  }
43435
43435
  function isNestedInFunction3(node2) {
43436
43436
  let current = node2.parent;
@@ -79021,6 +79021,19 @@ var init_prototype_pollution = __esm({
79021
79021
  });
79022
79022
 
79023
79023
  // packages/analyzer/dist/rules/bugs/visitors/javascript/void-zero-argument.js
79024
+ function operandIsCall(operand) {
79025
+ let cur = operand;
79026
+ while (cur) {
79027
+ if (cur.type === "call_expression" || cur.type === "new_expression")
79028
+ return true;
79029
+ if (cur.type === "parenthesized_expression" || cur.type === "await_expression") {
79030
+ cur = cur.namedChildren[cur.namedChildren.length - 1] ?? null;
79031
+ continue;
79032
+ }
79033
+ return false;
79034
+ }
79035
+ return false;
79036
+ }
79024
79037
  var voidZeroArgumentVisitor;
79025
79038
  var init_void_zero_argument = __esm({
79026
79039
  "packages/analyzer/dist/rules/bugs/visitors/javascript/void-zero-argument.js"() {
@@ -79035,6 +79048,8 @@ var init_void_zero_argument = __esm({
79035
79048
  const op = node2.children.find((c) => c.text === "void");
79036
79049
  if (!op)
79037
79050
  return null;
79051
+ if (operandIsCall(node2.children[node2.children.length - 1]))
79052
+ return null;
79038
79053
  return makeViolation(this.ruleKey, node2, filePath, "medium", "Unnecessary void expression", `\`${node2.text}\` can be replaced with \`undefined\` directly.`, sourceCode, "Use `undefined` instead of `void 0`.");
79039
79054
  }
79040
79055
  };
@@ -81857,12 +81872,34 @@ function isModuleLevel(node2) {
81857
81872
  function isMutableInit(node2) {
81858
81873
  return node2.type === "object" || node2.type === "array" || node2.type === "new_expression";
81859
81874
  }
81860
- var sharedMutableModuleStateVisitor;
81875
+ function isClientSideFile(filePath, program) {
81876
+ for (const tok of CLIENT_PATH_TOKENS) {
81877
+ if (filePath.includes(tok))
81878
+ return true;
81879
+ }
81880
+ const first2 = program?.namedChildren[0];
81881
+ if (first2?.type === "expression_statement") {
81882
+ const text = first2.text.trim().replace(/;$/, "").replace(/['"`]/g, "");
81883
+ if (text === "use client")
81884
+ return true;
81885
+ }
81886
+ return false;
81887
+ }
81888
+ var CLIENT_PATH_TOKENS, sharedMutableModuleStateVisitor;
81861
81889
  var init_shared_mutable_module_state = __esm({
81862
81890
  "packages/analyzer/dist/rules/bugs/visitors/javascript/shared-mutable-module-state.js"() {
81863
81891
  "use strict";
81864
81892
  init_types2();
81865
81893
  init_helpers();
81894
+ CLIENT_PATH_TOKENS = [
81895
+ "/client/",
81896
+ "/client-only/",
81897
+ "/hooks/",
81898
+ "/primitives/",
81899
+ "/components/",
81900
+ ".client.ts",
81901
+ ".client.tsx"
81902
+ ];
81866
81903
  sharedMutableModuleStateVisitor = {
81867
81904
  ruleKey: "bugs/deterministic/shared-mutable-module-state",
81868
81905
  languages: JS_LANGUAGES,
@@ -81870,6 +81907,11 @@ var init_shared_mutable_module_state = __esm({
81870
81907
  visit(node2, filePath, sourceCode) {
81871
81908
  if (!isModuleLevel(node2))
81872
81909
  return null;
81910
+ let program = node2;
81911
+ while (program && program.type !== "program")
81912
+ program = program.parent;
81913
+ if (isClientSideFile(filePath, program))
81914
+ return null;
81873
81915
  const kindChild = node2.children[0];
81874
81916
  if (!kindChild || kindChild.text === "const")
81875
81917
  return null;
@@ -83958,13 +84000,38 @@ var init_switch_exhaustiveness = __esm({
83958
84000
  });
83959
84001
 
83960
84002
  // packages/analyzer/dist/rules/bugs/visitors/javascript/non-number-arithmetic.js
83961
- var ARITHMETIC_OPS, nonNumberArithmeticVisitor;
84003
+ function isConfidentlyNonNumeric(t) {
84004
+ if (CONFIDENTLY_NON_NUMERIC.has(t))
84005
+ return true;
84006
+ if (/^["']/.test(t))
84007
+ return true;
84008
+ if (t === "true" || t === "false")
84009
+ return true;
84010
+ if (/\[\]$/.test(t) || /^readonly\s/.test(t))
84011
+ return true;
84012
+ if (t.includes("=>") || t.startsWith("{"))
84013
+ return true;
84014
+ return false;
84015
+ }
84016
+ var ARITHMETIC_OPS, CONFIDENTLY_NON_NUMERIC, nonNumberArithmeticVisitor;
83962
84017
  var init_non_number_arithmetic = __esm({
83963
84018
  "packages/analyzer/dist/rules/bugs/visitors/javascript/non-number-arithmetic.js"() {
83964
84019
  "use strict";
83965
84020
  init_types2();
83966
84021
  init_helpers();
83967
84022
  ARITHMETIC_OPS = /* @__PURE__ */ new Set(["-", "*", "/", "%", "**"]);
84023
+ CONFIDENTLY_NON_NUMERIC = /* @__PURE__ */ new Set([
84024
+ "string",
84025
+ "boolean",
84026
+ "null",
84027
+ "undefined",
84028
+ "void",
84029
+ "symbol",
84030
+ "never",
84031
+ "object",
84032
+ // Common runtime types that don't auto-coerce to number in arithmetic.
84033
+ "Date"
84034
+ ]);
83968
84035
  nonNumberArithmeticVisitor = {
83969
84036
  ruleKey: "bugs/deterministic/non-number-arithmetic",
83970
84037
  languages: TS_LANGUAGES,
@@ -83984,16 +84051,14 @@ var init_non_number_arithmetic = __esm({
83984
84051
  const rightType = typeQuery.getTypeAtPosition(filePath, right.startPosition.row, right.startPosition.column, right.endPosition.row, right.endPosition.column);
83985
84052
  if (!leftType || !rightType)
83986
84053
  return null;
83987
- const numericTypes = /* @__PURE__ */ new Set(["number", "bigint", "any"]);
83988
- const leftOk = numericTypes.has(leftType) || /^\d+$/.test(leftType);
83989
- const rightOk = numericTypes.has(rightType) || /^\d+$/.test(rightType);
83990
84054
  if (left.type === "number" || right.type === "number")
83991
84055
  return null;
83992
- if (!leftOk || !rightOk) {
83993
- const badSide = !leftOk ? `left operand is \`${leftType}\`` : `right operand is \`${rightType}\``;
83994
- return makeViolation(this.ruleKey, node2, filePath, "high", "Non-numeric value in arithmetic", `Arithmetic operator \`${operator.text}\` used with non-numeric operand \u2014 ${badSide}. This will produce \`NaN\` at runtime.`, sourceCode, "Convert operands to numbers or fix the expression.");
83995
- }
83996
- return null;
84056
+ const leftBad = isConfidentlyNonNumeric(leftType);
84057
+ const rightBad = isConfidentlyNonNumeric(rightType);
84058
+ if (!leftBad && !rightBad)
84059
+ return null;
84060
+ const badSide = leftBad ? `left operand is \`${leftType}\`` : `right operand is \`${rightType}\``;
84061
+ return makeViolation(this.ruleKey, node2, filePath, "high", "Non-numeric value in arithmetic", `Arithmetic operator \`${operator.text}\` used with non-numeric operand \u2014 ${badSide}. This will produce \`NaN\` at runtime.`, sourceCode, "Convert operands to numbers or fix the expression.");
83997
84062
  }
83998
84063
  };
83999
84064
  }
@@ -96852,12 +96917,29 @@ var init_no_var_declaration = __esm({
96852
96917
  });
96853
96918
 
96854
96919
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/cognitive-complexity.js
96855
- var cognitiveComplexityVisitor;
96920
+ function isInsideJsxExpression(node2, functionNodeId) {
96921
+ let cur = node2.parent;
96922
+ while (cur && cur.id !== functionNodeId) {
96923
+ if (JSX_EXPRESSION_TYPES.has(cur.type))
96924
+ return true;
96925
+ if (JS_FUNCTION_TYPES.includes(cur.type))
96926
+ return false;
96927
+ cur = cur.parent;
96928
+ }
96929
+ return false;
96930
+ }
96931
+ var JSX_EXPRESSION_TYPES, cognitiveComplexityVisitor;
96856
96932
  var init_cognitive_complexity = __esm({
96857
96933
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/cognitive-complexity.js"() {
96858
96934
  "use strict";
96859
96935
  init_types2();
96860
96936
  init_helpers3();
96937
+ JSX_EXPRESSION_TYPES = /* @__PURE__ */ new Set([
96938
+ "jsx_expression",
96939
+ // {expr} attribute / child
96940
+ "jsx_attribute"
96941
+ // attr={expr}
96942
+ ]);
96861
96943
  cognitiveComplexityVisitor = {
96862
96944
  ruleKey: "code-quality/deterministic/cognitive-complexity",
96863
96945
  languages: ["typescript", "tsx", "javascript"],
@@ -96873,14 +96955,17 @@ var init_cognitive_complexity = __esm({
96873
96955
  if (JS_FUNCTION_TYPES.includes(n.type) && n.id !== node2.id)
96874
96956
  return;
96875
96957
  if (INCREMENT_TYPES.has(n.type)) {
96876
- complexity += 1 + nesting;
96958
+ if (n.type === "ternary_expression" && isInsideJsxExpression(n, node2.id)) {
96959
+ } else {
96960
+ complexity += 1 + nesting;
96961
+ }
96877
96962
  }
96878
96963
  if (n.type === "else_clause") {
96879
96964
  complexity += 1;
96880
96965
  }
96881
96966
  if (n.type === "binary_expression") {
96882
96967
  const op = n.children.find((c) => c.type === "&&" || c.type === "||");
96883
- if (op)
96968
+ if (op && !isInsideJsxExpression(n, node2.id))
96884
96969
  complexity += 1;
96885
96970
  }
96886
96971
  const nextNesting = NESTING_TYPES3.has(n.type) ? nesting + 1 : nesting;
@@ -97392,6 +97477,19 @@ var init_no_proto = __esm({
97392
97477
  });
97393
97478
 
97394
97479
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/no-void.js
97480
+ function operandIsCall2(operand) {
97481
+ let cur = operand;
97482
+ while (cur) {
97483
+ if (cur.type === "call_expression" || cur.type === "new_expression")
97484
+ return true;
97485
+ if (cur.type === "parenthesized_expression" || cur.type === "await_expression") {
97486
+ cur = cur.namedChildren[cur.namedChildren.length - 1] ?? null;
97487
+ continue;
97488
+ }
97489
+ return false;
97490
+ }
97491
+ return false;
97492
+ }
97395
97493
  var noVoidVisitor;
97396
97494
  var init_no_void = __esm({
97397
97495
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/no-void.js"() {
@@ -97408,6 +97506,8 @@ var init_no_void = __esm({
97408
97506
  const operand = node2.children[1];
97409
97507
  if (operand?.text === "0")
97410
97508
  return null;
97509
+ if (operandIsCall2(operand))
97510
+ return null;
97411
97511
  return makeViolation(this.ruleKey, node2, filePath, "low", "Void expression", "The `void` operator is confusing. Use `undefined` directly or omit the return value.", sourceCode, "Replace `void expr` with `undefined` or remove the expression.");
97412
97512
  }
97413
97513
  };
@@ -100942,7 +101042,7 @@ var init_unnamed_regex_capture = __esm({
100942
101042
  visit(node2, filePath, sourceCode) {
100943
101043
  const pattern = node2.namedChildren.find((c) => c.type === "regex_pattern");
100944
101044
  const src = pattern?.text ?? "";
100945
- let hasUnnamed = false;
101045
+ let unnamedCount = 0;
100946
101046
  for (let i = 0; i < src.length; i++) {
100947
101047
  if (src[i] === "\\") {
100948
101048
  i++;
@@ -100965,19 +101065,22 @@ var init_unnamed_regex_capture = __esm({
100965
101065
  j++;
100966
101066
  }
100967
101067
  const groupContent = src.slice(i + 1, j - 1);
101068
+ const after = src[j];
101069
+ if (after === "+" || after === "*" || after === "?" || after === "{") {
101070
+ continue;
101071
+ }
100968
101072
  if (groupContent.includes("|")) {
100969
101073
  const alternatives = groupContent.split("|");
100970
101074
  const isSimpleAlternation = alternatives.every((alt) => /^[a-zA-Z0-9_\\?.^$*+\-]+$/.test(alt) && alt.length <= 10);
100971
101075
  if (isSimpleAlternation)
100972
101076
  continue;
100973
101077
  }
100974
- hasUnnamed = true;
100975
- break;
101078
+ unnamedCount++;
100976
101079
  }
100977
101080
  }
100978
101081
  }
100979
- if (hasUnnamed) {
100980
- return makeViolation(this.ruleKey, node2, filePath, "low", "Unnamed capture group", "Regex contains unnamed capture groups. Use named groups `(?<name>...)` for better readability.", sourceCode, "Convert capture groups to named: `(?<name>...)` or non-capturing: `(?:...)`.");
101082
+ if (unnamedCount >= 2) {
101083
+ return makeViolation(this.ruleKey, node2, filePath, "low", "Unnamed capture groups", "Regex contains multiple unnamed capture groups. Use named groups `(?<name>...)` for better readability.", sourceCode, "Convert capture groups to named: `(?<name>...)` or non-capturing: `(?:...)`.");
100981
101084
  }
100982
101085
  return null;
100983
101086
  }
@@ -101466,6 +101569,25 @@ var init_computed_enum_value = __esm({
101466
101569
  });
101467
101570
 
101468
101571
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/useless-empty-export.js
101572
+ function fileHasOtherTopLevelModuleStatement(emptyExport) {
101573
+ let program = emptyExport;
101574
+ while (program && program.type !== "program")
101575
+ program = program.parent;
101576
+ if (!program)
101577
+ return false;
101578
+ for (const child of program.namedChildren) {
101579
+ if (child.id === emptyExport.id)
101580
+ continue;
101581
+ if (child.type === "import_statement")
101582
+ return true;
101583
+ if (child.type === "export_statement") {
101584
+ const inner = child.namedChildren.find((c) => c.type === "named_exports" || c.type === "export_clause");
101585
+ if (!inner || inner.namedChildCount > 0)
101586
+ return true;
101587
+ }
101588
+ }
101589
+ return false;
101590
+ }
101469
101591
  var uselessEmptyExportVisitor;
101470
101592
  var init_useless_empty_export = __esm({
101471
101593
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/useless-empty-export.js"() {
@@ -101479,10 +101601,11 @@ var init_useless_empty_export = __esm({
101479
101601
  const namedExports = node2.namedChildren.find((c) => c.type === "named_exports" || c.type === "export_clause");
101480
101602
  if (!namedExports)
101481
101603
  return null;
101482
- if (namedExports.namedChildCount === 0) {
101483
- return makeViolation(this.ruleKey, node2, filePath, "low", "Useless empty export", "`export {}` does nothing useful. Remove it unless it is needed to mark the file as a module.", sourceCode, "Remove the empty `export {}` statement.");
101484
- }
101485
- return null;
101604
+ if (namedExports.namedChildCount !== 0)
101605
+ return null;
101606
+ if (!fileHasOtherTopLevelModuleStatement(node2))
101607
+ return null;
101608
+ return makeViolation(this.ruleKey, node2, filePath, "low", "Useless empty export", "`export {}` does nothing useful. Remove it unless it is needed to mark the file as a module.", sourceCode, "Remove the empty `export {}` statement.");
101486
101609
  }
101487
101610
  };
101488
101611
  }
@@ -101786,6 +101909,14 @@ var init_multiline_block_without_braces = __esm({
101786
101909
  });
101787
101910
 
101788
101911
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/negated-condition.js
101912
+ function statementCount(body) {
101913
+ if (!body)
101914
+ return 0;
101915
+ if (body.type === "statement_block") {
101916
+ return body.namedChildren.filter((c) => c.type !== "comment").length;
101917
+ }
101918
+ return 1;
101919
+ }
101789
101920
  var negatedConditionVisitor;
101790
101921
  var init_negated_condition = __esm({
101791
101922
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/negated-condition.js"() {
@@ -101809,6 +101940,11 @@ var init_negated_condition = __esm({
101809
101940
  const elseBody = elsePart.namedChildren[0];
101810
101941
  if (elseBody?.type === "if_statement")
101811
101942
  return null;
101943
+ const ifBody = node2.childForFieldName("consequence");
101944
+ const ifCount = statementCount(ifBody);
101945
+ const elseCount = statementCount(elseBody);
101946
+ if (elseCount < ifCount + 2)
101947
+ return null;
101812
101948
  return makeViolation(this.ruleKey, node2, filePath, "low", "Negated condition with else", "Condition is negated but has an else block. Invert the condition and swap the branches for better readability.", sourceCode, "Invert the condition and swap the if/else bodies.");
101813
101949
  }
101814
101950
  };
@@ -103379,6 +103515,15 @@ var init_undefined_as_identifier = __esm({
103379
103515
  });
103380
103516
 
103381
103517
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/require-unicode-regexp.js
103518
+ function unicodeFlagWouldMatter(pattern) {
103519
+ if (/[^\x00-\x7F]/.test(pattern))
103520
+ return true;
103521
+ if (/\\u\{/.test(pattern))
103522
+ return true;
103523
+ if (/\\[pP]\{/.test(pattern))
103524
+ return true;
103525
+ return false;
103526
+ }
103382
103527
  var requireUnicodeRegexpVisitor;
103383
103528
  var init_require_unicode_regexp = __esm({
103384
103529
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/require-unicode-regexp.js"() {
@@ -103393,6 +103538,9 @@ var init_require_unicode_regexp = __esm({
103393
103538
  const flagText = flags?.text ?? "";
103394
103539
  if (flagText.includes("u") || flagText.includes("v"))
103395
103540
  return null;
103541
+ const pattern = node2.namedChildren.find((c) => c.type === "regex_pattern")?.text ?? "";
103542
+ if (!unicodeFlagWouldMatter(pattern))
103543
+ return null;
103396
103544
  return makeViolation(this.ruleKey, node2, filePath, "low", "RegExp missing unicode flag", "Regular expression should use the `u` or `v` flag for correct Unicode character handling.", sourceCode, `Add the \`u\` flag: ${node2.text}u`);
103397
103545
  }
103398
103546
  };
@@ -107258,6 +107406,18 @@ var init_readonly_parameter_types = __esm({
107258
107406
  });
107259
107407
 
107260
107408
  // packages/analyzer/dist/rules/code-quality/visitors/javascript/complex-type-alias.js
107409
+ function flattenUnionMembers(node2) {
107410
+ if (node2.type !== "union_type")
107411
+ return [node2];
107412
+ const out = [];
107413
+ for (const child of node2.namedChildren) {
107414
+ if (child.type === "union_type")
107415
+ out.push(...flattenUnionMembers(child));
107416
+ else
107417
+ out.push(child);
107418
+ }
107419
+ return out;
107420
+ }
107261
107421
  function maxBracketDepth(text) {
107262
107422
  let depth = 0;
107263
107423
  let max = 0;
@@ -107285,11 +107445,20 @@ function countUnionIntersectionMembers(text) {
107285
107445
  }
107286
107446
  return count;
107287
107447
  }
107288
- var DEPTH_THRESHOLD, MEMBER_THRESHOLD, complexTypeAliasVisitor;
107448
+ var SIMPLE_UNION_MEMBER_TYPES, DEPTH_THRESHOLD, MEMBER_THRESHOLD, complexTypeAliasVisitor;
107289
107449
  var init_complex_type_alias = __esm({
107290
107450
  "packages/analyzer/dist/rules/code-quality/visitors/javascript/complex-type-alias.js"() {
107291
107451
  "use strict";
107292
107452
  init_types2();
107453
+ SIMPLE_UNION_MEMBER_TYPES = /* @__PURE__ */ new Set([
107454
+ "literal_type",
107455
+ "string",
107456
+ "number",
107457
+ "type_identifier",
107458
+ "predefined_type",
107459
+ "null",
107460
+ "undefined"
107461
+ ]);
107293
107462
  DEPTH_THRESHOLD = 4;
107294
107463
  MEMBER_THRESHOLD = 6;
107295
107464
  complexTypeAliasVisitor = {
@@ -107304,11 +107473,9 @@ var init_complex_type_alias = __esm({
107304
107473
  const depth = maxBracketDepth(typeText);
107305
107474
  const members = countUnionIntersectionMembers(typeText);
107306
107475
  if (typeValue.type === "union_type") {
107307
- const allLiterals = typeValue.namedChildren.every((child) => {
107308
- const t = child.type;
107309
- return t === "literal_type" || t === "string" || t === "number" || t === "predefined_type" && (child.text === "string" || child.text === "number");
107310
- });
107311
- if (allLiterals)
107476
+ const members2 = flattenUnionMembers(typeValue);
107477
+ const allSimple = members2.every((m) => SIMPLE_UNION_MEMBER_TYPES.has(m.type));
107478
+ if (allSimple)
107312
107479
  return null;
107313
107480
  }
107314
107481
  if (depth >= DEPTH_THRESHOLD || members >= MEMBER_THRESHOLD) {
@@ -156921,7 +157088,7 @@ function readToolVersion() {
156921
157088
  if (cachedVersion)
156922
157089
  return cachedVersion;
156923
157090
  if (true) {
156924
- cachedVersion = "0.6.0-next.8";
157091
+ cachedVersion = "0.6.0";
156925
157092
  return cachedVersion;
156926
157093
  }
156927
157094
  try {
@@ -176027,6 +176194,7 @@ function buildScanState(result) {
176027
176194
  }
176028
176195
  async function scanInProcess(repoRoot, options = {}) {
176029
176196
  const { tracker } = options;
176197
+ const startedAt = Date.now();
176030
176198
  let docsSeen = 0;
176031
176199
  let blocksTotal = 0;
176032
176200
  let blocksDone = 0;
@@ -176143,6 +176311,15 @@ async function scanInProcess(repoRoot, options = {}) {
176143
176311
  const tWriteStart = perfNow3();
176144
176312
  writeScanState(repoRoot, scanState);
176145
176313
  debugLog3(`scan: buildScanState=${(tWriteStart - tBuildStart).toFixed(0)}ms writeScanState=${(perfNow3() - tWriteStart).toFixed(0)}ms`);
176314
+ if (options.source) {
176315
+ await trackEvent("spec_scan", {
176316
+ source: options.source,
176317
+ docsScannedRange: bucketFileCount(result.extract.docsScanned),
176318
+ claimsRange: bucketFileCount(result.extract.claims.length),
176319
+ openConflicts: result.merge.openConflicts.length,
176320
+ durationRange: bucketDuration(Date.now() - startedAt)
176321
+ });
176322
+ }
176146
176323
  return { consolidate: result, scanState };
176147
176324
  }
176148
176325
  async function resolveAllDefaultsInProcess(repoRoot, options = {}) {
@@ -176238,6 +176415,7 @@ function isChainConflict2(c) {
176238
176415
  }
176239
176416
  async function generateContractsInProcess(repoRoot, options = {}) {
176240
176417
  const { tracker } = options;
176418
+ const startedAt = Date.now();
176241
176419
  if (!hasCanonicalSpec(repoRoot)) {
176242
176420
  tracker?.start("il");
176243
176421
  tracker?.done("il", "skipped \u2014 no canonical spec");
@@ -176285,6 +176463,14 @@ async function generateContractsInProcess(repoRoot, options = {}) {
176285
176463
  if (issueCount === 0) {
176286
176464
  stampGeneratedMarker(repoRoot);
176287
176465
  }
176466
+ if (options.source) {
176467
+ await trackEvent("contracts_generate", {
176468
+ source: options.source,
176469
+ artifactsWrittenRange: bucketFileCount(wrote),
176470
+ validationIssues: issueCount,
176471
+ durationRange: bucketDuration(Date.now() - startedAt)
176472
+ });
176473
+ }
176288
176474
  return { il: { kind: "extracted", result: il } };
176289
176475
  } catch (e) {
176290
176476
  tracker?.error("il", e.message);
@@ -176331,6 +176517,7 @@ function readVerifyRunState(repoRoot, runId) {
176331
176517
  }
176332
176518
  async function verifyInProcess(repoRoot, options = {}) {
176333
176519
  const { tracker } = options;
176520
+ const startedAt = Date.now();
176334
176521
  const contractsDir = options.contractsDir ?? path45.join(repoRoot, ".truecourse", "contracts");
176335
176522
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot);
176336
176523
  if (!fs41.existsSync(contractsDir)) {
@@ -176402,6 +176589,16 @@ async function verifyInProcess(repoRoot, options = {}) {
176402
176589
  resolverErrors: result.resolverErrors,
176403
176590
  unresolvedRefs: result.unresolvedRefs
176404
176591
  };
176592
+ if (options.source) {
176593
+ await trackEvent("verify", {
176594
+ source: options.source,
176595
+ mode: "full",
176596
+ artifactCountRange: bucketFileCount(result.artifactCount),
176597
+ operationCountRange: bucketFileCount(result.extractedOperationCount),
176598
+ driftCountRange: bucketFileCount(result.drifts.length),
176599
+ durationRange: bucketDuration(Date.now() - startedAt)
176600
+ });
176601
+ }
176405
176602
  return { verify: result, state };
176406
176603
  }
176407
176604
  async function gitMeta(repoRoot) {
@@ -176467,6 +176664,7 @@ async function gitChangedFiles(repoRoot) {
176467
176664
  }
176468
176665
  async function verifyDiffInProcess(repoRoot, options = {}) {
176469
176666
  const { tracker } = options;
176667
+ const startedAt = Date.now();
176470
176668
  const contractsDir = options.contractsDir ?? path45.join(repoRoot, ".truecourse", "contracts");
176471
176669
  const codeDir = options.codeDir ?? autodetectCodeDir(repoRoot);
176472
176670
  if (!await isGitRepo(repoRoot)) {
@@ -176514,6 +176712,15 @@ async function verifyDiffInProcess(repoRoot, options = {}) {
176514
176712
  };
176515
176713
  writeVerifyDiff(repoRoot, diff);
176516
176714
  tracker?.done("compare", `+${added.length} / -${resolved.length} drift${added.length + resolved.length === 1 ? "" : "s"}`);
176715
+ if (options.source) {
176716
+ await trackEvent("verify", {
176717
+ source: options.source,
176718
+ mode: "diff",
176719
+ addedRange: bucketFileCount(added.length),
176720
+ resolvedRange: bucketFileCount(resolved.length),
176721
+ durationRange: bucketDuration(Date.now() - startedAt)
176722
+ });
176723
+ }
176517
176724
  return { verify: result, diff };
176518
176725
  }
176519
176726
  function autodetectCodeDir(repoRoot) {
@@ -176646,7 +176853,7 @@ router10.get(
176646
176853
  return;
176647
176854
  }
176648
176855
  const tracker = createSocketSpecTracker(repoIdForCleanup, SCAN_STEPS.map((s) => ({ ...s })));
176649
- const { scanState } = await scanInProcess(repo.path, { tracker });
176856
+ const { scanState } = await scanInProcess(repo.path, { tracker, source: "dashboard" });
176650
176857
  emitSpecComplete(repoIdForCleanup, "scan");
176651
176858
  res.json(scanState);
176652
176859
  } catch (e) {
@@ -176995,7 +177202,7 @@ router11.post(
176995
177202
  repoIdForCleanup,
176996
177203
  GENERATE_STEPS.map((s) => ({ ...s }))
176997
177204
  );
176998
- const outcome = await generateContractsInProcess(repo.path, { tracker });
177205
+ const outcome = await generateContractsInProcess(repo.path, { tracker, source: "dashboard" });
176999
177206
  const response = {};
177000
177207
  if (outcome.il.kind === "extracted") {
177001
177208
  response.il = {
@@ -177082,7 +177289,8 @@ router12.post(
177082
177289
  );
177083
177290
  const { state } = await verifyInProcess(repo.path, {
177084
177291
  tracker,
177085
- skipStash: stashDecision === "no-stash"
177292
+ skipStash: stashDecision === "no-stash",
177293
+ source: "dashboard"
177086
177294
  });
177087
177295
  emitSpecComplete(repoIdForCleanup, "verify");
177088
177296
  res.json(state);
@@ -177168,7 +177376,7 @@ router12.post(
177168
177376
  repoIdForCleanup,
177169
177377
  VERIFY_STEPS.map((s) => ({ ...s }))
177170
177378
  );
177171
- const { diff } = await verifyDiffInProcess(repo.path, { tracker });
177379
+ const { diff } = await verifyDiffInProcess(repo.path, { tracker, source: "dashboard" });
177172
177380
  emitSpecComplete(repoIdForCleanup, "verify");
177173
177381
  res.json(diff);
177174
177382
  } catch (e) {