pi-lens 3.7.1 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +217 -0
  2. package/README.md +706 -619
  3. package/clients/architect-client.ts +7 -2
  4. package/clients/ast-grep-client.ts +7 -1
  5. package/clients/dispatch/plan.ts +10 -4
  6. package/clients/dispatch/runners/architect.ts +20 -7
  7. package/clients/dispatch/runners/ast-grep-napi.ts +5 -2
  8. package/clients/dispatch/runners/ast-grep.ts +29 -18
  9. package/clients/dispatch/runners/biome.ts +4 -4
  10. package/clients/dispatch/runners/python-slop.ts +17 -7
  11. package/clients/dispatch/runners/ruff.ts +4 -4
  12. package/clients/dispatch/runners/tree-sitter.ts +30 -19
  13. package/clients/dispatch/runners/ts-slop.ts +17 -7
  14. package/clients/dispatch/runners/utils/runner-helpers.ts +76 -8
  15. package/clients/dispatch/utils/format-utils.ts +2 -1
  16. package/clients/fix-scanners.ts +8 -8
  17. package/clients/installer/index.ts +19 -1
  18. package/clients/lsp/index.ts +0 -40
  19. package/clients/lsp/launch.ts +5 -2
  20. package/clients/package-root.ts +44 -0
  21. package/clients/pipeline.ts +179 -8
  22. package/clients/scan-utils.ts +20 -32
  23. package/clients/sg-runner.ts +7 -5
  24. package/clients/source-filter.ts +222 -0
  25. package/clients/startup-scan.ts +142 -0
  26. package/clients/todo-scanner.ts +44 -55
  27. package/clients/tree-sitter-cache.ts +315 -0
  28. package/clients/tree-sitter-client.ts +208 -52
  29. package/clients/tree-sitter-fixer.ts +217 -0
  30. package/clients/tree-sitter-navigator.ts +329 -0
  31. package/clients/tree-sitter-query-loader.ts +55 -32
  32. package/commands/booboo.ts +47 -35
  33. package/default-architect.yaml +76 -87
  34. package/docs/ARCHITECTURE.md +74 -0
  35. package/docs/AST_GREP_RULES.md +266 -0
  36. package/docs/COMPLEXITY_METRICS.md +120 -0
  37. package/docs/EXCLUSIONS.md +83 -0
  38. package/docs/LSP_CONFIG.md +240 -0
  39. package/docs/TREE_SITTER_RULES.md +340 -0
  40. package/docs/WRITING_NEW_AST_GREP_RULES.md +200 -0
  41. package/index.ts +209 -86
  42. package/package.json +13 -4
  43. package/rules/ast-grep-rules/rules/array-callback-return-js.yml +33 -0
  44. package/rules/ast-grep-rules/rules/array-callback-return.yml +1 -1
  45. package/rules/ast-grep-rules/rules/constructor-super-js.yml +22 -0
  46. package/rules/ast-grep-rules/rules/empty-catch-js.yml +45 -0
  47. package/rules/ast-grep-rules/rules/empty-catch.yml +1 -1
  48. package/rules/ast-grep-rules/rules/getter-return-js.yml +59 -0
  49. package/rules/ast-grep-rules/rules/getter-return.yml +1 -1
  50. package/rules/ast-grep-rules/rules/hardcoded-url-js.yml +12 -0
  51. package/rules/ast-grep-rules/rules/jsx-boolean-short-circuit.yml +1 -1
  52. package/rules/ast-grep-rules/rules/jwt-no-verify-js.yml +14 -0
  53. package/rules/ast-grep-rules/rules/missed-concurrency-js.yml +25 -0
  54. package/rules/ast-grep-rules/rules/nested-ternary-js.yml +10 -0
  55. package/rules/ast-grep-rules/rules/no-alert-js.yml +6 -0
  56. package/rules/ast-grep-rules/rules/no-architecture-violation.yml +21 -18
  57. package/rules/ast-grep-rules/rules/no-array-constructor-js.yml +10 -0
  58. package/rules/ast-grep-rules/rules/no-async-promise-executor-js.yml +15 -0
  59. package/rules/ast-grep-rules/rules/no-async-promise-executor.yml +1 -1
  60. package/rules/ast-grep-rules/rules/no-await-in-loop-js.yml +30 -0
  61. package/rules/ast-grep-rules/rules/no-await-in-promise-all-js.yml +20 -0
  62. package/rules/ast-grep-rules/rules/no-await-in-promise-all.yml +1 -1
  63. package/rules/ast-grep-rules/rules/no-bare-except.yml +1 -1
  64. package/rules/ast-grep-rules/rules/no-case-declarations-js.yml +16 -0
  65. package/rules/ast-grep-rules/rules/no-compare-neg-zero-js.yml +13 -0
  66. package/rules/ast-grep-rules/rules/no-compare-neg-zero.yml +1 -1
  67. package/rules/ast-grep-rules/rules/no-comparison-to-none.yml +1 -1
  68. package/rules/ast-grep-rules/rules/no-cond-assign-js.yml +36 -0
  69. package/rules/ast-grep-rules/rules/no-cond-assign.yml +1 -1
  70. package/rules/ast-grep-rules/rules/no-constant-condition-js.yml +25 -0
  71. package/rules/ast-grep-rules/rules/no-constant-condition.yml +1 -1
  72. package/rules/ast-grep-rules/rules/no-constructor-return-js.yml +28 -0
  73. package/rules/ast-grep-rules/rules/no-constructor-return.yml +1 -1
  74. package/rules/ast-grep-rules/rules/no-discarded-error-js.yml +25 -0
  75. package/rules/ast-grep-rules/rules/no-discarded-error.yml +25 -0
  76. package/rules/ast-grep-rules/rules/no-dupe-args-js.yml +15 -0
  77. package/rules/ast-grep-rules/rules/no-dupe-keys-js.yml +73 -0
  78. package/rules/ast-grep-rules/rules/no-extra-boolean-cast-js.yml +25 -0
  79. package/rules/ast-grep-rules/rules/no-hardcoded-secrets-js.yml +17 -0
  80. package/rules/ast-grep-rules/rules/no-implied-eval-js.yml +15 -0
  81. package/rules/ast-grep-rules/rules/no-inner-html-js.yml +13 -0
  82. package/rules/ast-grep-rules/rules/no-insecure-randomness-js.yml +20 -0
  83. package/rules/ast-grep-rules/rules/no-insecure-randomness.yml +1 -1
  84. package/rules/ast-grep-rules/rules/no-javascript-url-js.yml +11 -0
  85. package/rules/ast-grep-rules/rules/no-nan-comparison-js.yml +22 -0
  86. package/rules/ast-grep-rules/rules/no-nan-comparison.yml +22 -0
  87. package/rules/ast-grep-rules/rules/no-new-symbol-js.yml +8 -0
  88. package/rules/ast-grep-rules/rules/no-new-wrappers-js.yml +13 -0
  89. package/rules/ast-grep-rules/rules/no-open-redirect-js.yml +15 -0
  90. package/rules/ast-grep-rules/rules/no-prototype-builtins-js.yml +15 -0
  91. package/rules/ast-grep-rules/rules/no-prototype-builtins.yml +1 -1
  92. package/rules/ast-grep-rules/rules/no-sql-in-code-js.yml +13 -0
  93. package/rules/ast-grep-rules/rules/no-sql-in-code.yml +1 -1
  94. package/rules/ast-grep-rules/rules/no-throw-string-js.yml +12 -0
  95. package/rules/ast-grep-rules/rules/no-throw-string.yml +1 -1
  96. package/rules/ast-grep-rules/rules/strict-equality-js.yml +10 -0
  97. package/rules/ast-grep-rules/rules/strict-inequality-js.yml +10 -0
  98. package/rules/ast-grep-rules/rules/toctou-js.yml +112 -0
  99. package/rules/ast-grep-rules/rules/toctou.yml +1 -1
  100. package/rules/ast-grep-rules/rules/unchecked-sync-fs-js.yml +44 -0
  101. package/rules/ast-grep-rules/rules/unchecked-sync-fs.yml +44 -0
  102. package/rules/ast-grep-rules/rules/unchecked-throwing-call-js.yml +31 -0
  103. package/rules/ast-grep-rules/rules/unchecked-throwing-call-python.yml +48 -0
  104. package/rules/ast-grep-rules/rules/unchecked-throwing-call-ruby.yml +47 -0
  105. package/rules/ast-grep-rules/rules/unchecked-throwing-call.yml +31 -0
  106. package/rules/ast-grep-rules/rules/weak-rsa-key-js.yml +15 -0
  107. package/rules/tree-sitter-queries/go/go-bare-error.yml +47 -0
  108. package/rules/tree-sitter-queries/go/go-defer-in-loop.yml +47 -0
  109. package/rules/tree-sitter-queries/go/go-hardcoded-secrets.yml +54 -0
  110. package/rules/tree-sitter-queries/python/is-vs-equals.yml +1 -1
  111. package/rules/tree-sitter-queries/python/python-debugger.yml +46 -0
  112. package/rules/tree-sitter-queries/python/python-empty-except.yml +48 -0
  113. package/rules/tree-sitter-queries/python/python-hardcoded-secrets.yml +44 -0
  114. package/rules/tree-sitter-queries/python/python-mutable-class-attr.yml +57 -0
  115. package/rules/tree-sitter-queries/python/python-print-statement.yml +53 -0
  116. package/rules/tree-sitter-queries/python/python-raise-string.yml +38 -0
  117. package/rules/tree-sitter-queries/python/python-unsafe-regex.yml +58 -0
  118. package/rules/tree-sitter-queries/ruby/ruby-debugger.yml +44 -0
  119. package/rules/tree-sitter-queries/ruby/ruby-empty-rescue.yml +47 -0
  120. package/rules/tree-sitter-queries/ruby/ruby-eval.yml +43 -0
  121. package/rules/tree-sitter-queries/ruby/ruby-hardcoded-secrets.yml +40 -0
  122. package/rules/tree-sitter-queries/ruby/ruby-open-struct.yml +48 -0
  123. package/rules/tree-sitter-queries/ruby/ruby-puts-statement.yml +52 -0
  124. package/rules/tree-sitter-queries/ruby/ruby-rescue-exception.yml +51 -0
  125. package/rules/tree-sitter-queries/ruby/ruby-unsafe-regex.yml +49 -0
  126. package/rules/tree-sitter-queries/rust/rust-clone-in-loop.yml +49 -0
  127. package/rules/tree-sitter-queries/rust/rust-unwrap.yml +45 -0
  128. package/rules/tree-sitter-queries/typescript/console-statement.yml +3 -3
  129. package/rules/tree-sitter-queries/typescript/hardcoded-secrets.yml +13 -27
  130. package/rules/tree-sitter-queries/typescript/injections.scm +40 -0
  131. package/rules/tree-sitter-queries/typescript/no-console-in-tests.yml +52 -0
  132. package/rules/tree-sitter-queries/typescript/sql-injection.yml +55 -0
  133. package/rules/tree-sitter-queries/typescript/unsafe-regex.yml +71 -0
  134. package/rules/tree-sitter-queries/typescript/variable-shadowing.yml +51 -0
  135. package/scripts/download-grammars.ts +78 -0
@@ -24,6 +24,10 @@ import {
24
24
  import { RunnerTracker } from "../clients/runner-tracker.js";
25
25
  import { safeSpawn } from "../clients/safe-spawn.js";
26
26
  import { getSourceFiles } from "../clients/scan-utils.js";
27
+ import {
28
+ collectSourceFiles,
29
+ getFilterStats,
30
+ } from "../clients/source-filter.js";
27
31
  import { calculateSimilarity } from "../clients/state-matrix.js";
28
32
  import type { TodoScanner } from "../clients/todo-scanner.js";
29
33
  import { TreeSitterClient } from "../clients/tree-sitter-client.js";
@@ -89,6 +93,35 @@ export async function handleBooboo(
89
93
  // Detect project type once for all runners
90
94
  const isTsProject = nodeFs.existsSync(path.join(targetPath, "tsconfig.json"));
91
95
 
96
+ // Collect source files once with unified artifact filtering
97
+ // This ensures all scanners work on the same deduplicated file set
98
+ const sourceFiles = collectSourceFiles(targetPath);
99
+ const allFiles = collectSourceFiles(targetPath, {
100
+ extensions: [
101
+ ".ts",
102
+ ".tsx",
103
+ ".js",
104
+ ".jsx",
105
+ ".mjs",
106
+ ".cjs",
107
+ ".py",
108
+ ".go",
109
+ ".rs",
110
+ ".rb",
111
+ ],
112
+ });
113
+ const filterStats = getFilterStats(allFiles, sourceFiles);
114
+
115
+ if (filterStats.skipped > 0) {
116
+ const byTypeStr = Object.entries(filterStats.byType)
117
+ .map(([ext, count]) => `${count} ${ext}`)
118
+ .join(", ");
119
+ // biome-ignore lint/suspicious/noConsole: CLI output
120
+ console.log(
121
+ `[lens-booboo] Filtered ${filterStats.skipped} build artifacts (${byTypeStr}), scanning ${filterStats.kept} source files`,
122
+ );
123
+ }
124
+
92
125
  // Get available commands for the project
93
126
  const availableCommands = getAvailableCommands(projectMeta);
94
127
 
@@ -390,9 +423,8 @@ export async function handleBooboo(
390
423
  const results: import("../clients/complexity-client.js").FileComplexity[] =
391
424
  [];
392
425
  const aiSlopIssues: string[] = [];
393
- const files = getSourceFiles(targetPath, isTsProject).filter(
394
- shouldIncludeFile,
395
- );
426
+ // Use pre-collected sourceFiles (already filtered for artifacts)
427
+ const files = sourceFiles.filter(shouldIncludeFile);
396
428
 
397
429
  for (const fullPath of files) {
398
430
  if (clients.complexity.isSupportedFile(fullPath)) {
@@ -937,40 +969,20 @@ export async function handleBooboo(
937
969
  return { findings: 0, status: "skipped" };
938
970
  }
939
971
 
940
- // Detect TypeScript project - skip .js files in TS projects (compiled artifacts)
941
- const isTsProject = nodeFs.existsSync(
942
- path.join(targetPath, "tsconfig.json"),
943
- );
944
-
945
972
  const archViolations: Array<{ file: string; message: string }> = [];
946
- const archScanDir = (dir: string) => {
947
- for (const entry of nodeFs.readdirSync(dir, { withFileTypes: true })) {
948
- const full = path.join(dir, entry.name);
949
- if (entry.isDirectory()) {
950
- if (EXCLUDED_DIRS.includes(entry.name)) continue;
951
- archScanDir(full);
952
- } else if (/\.(ts|tsx|js|jsx|py|go|rs)$/.test(entry.name)) {
953
- if (isTestFile(full)) continue;
954
- // In TS projects, skip .js files (they're compiled artifacts)
955
- if (
956
- isTsProject &&
957
- /\.(js|jsx)$/.test(entry.name) &&
958
- nodeFs.existsSync(full.replace(/\.(js|jsx)$/, ".ts"))
959
- )
960
- continue;
961
- const relPath = path.relative(targetPath, full).replace(/\\/g, "/");
962
- const content = nodeFs.readFileSync(full, "utf-8");
963
- const lineCount = content.split("\n").length;
964
- for (const v of clients.architect.checkFile(relPath, content)) {
965
- archViolations.push({ file: relPath, message: v.message });
966
- }
967
- const sizeV = clients.architect.checkFileSize(relPath, lineCount);
968
- if (sizeV)
969
- archViolations.push({ file: relPath, message: sizeV.message });
970
- }
973
+
974
+ // Use pre-collected sourceFiles (already filtered for artifacts and exclusions)
975
+ for (const fullPath of sourceFiles) {
976
+ if (isTestFile(fullPath)) continue;
977
+ const relPath = path.relative(targetPath, fullPath).replace(/\\/g, "/");
978
+ const content = nodeFs.readFileSync(fullPath, "utf-8");
979
+ const lineCount = content.split("\n").length;
980
+ for (const v of clients.architect.checkFile(relPath, content)) {
981
+ archViolations.push({ file: relPath, message: v.message });
971
982
  }
972
- };
973
- archScanDir(targetPath);
983
+ const sizeV = clients.architect.checkFileSize(relPath, lineCount);
984
+ if (sizeV) archViolations.push({ file: relPath, message: sizeV.message });
985
+ }
974
986
 
975
987
  if (archViolations.length > 0) {
976
988
  summaryItems.push({
@@ -1,141 +1,130 @@
1
1
  # .pi-lens/architect.yaml
2
- # Architectural rules for your project
3
- # Copy to your project root as .pi-lens/architect.yaml and customize.
2
+ # Architectural rules for your project.
3
+ # Copy to your project root as .pi-lens/architect.yaml and customise.
4
4
  #
5
- # Structure is language-agnostic regex patterns in must_not are language-specific.
5
+ # Patterns use JavaScript regex syntax (multiline mode enabled).
6
6
  #
7
- # IMPORTANT: Patterns are JavaScript regex syntax. Use single quotes for patterns
8
- # containing double quotes, and escape backslashes appropriately.
7
+ # NOTE: Rules here are intentionally non-overlapping with pi-lens built-in runners:
8
+ # - Empty catch blocks -> tree-sitter + ast-grep handle these
9
+ # - Hardcoded secrets -> secrets scanner handles these
10
+ # - as any / :any -> ast-grep no-any-type/no-as-any handle these
9
11
 
10
12
  version: "1.2"
11
13
 
12
- # =============================================================================
13
- # FILE SIZE LIMITS (per file type)
14
- # =============================================================================
14
+ rules:
15
+
16
+ # ===========================================================================
17
+ # FILE SIZE LIMITS
18
+ # ===========================================================================
15
19
 
16
20
  # Services: focused, single-purpose modules
17
21
  - pattern: "**/services/**/*.ts"
18
22
  max_lines: 500
19
23
  must_not:
20
- # TUNED: Was (8+ spaces, 5 lines), now (12+ spaces, 10 lines)
21
24
  - pattern: '(?:\s{12,}.*\n){10,}'
22
- message: "Extreme nesting detected (6+ levels). Refactor into smaller functions."
25
+ message: "Extreme nesting (6+ levels) refactor into smaller functions."
23
26
 
24
- # Clients: can be larger but still bounded
27
+ # Clients/commands: can be larger but still bounded
25
28
  - pattern: "**/clients/**/*.ts|**/commands/**/*.ts"
26
29
  max_lines: 1000
27
30
  must_not:
28
- # TUNED: Was (8+ spaces, 5 lines), now (12+ spaces, 10 lines)
29
31
  - pattern: '(?:\s{12,}.*\n){10,}'
30
- message: "Extreme nesting detected (6+ levels). Refactor into smaller functions."
32
+ message: "Extreme nesting (6+ levels) refactor into smaller functions."
31
33
 
32
- # General limit for other files (higher than before)
34
+ # General source files
33
35
  - pattern: "**/*.{ts,tsx,js,jsx,py,go,rs}"
34
36
  max_lines: 3000
35
37
  must_not:
36
- # TUNED: Was (8+ spaces, 5 lines), now (12+ spaces, 10 lines) for extreme nesting only
37
38
  - pattern: '(?:\s{12,}.*\n){10,}'
38
- message: "Extreme nesting detected (6+ levels). Refactor into smaller functions."
39
+ message: "Extreme nesting (6+ levels) refactor into smaller functions."
39
40
 
40
- # Test files: more relaxed (structure is different)
41
+ # Test files: more relaxed
41
42
  - pattern: "**/*.test.ts|**/*.test.js|**/*.spec.ts|**/*.spec.js"
42
43
  max_lines: 5000
43
44
 
44
- # =============================================================================
45
- # LANGUAGE-AGNOSTIC RULES (The "Universal Truths")
46
- # =============================================================================
45
+ # ===========================================================================
46
+ # UNIVERSAL RULES
47
+ # ===========================================================================
47
48
 
48
- rules:
49
- # --- CI & Environment Safety ---
49
+ # CI & cross-platform safety
50
50
  - pattern: "**/*.{ts,tsx,js,jsx,py,go,rs,java,cpp}"
51
51
  must_not:
52
52
  - pattern: '[a-zA-Z]:\\(?:Users|Program Files|Windows|Temp)\\[^\\]*'
53
53
  message: "No absolute Windows paths — breaks CI and cross-platform builds."
54
54
  fix: "Use path.join(__dirname, 'relative/path') or process.cwd()"
55
55
  - pattern: '/(?:home|Users|usr|etc|var)/[a-zA-Z0-9_-]+/'
56
- message: "Potential absolute Unix path detected — use relative paths or path.join()."
56
+ message: "Absolute Unix path — use relative paths or path.join()."
57
57
  fix: "Use path.join(process.cwd(), 'relative/path') or path.resolve()"
58
58
  - pattern: 'https?://localhost:[0-9]+'
59
- message: "No hardcoded localhost URLs — use environment variables or a config service."
59
+ message: "No hardcoded localhost URLs — use an environment variable."
60
60
  fix: "Use process.env.API_URL || 'http://localhost:3000'"
61
61
 
62
- # --- Complexity & Technical Debt ---
62
+ # Technical debt
63
63
  - pattern: "**/*.{ts,tsx,js,jsx,py,go,rs}"
64
64
  must_not:
65
65
  - pattern: '(?:(?://|#).*\n){10,}'
66
- message: "Large block of commented-out code detected. This is dead code — delete it and rely on Git history."
67
- fix: "Delete the commented code. Git history preserves it if needed later."
66
+ message: "Large commented-out code block — delete it and rely on Git history."
67
+ fix: "Delete the commented code. Git preserves it if needed."
68
68
 
69
- # --- Reliability & Fragility ---
70
- - pattern: "**/*.{ts,tsx,js,jsx,py,go,rs}"
71
- must_not:
72
- - pattern: '(?:catch|except)\s*\(?.*?\)?\s*\{\s*\}'
73
- message: "No empty catch/except blocks. Swallowing errors makes debugging impossible — at least log the error."
74
- fix: |
75
- console.error(`[context] Operation failed:`, err);
76
- // or: throw err;
77
- // or: return null;
78
- - pattern: '\b(?:password|secret|api_?key|token|private_?key)\b\s*[:=]\s*(?:"|'')[^''"]{8,}(?:"|'')'
79
- message: "No hardcoded secrets — use environment variables or a secrets manager."
80
- fix: "Use process.env.SECRET_NAME and add to .env.example (not .env!)"
81
-
82
- # =============================================================================
83
- # JS/TS-SPECIFIC RULES (Node.js & Frontend)
84
- # =============================================================================
85
-
86
- # --- Type Safety ---
87
- - pattern: "**/*.{ts,tsx}"
88
- must_not:
89
- - pattern: ':\s*any\b|as\s+any\b'
90
- message: "No 'any' types — use 'unknown' or define a proper interface to maintain type safety."
91
- fix: |
92
- // Instead of: x as any
93
- x as unknown as SpecificType
94
- // Or define proper interface and use it
95
- must:
96
- - "Use strict TypeScript mode"
97
-
98
- # --- Configuration & IO ---
99
- # REMOVED: no process.env rule - extensions and CLI tools need env access
100
- # If you want this rule for your project, add it to your .pi-lens/architect.yaml:
101
- # - pattern: "**/services/**/*.ts|**/domain/**/*.ts"
102
- # must_not:
103
- # - pattern: 'process\.env'
104
- # message: "Domain/Service logic must not read env vars directly — inject config."
69
+ # ===========================================================================
70
+ # JS/TS-SPECIFIC RULES
71
+ # ===========================================================================
105
72
 
106
- # --- Async Patterns ---
107
- # DISABLED: .then() rule was too aggressive - flagged all promise usage
108
- # Only flag in specific contexts (3+ level chains) via tree-sitter instead
109
- # - pattern: "**/*.{ts,tsx,js,jsx}"
110
- # must_not:
111
- # - pattern: '\.then\('
112
- # message: "Prefer async/await over .then() chains for better readability."
113
-
114
- # --- Grep-ability & Agent Search ---
115
- # Note: 'export default' is acceptable for entry points (index.ts)
73
+ # Deep relative imports hurt readability and agent code search
116
74
  - pattern: "**/*.{ts,tsx}"
117
75
  must_not:
118
- - pattern: "from\\s+['\"]\.\./\.\./\.\./"
119
- message: "Avoid deep relative imports (3+ levels) — use absolute imports (@app/...) for agent reasoning."
76
+ - pattern: "from\\s+['\"](\\.\\./){3,}"
77
+ message: "Deep relative import (3+ levels) — use path aliases instead."
78
+ fix: "Configure tsconfig paths: { '@app/*': ['src/*'] }"
120
79
 
121
- # =============================================================================
122
- # PYTHON-SPECIFIC RULES
123
- # =============================================================================
80
+ # ===========================================================================
81
+ # PYTHON-SPECIFIC RULES
82
+ # ===========================================================================
124
83
 
125
- # --- Clean Python ---
126
84
  - pattern: "**/*.py"
127
85
  must_not:
128
86
  - pattern: 'print\('
129
- message: "No print() statements in production code — use the 'logging' module."
87
+ message: "No print() in production code — use the 'logging' module."
130
88
  - pattern: 'global\s+[a-zA-Z_]'
131
- message: "No 'global' keyword — global state makes code untestable and unpredictable."
89
+ message: "No 'global' keyword — global state makes code untestable."
132
90
  - pattern: 'def\s+[a-zA-Z0-9_]+\([^)]*=\s*\[\]'
133
- message: "No mutable default arguments (e.g., x=[]). Use 'x=None' and initialize inside the function."
91
+ message: "Mutable default argument (x=[]) use x=None and initialise inside."
92
+ - pattern: 'eval\(|exec\('
93
+ message: "No eval()/exec() — security risk and architectural anti-pattern."
94
+
95
+ # ===========================================================================
96
+ # AGENT-SPECIFIC ANTI-PATTERNS
97
+ # Catches common failure modes where agents scaffold but forget to implement.
98
+ # Test files are excluded by the architect runner (skipTestFiles: true).
99
+ # ===========================================================================
100
+
101
+ - pattern: "**/*.{ts,tsx,js,jsx}"
102
+ must_not:
103
+ - pattern: 'throw new Error\((?:"(?:not implemented|TODO|todo|Not implemented)"|''(?:not implemented|TODO|todo|Not implemented)'')\)'
104
+ message: "Stub not implemented — function body was scaffolded but never completed."
105
+ fix: "Implement the function body or remove the stub."
134
106
 
135
- # --- Python Reliability ---
136
107
  - pattern: "**/*.py"
137
108
  must_not:
138
- - pattern: 'except:\s*pass|except\s+Exception:\s*pass'
139
- message: "Do not use bare 'except: pass'. Explicitly catch specific exceptions and log them."
140
- - pattern: 'eval\(|exec\('
141
- message: "No eval() or exec() — these are security risks and architectural anti-patterns."
109
+ - pattern: 'raise NotImplementedError'
110
+ message: "NotImplementedError stub function was scaffolded but never completed."
111
+ fix: "Implement the function body."
112
+ - pattern: '^\s*\.\.\.\s*$'
113
+ message: "Bare ellipsis body — function was scaffolded but never completed."
114
+ fix: "Implement the function body."
115
+
116
+ # ===========================================================================
117
+ # PROJECT-SPECIFIC TEMPLATES (uncomment and customise)
118
+ # ===========================================================================
119
+
120
+ # Layer boundary enforcement — adjust paths to match your project:
121
+ # - pattern: "**/domain/**/*.ts|**/services/**/*.ts"
122
+ # must_not:
123
+ # - pattern: "from\\s+['\"].*/(controllers|routes|api)/"
124
+ # message: "Domain/service layer must not import from API layer."
125
+ #
126
+ # No direct env access in domain layer:
127
+ # - pattern: "**/domain/**/*.ts"
128
+ # must_not:
129
+ # - pattern: 'process\.env'
130
+ # message: "Domain logic must not read env vars — inject config via constructor."
@@ -0,0 +1,74 @@
1
+ # Caching Architecture
2
+
3
+ pi-lens uses a multi-layer caching strategy to avoid redundant work across sessions.
4
+
5
+ ## Cache Layers
6
+
7
+ ### 1. Tool Availability Cache
8
+
9
+ **Location:** `clients/tool-availability.ts`
10
+
11
+ ```
12
+ Map<toolName, {available, version}>
13
+ • Persisted for session lifetime
14
+ • Refreshed on extension restart
15
+ ```
16
+
17
+ Avoids repeated `which`/`where` calls for tools like `biome`, `ruff`, `pyright`.
18
+
19
+ ### 2. Dispatch Baselines (Delta Mode)
20
+
21
+ **Location:** `clients/dispatch/dispatcher.ts`
22
+
23
+ ```
24
+ Map<filePath, Diagnostic[]>
25
+ • Cleared at turn start
26
+ • Updated after each runner execution
27
+ • Filters: only NEW issues shown
28
+ ```
29
+
30
+ First edit shows all issues; subsequent edits only show issues that weren't there before.
31
+
32
+ ### 3. Client-Level Caches
33
+
34
+ | Client | Cache | TTL | Purpose |
35
+ |--------|-------|-----|---------|
36
+ | **Knip** | `clients/cache-manager.ts` | 5 min | Dead code analysis |
37
+ | **jscpd** | `clients/cache-manager.ts` | 5 min | Duplicate detection |
38
+ | **Type Coverage** | In-memory | Session | `any` type percentage |
39
+ | **Complexity** | In-memory | File-level | MI, cognitive complexity |
40
+
41
+ ### 4. Session Turn State
42
+
43
+ **Location:** `clients/cache-manager.ts`
44
+
45
+ Tracks per-turn state:
46
+ - Modified files this turn
47
+ - Modified line ranges per file
48
+ - Import changes detected
49
+ - Turn cycle counter (max 10)
50
+
51
+ Used by:
52
+ - jscpd: Only re-scan modified files
53
+ - Madge: Only check deps if imports changed
54
+ - Cycle detection: Prevents infinite fix loops
55
+
56
+ ### 5. Tree-sitter Caches
57
+
58
+ | Component | Location | Strategy | Details |
59
+ |-----------|----------|----------|---------|
60
+ | **TreeCache** | `clients/tree-sitter-cache.ts` | SHA-256 content hash + mtime | Parsed ASTs cached by file content; 50-file LRU; mtime check for invalidation |
61
+ | **Query Cache** | `clients/tree-sitter-client.ts` | In-memory Map | Compiled tree-sitter queries cached per language |
62
+ | **Navigator** | `clients/tree-sitter-navigator.ts` | Runtime scope detection | Parent/sibling traversal, test block detection, try-catch detection |
63
+
64
+ **TreeCache implementation:**
65
+ - SHA-256 hashing of file content for cache keys
66
+ - mtime tracking for fast invalidation checks
67
+ - LRU eviction when cache exceeds 50 files
68
+ - `incrementalUpdate()` API ready for full incremental parsing when old content is tracked
69
+
70
+ ## Cache Invalidation
71
+
72
+ - **Tool caches:** Refreshed on extension restart
73
+ - **File caches:** Invalidated by mtime change
74
+ - **Turn state:** Reset at each turn start
@@ -0,0 +1,266 @@
1
+ # AST-Grep Rules Reference
2
+
3
+ pi-lens uses **ast-grep** for fast structural pattern matching across multiple languages. This document describes all 112 rules (56 unique patterns × 2 languages: TypeScript and JavaScript).
4
+
5
+ ## Overview
6
+
7
+ **What ast-grep catches:**
8
+ - **Security vulnerabilities** — Hardcoded secrets, SQL injection, unsafe regex, JWT without verification
9
+ - **Runtime errors** — NaN comparison, discarded errors, unchecked throwing calls, TOCTOU
10
+ - **Code quality** — Empty catch blocks, missing returns, long methods, deep nesting
11
+ - **Best practices** — Strict equality, proper error handling, no debugger statements
12
+
13
+ **How it works:**
14
+ 1. Patterns are written in YAML with AST matchers
15
+ 2. Runs via `@ast-grep/napi` (fast Rust core) or CLI ast-grep
16
+ 3. Severity determines blocking vs warning behavior
17
+ 4. Auto-fix available for some rules (applied by Biome/Ruff/ESLint, not ast-grep directly)
18
+
19
+ ---
20
+
21
+ ## Rule Categories
22
+
23
+ ### 🔴 Security (Blocking Errors)
24
+
25
+ Rules that detect vulnerabilities exploitable by attackers or guaranteed runtime crashes.
26
+
27
+ | Rule | Languages | What it catches | Severity |
28
+ |------|-----------|-----------------|----------|
29
+ | **no-hardcoded-secrets** | TS, JS | API keys, passwords, tokens hardcoded in source | 🔴 error |
30
+ | **no-sql-in-code** | TS, JS | SQL queries built with string concatenation | 🔴 error |
31
+ | **jwt-no-verify** | TS, JS | JWT verification without secret/key (accepts any token) | 🔴 error |
32
+ | **weak-rsa-key** | TS, JS | RSA keys < 2048 bits (cryptographically weak) | 🔴 error |
33
+ | **no-insecure-randomness** | TS, JS | `Math.random()` for security (predictable) | 🔴 error |
34
+ | **no-inner-html** | TS, JS | `innerHTML` assignment (XSS risk) | 🔴 error |
35
+ | **unchecked-sync-fs** | TS, JS | `fs.statSync/readFileSync` without try/catch | 🔴 error |
36
+ | **unchecked-throwing-call** | TS, JS | `JSON.parse`, `new URL()`, `execSync` without try/catch | 🔴 error |
37
+ | **unchecked-throwing-call-python** | Python | `open()`, `json.loads()` without try/except | 🔴 error |
38
+ | **unchecked-throwing-call-ruby** | Ruby | `File.read`, `JSON.parse` without begin/rescue | 🔴 error |
39
+ | **no-nan-comparison** | TS, JS | `x === NaN` (always false, use `Number.isNaN()`) | 🔴 error |
40
+ | **no-discarded-error** | TS, JS | `new Error()` as standalone statement (forgot throw) | 🔴 error |
41
+ | **toctou** | TS, JS | Time-of-check-time-of-use race conditions | 🔴 error |
42
+ | **no-throw-string** | TS, JS | `throw "string"` (loses stack trace) | 🔴 error |
43
+ | **no-prototype-builtins** | TS, JS | Calling prototype methods directly on objects | 🔴 error |
44
+
45
+ ### 🔴 Structural Safety (Blocking Errors)
46
+
47
+ | Rule | Languages | What it catches | Severity |
48
+ |------|-----------|-----------------|----------|
49
+ | **empty-catch** | TS, JS | `catch {}` silently swallowing errors | 🔴 error |
50
+ | **no-bare-except** | Python | `except:` catching all exceptions including SystemExit | 🔴 error |
51
+ | **no-cond-assign** | TS, JS | `if (x = y)` assignment in condition | 🔴 error |
52
+ | **no-constant-condition** | TS, JS | `if (true)` or always-true/false conditions | 🔴 error |
53
+ | **no-constructor-return** | TS, JS | `return` statement in constructor | 🔴 error |
54
+ | **no-async-promise-executor** | TS, JS | `new Promise(async () => {})` (error swallowing) | 🔴 error |
55
+ | **no-await-in-promise-all** | TS, JS | `await` inside `Promise.all()` (sequentializes parallel work) | 🔴 error |
56
+ | **no-compare-neg-zero** | TS, JS | `x === -0` (doesn't work as expected) | 🔴 error |
57
+ | **no-comparison-to-none** | Python | `== None` instead of `is None` | 🔴 error |
58
+
59
+ ### 🟡 Code Quality (Warnings)
60
+
61
+ | Rule | Languages | What it catches | Severity |
62
+ |------|-----------|-----------------|----------|
63
+ | **no-console** | TS, JS | `console.log` in production code | 🟡 warning |
64
+ | **no-debugger** | TS, JS | `debugger` statements left in code | 🟡 warning |
65
+ | **no-alert** | TS, JS | `alert()`, `confirm()`, `prompt()` (poor UX) | 🟡 warning |
66
+ | **strict-equality** | TS, JS | `==` instead of `===` | 🟡 warning |
67
+ | **no-await-in-loop** | TS, JS | Sequential await in loops (slow) | 🟡 warning |
68
+ | **missed-concurrency** | TS, JS | Sequential awaits that could be parallel | 🟡 warning |
69
+ | **no-as-any** | TS, JS | `as any` type assertions (unsafe) | 🟡 warning |
70
+ | **no-any-type** | TS | `any` type usage (loses type safety) | 🟡 warning |
71
+ | **long-method** | TS, JS | Functions > 50 lines | 🟡 warning |
72
+ | **long-parameter-list** | TS, JS | Functions with > 5 parameters | 🟡 warning |
73
+ | **nested-ternary** | TS, JS | Ternary nesting > 2 levels | 🟡 warning |
74
+ | **large-class** | TS, JS | Classes > 300 lines | 🟡 warning |
75
+ | **array-callback-return** | TS, JS | Missing return in array callbacks | 🔴 error |
76
+ | **getter-return** | TS, JS | Getter without return statement | 🔴 error |
77
+ | **no-case-declarations** | TS, JS | Variables declared in switch cases | 🟡 warning |
78
+ | **no-array-constructor** | TS, JS | `new Array()` (inconsistent behavior) | 🟡 warning |
79
+ | **jsx-boolean-short-circuit** | TS, JS | `{condition && <Component />}` (0 renders as 0) | 🔴 error |
80
+ | **no-unsafe-optional-chaining** | TS, JS | `a?.b.c` where `a?.b` could be undefined | 🔴 error |
81
+ | **no-unsafe-finally** | TS, JS | `return/throw/break/continue` in finally | 🔴 error |
82
+
83
+ ### 🟡 Python-Specific
84
+
85
+ | Rule | What it catches | Severity |
86
+ |------|-----------------|----------|
87
+ | **no-bare-except** | `except:` without exception type | 🔴 error |
88
+ | **no-comparison-to-none** | `== None` instead of `is None` | 🔴 error |
89
+
90
+ ---
91
+
92
+ ## Severity Philosophy
93
+
94
+ | Level | Signal | Examples |
95
+ |-------|--------|----------|
96
+ | **error** | 🔴 STOP | Security bugs, runtime crashes, NaN comparison, unguarded throwing calls |
97
+ | **warning** | 🟡 | Style issues, readability, performance hints, console/debugger statements |
98
+
99
+ **Why severity matters:**
100
+ - **Errors block the agent** — Must be fixed before proceeding
101
+ - **Warnings accumulate** — Shown in `/lens-booboo` but don't block
102
+ - **Delta tracking** — Only NEW issues shown after first write
103
+
104
+ ---
105
+
106
+ ## Rule Details
107
+
108
+ ### unchecked-throwing-call (Error)
109
+
110
+ **Catches:** `JSON.parse()`, `new URL()`, `execSync()`, `spawnSync()` without try/catch
111
+
112
+ **Why it matters:** These calls throw on invalid input. Without try/catch, they crash the process.
113
+
114
+ **Pattern:**
115
+ ```yaml
116
+ rule:
117
+ any:
118
+ - pattern: JSON.parse($INPUT)
119
+ - pattern: new URL($URL)
120
+ - pattern: execSync($CMD)
121
+ not:
122
+ inside:
123
+ kind: try_statement
124
+ stopBy: end # Check all ancestors, not just immediate parent
125
+ ```
126
+
127
+ **Fix:** Wrap in try/catch with proper error handling.
128
+
129
+ ---
130
+
131
+ ### no-hardcoded-secrets (Error)
132
+
133
+ **Catches:** Hardcoded credentials in variable assignments
134
+
135
+ **Matches:** Variable names matching credential patterns:
136
+ - `password`, `passwd`, `pwd`
137
+ - `secret`, `token`, `apiKey`, `api_secret`
138
+ - `accessKey`, `privateKey`, `clientSecret`
139
+ - `credentials`, `bearer`, `auth`
140
+
141
+ **Pattern:**
142
+ ```yaml
143
+ rule:
144
+ any:
145
+ - pattern: const $VAR = "$_"
146
+ - pattern: const $VAR = '$_'
147
+ constraints:
148
+ VAR:
149
+ regex: "(password|secret|token|apiKey|...)"
150
+ ```
151
+
152
+ **Fix:** Use environment variables: `const apiKey = process.env.API_KEY`
153
+
154
+ ---
155
+
156
+ ### no-nan-comparison (Error)
157
+
158
+ **Catches:** `x === NaN` or `x == NaN`
159
+
160
+ **Why it matters:** `NaN === NaN` is always `false`. Use `Number.isNaN(x)` instead.
161
+
162
+ ---
163
+
164
+ ### no-discarded-error (Error)
165
+
166
+ **Catches:** `new Error("...")` as a standalone statement
167
+
168
+ **Why it matters:** Creates an Error object but doesn't throw it. Usually means `throw` was forgotten.
169
+
170
+ **Fix:** Change to `throw new Error("...")`
171
+
172
+ ---
173
+
174
+ ### toctou (Error)
175
+
176
+ **Catches:** Time-of-check-time-of-use race conditions in file operations
177
+
178
+ **Pattern:** `fs.existsSync(path)` followed by `fs.readFileSync(path)` — file could be modified between check and use.
179
+
180
+ ---
181
+
182
+ ### empty-catch (Error)
183
+
184
+ **Catches:** `catch (e) { }` with empty body
185
+
186
+ **Why it matters:** Swallows errors silently. Makes debugging impossible.
187
+
188
+ **Fix:** Handle the error or remove try/catch if truly don't care.
189
+
190
+ ---
191
+
192
+ ### strict-equality (Warning)
193
+
194
+ **Catches:** `==` and `!=` (loose equality)
195
+
196
+ **Why it matters:** `0 == "0"` is true, `0 == []` is true, `"" == false` is true. Type coercion causes bugs.
197
+
198
+ **Fix:** Use `===` and `!==` always.
199
+
200
+ ---
201
+
202
+ ### no-await-in-loop (Warning)
203
+
204
+ **Catches:** `await` inside `for` loops
205
+
206
+ **Why it matters:** Sequentializes operations that could run in parallel. Slow.
207
+
208
+ **Fix:** Use `Promise.all(array.map(async item => ...))`
209
+
210
+ ---
211
+
212
+ ### missed-concurrency (Warning)
213
+
214
+ **Catches:** Sequential independent awaits
215
+
216
+ **Pattern:**
217
+ ```typescript
218
+ const a = await fetchA(); // Sequential
219
+ const b = await fetchB(); // Doesn't need a's result
220
+ ```
221
+
222
+ **Fix:** `const [a, b] = await Promise.all([fetchA(), fetchB()])`
223
+
224
+ ---
225
+
226
+ ## JavaScript Coverage
227
+
228
+ Most TypeScript rules have JavaScript equivalents (`-js.yml` suffix) because:
229
+ 1. Different AST node types (TypeScript has `type_annotation`, `interface`, etc.)
230
+ 2. JavaScript projects need the same protections
231
+ 3. TS and JS run in separate passes
232
+
233
+ **Total rules:** 112 (56 unique patterns × 2 languages)
234
+
235
+ ---
236
+
237
+ ## Custom Rules
238
+
239
+ Add your own rules to `.pi-lens/rules/`:
240
+
241
+ ```yaml
242
+ # .pi-lens/rules/no-fetch-without-timeout.yml
243
+ id: no-fetch-without-timeout
244
+ language: typescript
245
+ severity: warning
246
+ message: "fetch() without timeout can hang indefinitely"
247
+ rule:
248
+ pattern: fetch($URL)
249
+ not:
250
+ inside:
251
+ pattern: Promise.race([fetch($URL), $TIMEOUT])
252
+ ```
253
+
254
+ **Tips:**
255
+ - Use `$VAR` for single node capture
256
+ - Use `$$$VAR` for multi-node capture
257
+ - Use `not: inside:` for negative context
258
+ - Test with `ast-grep scan --rule your-rule.yml`
259
+
260
+ ---
261
+
262
+ ## References
263
+
264
+ - [ast-grep documentation](https://ast-grep.github.io/)
265
+ - [AST patterns guide](https://ast-grep.github.io/guide/rule-syntax.html)
266
+ - Source: `rules/ast-grep-rules/rules/*.yml`